Repository: trol73/mucommander Branch: develop Commit: fa0963fe3774 Files: 1617 Total size: 23.1 MB Directory structure: gitextract_oxon2gw8/ ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── build.properties ├── build_template.properties ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jni/ │ ├── Makefile │ ├── build.sh │ ├── ru_trolsoft_jni_NativeFileUtils.c │ └── ru_trolsoft_jni_NativeFileUtils.h ├── lib/ │ ├── compile/ │ │ ├── apple/ │ │ │ ├── AppleJavaExtensions-1.6.jar │ │ │ └── ui.jar │ │ ├── checker-qual-3.32.0.jar │ │ ├── jaxb-api-2.3.1.jar │ │ └── jfxrt.jar │ ├── runtime/ │ │ ├── VAqua13.jar │ │ ├── apple/ │ │ │ └── AppleJavaExtensions-1.6.jar │ │ ├── image4j.jar │ │ ├── java-iso-tools/ │ │ │ ├── iso9660-writer-2.0.0.jar │ │ │ └── sabre-2.0.0.jar │ │ ├── javadjvu.jar │ │ ├── jcifs/ │ │ │ └── jcifs-1.3.18-kohsuke-2-SNAPSHOT.jar │ │ ├── jediterm/ │ │ │ ├── JediTerm-3.66-SNAPSHOT.jar │ │ │ ├── annotations.jar │ │ │ ├── jediterm-core-3.66-SNAPSHOT.jar │ │ │ ├── jediterm-ui-3.66-SNAPSHOT.jar │ │ │ ├── jzlib-1.1.1.jar │ │ │ ├── pty4j-0.13.4.jar │ │ │ └── purejavacomm.jar │ │ ├── jets3t/ │ │ │ └── jets3t-0.7.2.jar │ │ ├── jftp-1.60-trol1.jar │ │ ├── jide-oss-3.7.4.jar │ │ ├── quaqua/ │ │ │ ├── libquaqua.jnilib │ │ │ ├── libquaqua64.jnilib │ │ │ ├── quaqua-native.jar │ │ │ └── quaqua.jar │ │ ├── rsyntaxtextarea-3.6.3-SNAPSHOT.jar │ │ ├── sardine-5.3.jar │ │ ├── sevenzipjbinding-AllPlatforms.jar │ │ ├── sevenzipjbinding-Mac-arm64.jar │ │ ├── sevenzipjbinding.jar │ │ ├── trolcommander-native.jar │ │ ├── trolsoft.jar │ │ └── vmware/ │ │ └── vim25-2.5.jar │ └── tools/ │ ├── ant-contrib.jar │ ├── appbundler.jar │ ├── asm/ │ │ └── asm.jar │ ├── file-leak-detector-1.8-jar-with-dependencies.jar │ ├── jaxen/ │ │ └── jaxen.jar │ ├── jaxme/ │ │ └── jaxme-api.jar │ ├── jdeb.jar │ ├── jdom/ │ │ └── jdom.jar │ ├── msv/ │ │ ├── relaxngDatatype.jar │ │ └── xsdlib.jar │ ├── pull-parser/ │ │ └── pull-parser.jar │ ├── stax/ │ │ └── stax-api.jar │ ├── version.txt │ ├── xalan/ │ │ └── xalan.jar │ ├── xerces/ │ │ ├── xercesImpl.jar │ │ └── xmlParserAPIs.jar │ ├── xml-apis/ │ │ └── xml-apis.jar │ ├── xom/ │ │ └── xom.jar │ └── xpp3/ │ └── xpp3.jar ├── readme.txt ├── release-linux.sh ├── release.sh ├── res/ │ ├── jar/ │ │ ├── services/ │ │ │ └── services/ │ │ │ ├── org.apache.hadoop.crypto.key.KeyProviderFactory │ │ │ ├── org.apache.hadoop.fs.FileSystem │ │ │ ├── org.apache.hadoop.io.compress.CompressionCodec │ │ │ ├── org.apache.hadoop.security.SecurityInfo │ │ │ ├── org.apache.hadoop.security.alias.CredentialProviderFactory │ │ │ ├── org.apache.hadoop.security.token.TokenIdentifier │ │ │ └── org.apache.hadoop.security.token.TokenRenewer │ │ ├── servicesCOMMON/ │ │ │ ├── org.apache.hadoop.crypto.key.KeyProviderFactory │ │ │ ├── org.apache.hadoop.fs.FileSystem │ │ │ ├── org.apache.hadoop.io.compress.CompressionCodec │ │ │ ├── org.apache.hadoop.security.SecurityInfo │ │ │ └── org.apache.hadoop.security.alias.CredentialProviderFactory │ │ └── servicesHDFS/ │ │ ├── org.apache.hadoop.fs.FileSystem │ │ ├── org.apache.hadoop.security.token.TokenIdentifier │ │ └── org.apache.hadoop.security.token.TokenRenewer │ └── package/ │ ├── keystore │ ├── osx/ │ │ └── icon.icns │ ├── unix/ │ │ ├── deb/ │ │ │ ├── control │ │ │ ├── postinst │ │ │ └── postrm │ │ ├── trolcommander.desktop │ │ └── trolcommander.sh │ ├── version.xml │ └── windows/ │ └── trolcommander.nsi ├── settings.gradle └── src/ ├── main/ │ ├── java/ │ │ ├── com/ │ │ │ ├── ibm/ │ │ │ │ └── icu/ │ │ │ │ └── text/ │ │ │ │ ├── CharsetDetector.java │ │ │ │ ├── CharsetMatch.java │ │ │ │ ├── CharsetRecog_2022.java │ │ │ │ ├── CharsetRecog_UTF8.java │ │ │ │ ├── CharsetRecog_Unicode.java │ │ │ │ ├── CharsetRecog_mbcs.java │ │ │ │ ├── CharsetRecog_sbcs.java │ │ │ │ └── CharsetRecognizer.java │ │ │ ├── mucommander/ │ │ │ │ ├── PlatformManager.java │ │ │ │ ├── RuntimeConstants.java │ │ │ │ ├── StressTester.java │ │ │ │ ├── TrolCommander.java │ │ │ │ ├── adb/ │ │ │ │ │ ├── AdbUtils.java │ │ │ │ │ └── AndroidMenu.java │ │ │ │ ├── auth/ │ │ │ │ │ ├── CredentialsConstants.java │ │ │ │ │ ├── CredentialsManager.java │ │ │ │ │ ├── CredentialsMapping.java │ │ │ │ │ ├── CredentialsParser.java │ │ │ │ │ ├── CredentialsWriter.java │ │ │ │ │ └── package.html │ │ │ │ ├── bonjour/ │ │ │ │ │ ├── BonjourDirectory.java │ │ │ │ │ ├── BonjourMenu.java │ │ │ │ │ ├── BonjourService.java │ │ │ │ │ └── package.html │ │ │ │ ├── bookmark/ │ │ │ │ │ ├── Bookmark.java │ │ │ │ │ ├── BookmarkBuilder.java │ │ │ │ │ ├── BookmarkConstants.java │ │ │ │ │ ├── BookmarkException.java │ │ │ │ │ ├── BookmarkListener.java │ │ │ │ │ ├── BookmarkManager.java │ │ │ │ │ ├── BookmarkParser.java │ │ │ │ │ ├── BookmarkWriter.java │ │ │ │ │ ├── XORCipher.java │ │ │ │ │ ├── file/ │ │ │ │ │ │ ├── BookmarkFile.java │ │ │ │ │ │ ├── BookmarkOutputStream.java │ │ │ │ │ │ ├── BookmarkProtocolProvider.java │ │ │ │ │ │ ├── BookmarkRoot.java │ │ │ │ │ │ └── package.html │ │ │ │ │ └── package.html │ │ │ │ ├── cache/ │ │ │ │ │ ├── FastLRUCache.java │ │ │ │ │ ├── LRUCache.java │ │ │ │ │ ├── TextHistory.java │ │ │ │ │ ├── WindowsStorage.java │ │ │ │ │ └── package.html │ │ │ │ ├── command/ │ │ │ │ │ ├── AssociationBuilder.java │ │ │ │ │ ├── AssociationFactory.java │ │ │ │ │ ├── AssociationReader.java │ │ │ │ │ ├── AssociationWriter.java │ │ │ │ │ ├── AssociationsXmlConstants.java │ │ │ │ │ ├── Command.java │ │ │ │ │ ├── CommandAssociation.java │ │ │ │ │ ├── CommandBuilder.java │ │ │ │ │ ├── CommandException.java │ │ │ │ │ ├── CommandManager.java │ │ │ │ │ ├── CommandReader.java │ │ │ │ │ ├── CommandType.java │ │ │ │ │ ├── CommandWriter.java │ │ │ │ │ ├── CommandsXmlConstants.java │ │ │ │ │ ├── PermissionsFileFilter.java │ │ │ │ │ └── package.html │ │ │ │ ├── commons/ │ │ │ │ │ ├── DummyDecoratedFile.java │ │ │ │ │ ├── HasProgress.java │ │ │ │ │ ├── collections/ │ │ │ │ │ │ ├── AlteredVector.java │ │ │ │ │ │ ├── Enumerator.java │ │ │ │ │ │ └── VectorChangeListener.java │ │ │ │ │ ├── conf/ │ │ │ │ │ │ ├── BufferedConfigurationExplorer.java │ │ │ │ │ │ ├── Configuration.java │ │ │ │ │ │ ├── ConfigurationBuilder.java │ │ │ │ │ │ ├── ConfigurationEvent.java │ │ │ │ │ │ ├── ConfigurationException.java │ │ │ │ │ │ ├── ConfigurationExplorer.java │ │ │ │ │ │ ├── ConfigurationFormatException.java │ │ │ │ │ │ ├── ConfigurationListener.java │ │ │ │ │ │ ├── ConfigurationReader.java │ │ │ │ │ │ ├── ConfigurationReaderFactory.java │ │ │ │ │ │ ├── ConfigurationSection.java │ │ │ │ │ │ ├── ConfigurationSource.java │ │ │ │ │ │ ├── ConfigurationStructureException.java │ │ │ │ │ │ ├── ConfigurationWriterFactory.java │ │ │ │ │ │ ├── DefaultConfigurationBuilder.java │ │ │ │ │ │ ├── FileConfigurationSource.java │ │ │ │ │ │ ├── ReaderConfigurationException.java │ │ │ │ │ │ ├── SourceConfigurationException.java │ │ │ │ │ │ ├── ValueIterator.java │ │ │ │ │ │ ├── ValueList.java │ │ │ │ │ │ ├── WriterConfigurationException.java │ │ │ │ │ │ ├── XmlConfigurationReader.java │ │ │ │ │ │ ├── XmlConfigurationWriter.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── file/ │ │ │ │ │ │ ├── AbstractArchiveEntryFile.java │ │ │ │ │ │ ├── AbstractArchiveFile.java │ │ │ │ │ │ ├── AbstractFile.java │ │ │ │ │ │ ├── AbstractFileClassLoader.java │ │ │ │ │ │ ├── AbstractROArchiveFile.java │ │ │ │ │ │ ├── AbstractRWArchiveFile.java │ │ │ │ │ │ ├── ArchiveEntry.java │ │ │ │ │ │ ├── ArchiveEntryIterator.java │ │ │ │ │ │ ├── ArchiveEntryTree.java │ │ │ │ │ │ ├── ArchiveFormatProvider.java │ │ │ │ │ │ ├── AuthException.java │ │ │ │ │ │ ├── AuthenticationType.java │ │ │ │ │ │ ├── Authenticator.java │ │ │ │ │ │ ├── Credentials.java │ │ │ │ │ │ ├── DefaultPathCanonizer.java │ │ │ │ │ │ ├── DefaultSchemeHandler.java │ │ │ │ │ │ ├── DefaultSchemeParser.java │ │ │ │ │ │ ├── DummyFile.java │ │ │ │ │ │ ├── FileAccessDeniedException.java │ │ │ │ │ │ ├── FileAttributes.java │ │ │ │ │ │ ├── FileFactory.java │ │ │ │ │ │ ├── FileOperation.java │ │ │ │ │ │ ├── FilePermissions.java │ │ │ │ │ │ ├── FileProtocols.java │ │ │ │ │ │ ├── FileURL.java │ │ │ │ │ │ ├── GroupedPermissionBits.java │ │ │ │ │ │ ├── IndividualPermissionBits.java │ │ │ │ │ │ ├── MacOsSystemFolder.java │ │ │ │ │ │ ├── MimeTypes.java │ │ │ │ │ │ ├── MutableFileAttributes.java │ │ │ │ │ │ ├── PathCanonizer.java │ │ │ │ │ │ ├── PermissionAccesses.java │ │ │ │ │ │ ├── PermissionBits.java │ │ │ │ │ │ ├── PermissionTypes.java │ │ │ │ │ │ ├── ProtocolFile.java │ │ │ │ │ │ ├── ProtocolProvider.java │ │ │ │ │ │ ├── ROArchiveEntryFile.java │ │ │ │ │ │ ├── RWArchiveEntryFile.java │ │ │ │ │ │ ├── SchemeHandler.java │ │ │ │ │ │ ├── SchemeParser.java │ │ │ │ │ │ ├── SevenZipArchiveFormatDetector.java │ │ │ │ │ │ ├── SimpleFileAttributes.java │ │ │ │ │ │ ├── SimpleFilePermissions.java │ │ │ │ │ │ ├── SingleArchiveEntryIterator.java │ │ │ │ │ │ ├── SyncedFileAttributes.java │ │ │ │ │ │ ├── UnsupportedFileOperation.java │ │ │ │ │ │ ├── UnsupportedFileOperationException.java │ │ │ │ │ │ ├── WrapperArchiveEntryIterator.java │ │ │ │ │ │ ├── archiver/ │ │ │ │ │ │ │ ├── ArchiveFormat.java │ │ │ │ │ │ │ ├── Archiver.java │ │ │ │ │ │ │ ├── ISOArchiver.java │ │ │ │ │ │ │ ├── SingleFileArchiver.java │ │ │ │ │ │ │ ├── TarArchiver.java │ │ │ │ │ │ │ └── ZipArchiver.java │ │ │ │ │ │ ├── compat/ │ │ │ │ │ │ │ ├── CompatURLConnection.java │ │ │ │ │ │ │ └── CompatURLStreamHandler.java │ │ │ │ │ │ ├── connection/ │ │ │ │ │ │ │ ├── ConnectionHandler.java │ │ │ │ │ │ │ ├── ConnectionHandlerFactory.java │ │ │ │ │ │ │ └── ConnectionPool.java │ │ │ │ │ │ ├── filter/ │ │ │ │ │ │ │ ├── AbstractContainsFilter.java │ │ │ │ │ │ │ ├── AbstractCriterionFilter.java │ │ │ │ │ │ │ ├── AbstractEndsWithFilter.java │ │ │ │ │ │ │ ├── AbstractEqualsFilter.java │ │ │ │ │ │ │ ├── AbstractExtensionFilter.java │ │ │ │ │ │ │ ├── AbstractFileFilter.java │ │ │ │ │ │ │ ├── AbstractFilenameFilter.java │ │ │ │ │ │ │ ├── AbstractPathFilter.java │ │ │ │ │ │ │ ├── AbstractRegexpFilter.java │ │ │ │ │ │ │ ├── AbstractStartsWithFilter.java │ │ │ │ │ │ │ ├── AbstractStringCriterionFilter.java │ │ │ │ │ │ │ ├── AndFileFilter.java │ │ │ │ │ │ │ ├── AttributeFileFilter.java │ │ │ │ │ │ │ ├── ChainedFileFilter.java │ │ │ │ │ │ │ ├── ContainsFilenameFilter.java │ │ │ │ │ │ │ ├── ContainsPathFilter.java │ │ │ │ │ │ │ ├── CriterionFilter.java │ │ │ │ │ │ │ ├── CriterionValueGenerator.java │ │ │ │ │ │ │ ├── EmptyFileFilter.java │ │ │ │ │ │ │ ├── EndsWithFilenameFilter.java │ │ │ │ │ │ │ ├── EndsWithPathFilter.java │ │ │ │ │ │ │ ├── EqualsFilenameFilter.java │ │ │ │ │ │ │ ├── EqualsPathFilter.java │ │ │ │ │ │ │ ├── ExtensionFilenameFilter.java │ │ │ │ │ │ │ ├── ExtensionPathFilter.java │ │ │ │ │ │ │ ├── FileFilter.java │ │ │ │ │ │ │ ├── FileOperationFilter.java │ │ │ │ │ │ │ ├── FilenameFilter.java │ │ │ │ │ │ │ ├── FilenameGenerator.java │ │ │ │ │ │ │ ├── MountedDriveFilter.java │ │ │ │ │ │ │ ├── OrFileFilter.java │ │ │ │ │ │ │ ├── PassThroughFileFilter.java │ │ │ │ │ │ │ ├── PathFilter.java │ │ │ │ │ │ │ ├── PathGenerator.java │ │ │ │ │ │ │ ├── RegexpFilenameFilter.java │ │ │ │ │ │ │ ├── RegexpPathFilter.java │ │ │ │ │ │ │ ├── StartsWithFilenameFilter.java │ │ │ │ │ │ │ ├── StartsWithPathFilter.java │ │ │ │ │ │ │ ├── StringCriterionFilter.java │ │ │ │ │ │ │ └── WildcardFileFilter.java │ │ │ │ │ │ ├── icon/ │ │ │ │ │ │ │ ├── CacheableFileIconProvider.java │ │ │ │ │ │ │ ├── CachedFileIconProvider.java │ │ │ │ │ │ │ ├── FileIconProvider.java │ │ │ │ │ │ │ ├── IconCache.java │ │ │ │ │ │ │ ├── LocalFileIconProvider.java │ │ │ │ │ │ │ └── impl/ │ │ │ │ │ │ │ ├── SwingFileIconProvider.java │ │ │ │ │ │ │ └── SwingFileIconProviderImpl.java │ │ │ │ │ │ ├── impl/ │ │ │ │ │ │ │ ├── CachedFile.java │ │ │ │ │ │ │ ├── DebugFile.java │ │ │ │ │ │ │ ├── ProxyFile.java │ │ │ │ │ │ │ ├── SevenZipJBindingROArchiveFile.java │ │ │ │ │ │ │ ├── adb/ │ │ │ │ │ │ │ │ ├── AdbFile.java │ │ │ │ │ │ │ │ ├── AdbInputStream.java │ │ │ │ │ │ │ │ └── AdbProtocolProvider.java │ │ │ │ │ │ │ ├── ar/ │ │ │ │ │ │ │ │ ├── ArArchiveEntryIterator.java │ │ │ │ │ │ │ │ ├── ArArchiveFile.java │ │ │ │ │ │ │ │ ├── ArFormatProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── arj/ │ │ │ │ │ │ │ │ └── ArjFormatProvider.java │ │ │ │ │ │ │ ├── avrdude/ │ │ │ │ │ │ │ │ ├── AvrConfigFileUtils.java │ │ │ │ │ │ │ │ ├── AvrDudeInputStream.java │ │ │ │ │ │ │ │ ├── Avrdude.java │ │ │ │ │ │ │ │ ├── AvrdudeConfiguration.java │ │ │ │ │ │ │ │ ├── AvrdudeDevice.java │ │ │ │ │ │ │ │ ├── AvrdudeOutputStream.java │ │ │ │ │ │ │ │ ├── AvrdudeProtocolProvider.java │ │ │ │ │ │ │ │ ├── StreamType.java │ │ │ │ │ │ │ │ └── files/ │ │ │ │ │ │ │ │ ├── AvrConfigFile.java │ │ │ │ │ │ │ │ ├── AvrDeviceDir.java │ │ │ │ │ │ │ │ ├── AvrMemoryDir.java │ │ │ │ │ │ │ │ ├── AvrMemoryFile.java │ │ │ │ │ │ │ │ ├── AvrRootDir.java │ │ │ │ │ │ │ │ └── AvrdudeFile.java │ │ │ │ │ │ │ ├── bzip2/ │ │ │ │ │ │ │ │ ├── Bzip2FormatProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── cab/ │ │ │ │ │ │ │ │ └── CabFormatProvider.java │ │ │ │ │ │ │ ├── cpio/ │ │ │ │ │ │ │ │ └── CpioFormatProvider.java │ │ │ │ │ │ │ ├── deb/ │ │ │ │ │ │ │ │ └── DebFormatProvider.java │ │ │ │ │ │ │ ├── ftp/ │ │ │ │ │ │ │ │ ├── FTPFile.java │ │ │ │ │ │ │ │ ├── FTPProtocolProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── gzip/ │ │ │ │ │ │ │ │ ├── GzipArchiveFile.java │ │ │ │ │ │ │ │ ├── GzipFormatProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── hadoop/ │ │ │ │ │ │ │ │ ├── HDFSFile.java │ │ │ │ │ │ │ │ ├── HDFSProtocolProvider.java │ │ │ │ │ │ │ │ ├── HadoopFile.java │ │ │ │ │ │ │ │ ├── S3File.java │ │ │ │ │ │ │ │ └── S3ProtocolProvider.java │ │ │ │ │ │ │ ├── http/ │ │ │ │ │ │ │ │ ├── HTTPFile.java │ │ │ │ │ │ │ │ ├── HTTPProtocolProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── iso/ │ │ │ │ │ │ │ │ ├── IsoArchiveEntry.java │ │ │ │ │ │ │ │ ├── IsoArchiveFile.java │ │ │ │ │ │ │ │ ├── IsoEntryInputStream.java │ │ │ │ │ │ │ │ ├── IsoEntryIterator.java │ │ │ │ │ │ │ │ ├── IsoFormatProvider.java │ │ │ │ │ │ │ │ ├── IsoParser.java │ │ │ │ │ │ │ │ ├── IsoUtil.java │ │ │ │ │ │ │ │ ├── MuCreateISO.java │ │ │ │ │ │ │ │ ├── MuFileHandler.java │ │ │ │ │ │ │ │ ├── NrgParser.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── local/ │ │ │ │ │ │ │ │ ├── LocalFile.java │ │ │ │ │ │ │ │ ├── LocalProtocolProvider.java │ │ │ │ │ │ │ │ ├── SpecialWindowsLocation.java │ │ │ │ │ │ │ │ ├── UNCFile.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── lst/ │ │ │ │ │ │ │ │ ├── LstArchiveEntry.java │ │ │ │ │ │ │ │ ├── LstArchiveEntryIterator.java │ │ │ │ │ │ │ │ ├── LstArchiveFile.java │ │ │ │ │ │ │ │ ├── LstFormatProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── lzh/ │ │ │ │ │ │ │ │ └── LzhFormatProvider.java │ │ │ │ │ │ │ ├── lzma/ │ │ │ │ │ │ │ │ └── LzmaFormatProvider.java │ │ │ │ │ │ │ ├── nfs/ │ │ │ │ │ │ │ │ ├── NFSFile.java │ │ │ │ │ │ │ │ ├── NFSProtocolProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── rar/ │ │ │ │ │ │ │ │ └── RarFormatProvider.java │ │ │ │ │ │ │ ├── rpm/ │ │ │ │ │ │ │ │ └── RpmFormatProvider.java │ │ │ │ │ │ │ ├── s3/ │ │ │ │ │ │ │ │ ├── S3Bucket.java │ │ │ │ │ │ │ │ ├── S3File.java │ │ │ │ │ │ │ │ ├── S3Object.java │ │ │ │ │ │ │ │ ├── S3ProtocolProvider.java │ │ │ │ │ │ │ │ └── S3Root.java │ │ │ │ │ │ │ ├── sevenzip/ │ │ │ │ │ │ │ │ ├── SevenZipArchiveFile.java │ │ │ │ │ │ │ │ ├── SevenZipFormatProvider.java │ │ │ │ │ │ │ │ ├── SignatureCheckedRandomAccessFile.java │ │ │ │ │ │ │ │ ├── multivolume/ │ │ │ │ │ │ │ │ │ ├── InArchiveWrapper.java │ │ │ │ │ │ │ │ │ ├── SevenZipMultiVolumeCallbackHandler.java │ │ │ │ │ │ │ │ │ └── SevenZipRarMultiVolumeCallbackHandler.java │ │ │ │ │ │ │ │ └── provider/ │ │ │ │ │ │ │ │ ├── Common/ │ │ │ │ │ │ │ │ │ ├── BoolVector.java │ │ │ │ │ │ │ │ │ ├── ByteBuffer.java │ │ │ │ │ │ │ │ │ ├── CRC.java │ │ │ │ │ │ │ │ │ ├── IntVector.java │ │ │ │ │ │ │ │ │ ├── LimitedSequentialInStream.java │ │ │ │ │ │ │ │ │ ├── LockedInStream.java │ │ │ │ │ │ │ │ │ ├── LockedSequentialInStreamImp.java │ │ │ │ │ │ │ │ │ ├── LongVector.java │ │ │ │ │ │ │ │ │ ├── ObjectVector.java │ │ │ │ │ │ │ │ │ └── RecordVector.java │ │ │ │ │ │ │ │ └── SevenZip/ │ │ │ │ │ │ │ │ ├── Archive/ │ │ │ │ │ │ │ │ │ ├── Common/ │ │ │ │ │ │ │ │ │ │ ├── BindInfo.java │ │ │ │ │ │ │ │ │ │ ├── BindPair.java │ │ │ │ │ │ │ │ │ │ ├── CoderInfo.java │ │ │ │ │ │ │ │ │ │ ├── CoderMixer2.java │ │ │ │ │ │ │ │ │ │ ├── CoderMixer2ST.java │ │ │ │ │ │ │ │ │ │ ├── CoderStreamsInfo.java │ │ │ │ │ │ │ │ │ │ ├── FilterCoder.java │ │ │ │ │ │ │ │ │ │ ├── OutStreamWithCRC.java │ │ │ │ │ │ │ │ │ │ └── STCoderInfo.java │ │ │ │ │ │ │ │ │ ├── IArchiveExtractCallback.java │ │ │ │ │ │ │ │ │ ├── IInArchive.java │ │ │ │ │ │ │ │ │ ├── SevenZip/ │ │ │ │ │ │ │ │ │ │ ├── AltCoderInfo.java │ │ │ │ │ │ │ │ │ │ ├── ArchiveDatabase.java │ │ │ │ │ │ │ │ │ │ ├── ArchiveDatabaseEx.java │ │ │ │ │ │ │ │ │ │ ├── BindInfoEx.java │ │ │ │ │ │ │ │ │ │ ├── CoderInfo.java │ │ │ │ │ │ │ │ │ │ ├── Decoder.java │ │ │ │ │ │ │ │ │ │ ├── ExtractFolderInfo.java │ │ │ │ │ │ │ │ │ │ ├── FileItem.java │ │ │ │ │ │ │ │ │ │ ├── Folder.java │ │ │ │ │ │ │ │ │ │ ├── FolderOutStream.java │ │ │ │ │ │ │ │ │ │ ├── Handler.java │ │ │ │ │ │ │ │ │ │ ├── Header.java │ │ │ │ │ │ │ │ │ │ ├── InArchive.java │ │ │ │ │ │ │ │ │ │ ├── InArchiveInfo.java │ │ │ │ │ │ │ │ │ │ ├── InByte2.java │ │ │ │ │ │ │ │ │ │ ├── MethodID.java │ │ │ │ │ │ │ │ │ │ └── StreamSwitch.java │ │ │ │ │ │ │ │ │ └── SevenZipEntry.java │ │ │ │ │ │ │ │ ├── ArchiveExtractCallback.java │ │ │ │ │ │ │ │ ├── Common/ │ │ │ │ │ │ │ │ │ ├── InBuffer.java │ │ │ │ │ │ │ │ │ ├── LocalCompressProgressInfo.java │ │ │ │ │ │ │ │ │ ├── LocalProgress.java │ │ │ │ │ │ │ │ │ ├── SequentialOutStreamImp2.java │ │ │ │ │ │ │ │ │ └── StreamUtils.java │ │ │ │ │ │ │ │ ├── Compression/ │ │ │ │ │ │ │ │ │ ├── Branch/ │ │ │ │ │ │ │ │ │ │ ├── BCJ2_x86_Decoder.java │ │ │ │ │ │ │ │ │ │ └── BCJ_x86_Decoder.java │ │ │ │ │ │ │ │ │ ├── Copy/ │ │ │ │ │ │ │ │ │ │ └── Decoder.java │ │ │ │ │ │ │ │ │ ├── LZ/ │ │ │ │ │ │ │ │ │ │ ├── BinTree.java │ │ │ │ │ │ │ │ │ │ ├── InWindow.java │ │ │ │ │ │ │ │ │ │ └── OutWindow.java │ │ │ │ │ │ │ │ │ ├── LZMA/ │ │ │ │ │ │ │ │ │ │ ├── Base.java │ │ │ │ │ │ │ │ │ │ ├── Decoder.java │ │ │ │ │ │ │ │ │ │ └── Encoder.java │ │ │ │ │ │ │ │ │ └── RangeCoder/ │ │ │ │ │ │ │ │ │ ├── BitDecoder.java │ │ │ │ │ │ │ │ │ ├── BitModel.java │ │ │ │ │ │ │ │ │ ├── BitTreeDecoder.java │ │ │ │ │ │ │ │ │ ├── BitTreeEncoder.java │ │ │ │ │ │ │ │ │ ├── Decoder.java │ │ │ │ │ │ │ │ │ └── Encoder.java │ │ │ │ │ │ │ │ ├── HRESULT.java │ │ │ │ │ │ │ │ ├── ICodeProgress.java │ │ │ │ │ │ │ │ ├── ICompressCoder.java │ │ │ │ │ │ │ │ ├── ICompressCoder2.java │ │ │ │ │ │ │ │ ├── ICompressFilter.java │ │ │ │ │ │ │ │ ├── ICompressGetInStreamProcessedSize.java │ │ │ │ │ │ │ │ ├── ICompressProgressInfo.java │ │ │ │ │ │ │ │ ├── ICompressSetDecoderProperties2.java │ │ │ │ │ │ │ │ ├── ICompressSetInStream.java │ │ │ │ │ │ │ │ ├── ICompressSetOutStream.java │ │ │ │ │ │ │ │ ├── ICompressSetOutStreamSize.java │ │ │ │ │ │ │ │ ├── IInStream.java │ │ │ │ │ │ │ │ ├── IProgress.java │ │ │ │ │ │ │ │ ├── J7zip.java │ │ │ │ │ │ │ │ ├── LzmaAlone.java │ │ │ │ │ │ │ │ ├── LzmaBench.java │ │ │ │ │ │ │ │ └── MyRandomAccessFile.java │ │ │ │ │ │ │ ├── sftp/ │ │ │ │ │ │ │ │ ├── SFTPConnectionHandler.java │ │ │ │ │ │ │ │ ├── SFTPConnectionHandlerFactory.java │ │ │ │ │ │ │ │ ├── SFTPFile.java │ │ │ │ │ │ │ │ ├── SFTPProtocolProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── smb/ │ │ │ │ │ │ │ │ ├── SMBFile.java │ │ │ │ │ │ │ │ ├── SMBProtocolProvider.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── tar/ │ │ │ │ │ │ │ │ ├── TarArchiveFile.java │ │ │ │ │ │ │ │ ├── TarEntryIterator.java │ │ │ │ │ │ │ │ ├── TarFormatProvider.java │ │ │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ │ │ └── provider/ │ │ │ │ │ │ │ │ ├── TarBuffer.java │ │ │ │ │ │ │ │ ├── TarConstants.java │ │ │ │ │ │ │ │ ├── TarEntry.java │ │ │ │ │ │ │ │ ├── TarInputStream.java │ │ │ │ │ │ │ │ ├── TarOutputStream.java │ │ │ │ │ │ │ │ └── TarUtils.java │ │ │ │ │ │ │ ├── udf/ │ │ │ │ │ │ │ │ └── UdfFormatProvider.java │ │ │ │ │ │ │ ├── vsphere/ │ │ │ │ │ │ │ │ ├── ManagedObjectReferenceWrapper.java │ │ │ │ │ │ │ │ ├── VSphereClient.java │ │ │ │ │ │ │ │ ├── VSphereFile.java │ │ │ │ │ │ │ │ ├── VSphereProtocolProvider.java │ │ │ │ │ │ │ │ ├── VsphereConnHandler.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── webdav/ │ │ │ │ │ │ │ │ ├── WebDAVFile.java │ │ │ │ │ │ │ │ └── WebDAVProvider.java │ │ │ │ │ │ │ ├── wim/ │ │ │ │ │ │ │ │ └── WimFormatProvider.java │ │ │ │ │ │ │ ├── xar/ │ │ │ │ │ │ │ │ └── XarFormatProvider.java │ │ │ │ │ │ │ ├── z/ │ │ │ │ │ │ │ │ └── ZFormatProvider.java │ │ │ │ │ │ │ └── zip/ │ │ │ │ │ │ │ ├── JavaUtilZipEntryIterator.java │ │ │ │ │ │ │ ├── ZipArchiveFile.java │ │ │ │ │ │ │ ├── ZipFormatProvider.java │ │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ │ └── provider/ │ │ │ │ │ │ │ ├── AsiExtraField.java │ │ │ │ │ │ │ ├── DeflatedOutputStream.java │ │ │ │ │ │ │ ├── ExtraFieldUtils.java │ │ │ │ │ │ │ ├── JarMarker.java │ │ │ │ │ │ │ ├── StoredOutputStream.java │ │ │ │ │ │ │ ├── UnixStat.java │ │ │ │ │ │ │ ├── UnrecognizedExtraField.java │ │ │ │ │ │ │ ├── ZipBuffer.java │ │ │ │ │ │ │ ├── ZipConstants.java │ │ │ │ │ │ │ ├── ZipEntry.java │ │ │ │ │ │ │ ├── ZipEntryInfo.java │ │ │ │ │ │ │ ├── ZipEntryOutputStream.java │ │ │ │ │ │ │ ├── ZipExtraField.java │ │ │ │ │ │ │ ├── ZipFile.java │ │ │ │ │ │ │ ├── ZipLong.java │ │ │ │ │ │ │ ├── ZipOutputStream.java │ │ │ │ │ │ │ └── ZipShort.java │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ └── util/ │ │ │ │ │ │ ├── C.java │ │ │ │ │ │ ├── CLibrary.java │ │ │ │ │ │ ├── Chmod.java │ │ │ │ │ │ ├── FileChangeListener.java │ │ │ │ │ │ ├── FileComparator.java │ │ │ │ │ │ ├── FileMonitor.java │ │ │ │ │ │ ├── FileMonitorConstants.java │ │ │ │ │ │ ├── FilePool.java │ │ │ │ │ │ ├── FileSet.java │ │ │ │ │ │ ├── Kernel32.java │ │ │ │ │ │ ├── Kernel32API.java │ │ │ │ │ │ ├── OSXFileUtils.java │ │ │ │ │ │ ├── PathTokenizer.java │ │ │ │ │ │ ├── PathUtils.java │ │ │ │ │ │ ├── ResourceLoader.java │ │ │ │ │ │ ├── Shell32.java │ │ │ │ │ │ ├── Shell32API.java │ │ │ │ │ │ └── SymLinkUtils.java │ │ │ │ │ ├── io/ │ │ │ │ │ │ ├── BinaryDetector.java │ │ │ │ │ │ ├── BlockRandomInputStream.java │ │ │ │ │ │ ├── Bounded.java │ │ │ │ │ │ ├── BoundedInputStream.java │ │ │ │ │ │ ├── BoundedOutputStream.java │ │ │ │ │ │ ├── BoundedReader.java │ │ │ │ │ │ ├── BufferPool.java │ │ │ │ │ │ ├── BufferedRandomOutputStream.java │ │ │ │ │ │ ├── ByteCounter.java │ │ │ │ │ │ ├── ByteUtils.java │ │ │ │ │ │ ├── ChecksumInputStream.java │ │ │ │ │ │ ├── ChecksumOutputStream.java │ │ │ │ │ │ ├── CounterInputStream.java │ │ │ │ │ │ ├── CounterOutputStream.java │ │ │ │ │ │ ├── EncodingDetector.java │ │ │ │ │ │ ├── FailSafePipedInputStream.java │ │ │ │ │ │ ├── FileTransferException.java │ │ │ │ │ │ ├── FilterRandomAccessInputStream.java │ │ │ │ │ │ ├── FilteredOutputStream.java │ │ │ │ │ │ ├── FilteredRandomOutputStream.java │ │ │ │ │ │ ├── FixedByteArrayOutputStream.java │ │ │ │ │ │ ├── MultiOutputStream.java │ │ │ │ │ │ ├── RandomAccess.java │ │ │ │ │ │ ├── RandomAccessInputStream.java │ │ │ │ │ │ ├── RandomAccessOutputStream.java │ │ │ │ │ │ ├── RandomGeneratorInputStream.java │ │ │ │ │ │ ├── SilenceableOutputStream.java │ │ │ │ │ │ ├── SinkOutputStream.java │ │ │ │ │ │ ├── StreamOutOfBoundException.java │ │ │ │ │ │ ├── StreamUtils.java │ │ │ │ │ │ ├── ThroughputLimitInputStream.java │ │ │ │ │ │ ├── base64/ │ │ │ │ │ │ │ ├── Base64Decoder.java │ │ │ │ │ │ │ ├── Base64Encoder.java │ │ │ │ │ │ │ ├── Base64InputStream.java │ │ │ │ │ │ │ ├── Base64OutputStream.java │ │ │ │ │ │ │ ├── Base64Table.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── bom/ │ │ │ │ │ │ │ ├── BOM.java │ │ │ │ │ │ │ ├── BOMConstants.java │ │ │ │ │ │ │ ├── BOMInputStream.java │ │ │ │ │ │ │ ├── BOMReader.java │ │ │ │ │ │ │ ├── BOMWriter.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── compound/ │ │ │ │ │ │ │ ├── CompoundInputStream.java │ │ │ │ │ │ │ ├── CompoundReader.java │ │ │ │ │ │ │ ├── IteratorCompoundInputStream.java │ │ │ │ │ │ │ └── IteratorCompoundReader.java │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ └── security/ │ │ │ │ │ │ ├── Adler32MessageDigest.java │ │ │ │ │ │ ├── CRC32MessageDigest.java │ │ │ │ │ │ ├── ChecksumMessageDigest.java │ │ │ │ │ │ ├── MuProvider.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── runtime/ │ │ │ │ │ │ ├── ComparableRuntimeProperty.java │ │ │ │ │ │ ├── JavaVersion.java │ │ │ │ │ │ ├── OsFamily.java │ │ │ │ │ │ ├── OsVersion.java │ │ │ │ │ │ └── package.html │ │ │ │ │ └── util/ │ │ │ │ │ ├── BufferOverflowException.java │ │ │ │ │ ├── CircularByteBuffer.java │ │ │ │ │ ├── Pair.java │ │ │ │ │ ├── StringUtils.java │ │ │ │ │ └── package-info.java │ │ │ │ ├── conf/ │ │ │ │ │ ├── TcConfigurationFile.java │ │ │ │ │ ├── TcConfigurations.java │ │ │ │ │ ├── TcPreference.java │ │ │ │ │ ├── TcPreferences.java │ │ │ │ │ ├── TcPreferencesAPI.java │ │ │ │ │ ├── TcPreferencesFile.java │ │ │ │ │ ├── TcSnapshot.java │ │ │ │ │ ├── TcSnapshotFile.java │ │ │ │ │ ├── VersionedXmlConfigurationReader.java │ │ │ │ │ ├── VersionedXmlConfigurationReaderFactory.java │ │ │ │ │ ├── VersionedXmlConfigurationWriter.java │ │ │ │ │ ├── VersionedXmlConfigurationWriterFactory.java │ │ │ │ │ └── package.html │ │ │ │ ├── core/ │ │ │ │ │ ├── FolderChangeMonitor.java │ │ │ │ │ ├── GlobalLocationHistory.java │ │ │ │ │ ├── LocalLocationHistory.java │ │ │ │ │ ├── LocationChanger.java │ │ │ │ │ └── NullableFile.java │ │ │ │ ├── desktop/ │ │ │ │ │ ├── AbstractTrash.java │ │ │ │ │ ├── CommandBrowse.java │ │ │ │ │ ├── CommandOpen.java │ │ │ │ │ ├── CommandOpenInFileManager.java │ │ │ │ │ ├── DefaultDesktopAdapter.java │ │ │ │ │ ├── DesktopAdapter.java │ │ │ │ │ ├── DesktopInitializationException.java │ │ │ │ │ ├── DesktopManager.java │ │ │ │ │ ├── DesktopOperation.java │ │ │ │ │ ├── InternalBrowse.java │ │ │ │ │ ├── InternalOpen.java │ │ │ │ │ ├── LocalFileOperation.java │ │ │ │ │ ├── QueuedTrash.java │ │ │ │ │ ├── TrashProvider.java │ │ │ │ │ ├── UrlOperation.java │ │ │ │ │ ├── gnome/ │ │ │ │ │ │ ├── ConfiguredGnomeDesktopAdapter.java │ │ │ │ │ │ ├── GnomeConfig.java │ │ │ │ │ │ ├── GnomeDesktopAdapter.java │ │ │ │ │ │ ├── GnomeTrash.java │ │ │ │ │ │ ├── GnomeTrashProvider.java │ │ │ │ │ │ └── GuessedGnomeDesktopAdapter.java │ │ │ │ │ ├── kde/ │ │ │ │ │ │ ├── ConfiguredKde3DesktopAdapter.java │ │ │ │ │ │ ├── ConfiguredKde4DesktopAdapter.java │ │ │ │ │ │ ├── GuessedKde3DesktopAdapter.java │ │ │ │ │ │ ├── GuessedKde4DesktopAdapter.java │ │ │ │ │ │ ├── Kde3DesktopAdapter.java │ │ │ │ │ │ ├── Kde3TrashProvider.java │ │ │ │ │ │ ├── Kde4DesktopAdapter.java │ │ │ │ │ │ ├── Kde4TrashProvider.java │ │ │ │ │ │ ├── KdeConfig.java │ │ │ │ │ │ ├── KdeDesktopAdapter.java │ │ │ │ │ │ └── KdeTrash.java │ │ │ │ │ ├── macos/ │ │ │ │ │ │ ├── OSXApplications.java │ │ │ │ │ │ ├── OSXDesktopAdapter.java │ │ │ │ │ │ ├── OSXTerminal.kt │ │ │ │ │ │ ├── OSXTrash.java │ │ │ │ │ │ └── OSXTrashProvider.java │ │ │ │ │ ├── openvms/ │ │ │ │ │ │ └── OpenVMSDesktopAdapter.java │ │ │ │ │ ├── windows/ │ │ │ │ │ │ ├── Win9xDesktopAdapter.java │ │ │ │ │ │ ├── WinNtDesktopAdapter.java │ │ │ │ │ │ ├── WindowsDesktopAdapter.java │ │ │ │ │ │ ├── WindowsTrash.java │ │ │ │ │ │ └── WindowsTrashProvider.java │ │ │ │ │ └── xfce/ │ │ │ │ │ ├── GuessedXfceDesktopAdapter.java │ │ │ │ │ ├── XfceDesktopAdapter.java │ │ │ │ │ ├── XfceTrash.java │ │ │ │ │ └── XfceTrashProvider.java │ │ │ │ ├── extension/ │ │ │ │ │ ├── ClassFilter.java │ │ │ │ │ ├── ClassFinder.java │ │ │ │ │ ├── ExtensionManager.java │ │ │ │ │ ├── LookAndFeelFilter.java │ │ │ │ │ └── package.html │ │ │ │ ├── io/ │ │ │ │ │ ├── backup/ │ │ │ │ │ │ ├── BackupConstants.java │ │ │ │ │ │ ├── BackupInputStream.java │ │ │ │ │ │ └── BackupOutputStream.java │ │ │ │ │ └── package.html │ │ │ │ ├── job/ │ │ │ │ │ ├── AbstractCopyJob.java │ │ │ │ │ ├── ArchiveJob.java │ │ │ │ │ ├── BatchRenameJob.java │ │ │ │ │ ├── CalculateChecksumJob.java │ │ │ │ │ ├── ChangeFileAttributesJob.java │ │ │ │ │ ├── CombineFilesJob.java │ │ │ │ │ ├── CopyJob.java │ │ │ │ │ ├── DeleteJob.java │ │ │ │ │ ├── FileCollisionChecker.java │ │ │ │ │ ├── FileJob.java │ │ │ │ │ ├── FileJobException.java │ │ │ │ │ ├── FileJobListener.java │ │ │ │ │ ├── FindFileJob.java │ │ │ │ │ ├── MakeDirectoryFileJob.java │ │ │ │ │ ├── MoveJob.java │ │ │ │ │ ├── PropertiesJob.java │ │ │ │ │ ├── SelfUpdateJob.java │ │ │ │ │ ├── SendMailJob.java │ │ │ │ │ ├── SplitFileJob.java │ │ │ │ │ ├── TempCopyJob.java │ │ │ │ │ ├── TempExecJob.java │ │ │ │ │ ├── TempOpenWithJob.java │ │ │ │ │ ├── TransferFileJob.java │ │ │ │ │ ├── UnpackJob.java │ │ │ │ │ ├── progress/ │ │ │ │ │ │ ├── JobProgress.java │ │ │ │ │ │ ├── JobProgressListener.java │ │ │ │ │ │ └── JobProgressMonitor.java │ │ │ │ │ ├── ui/ │ │ │ │ │ │ ├── DialogResult.java │ │ │ │ │ │ └── UserInputHelper.java │ │ │ │ │ └── utils/ │ │ │ │ │ └── ScanDirectoryThread.java │ │ │ │ ├── launcher/ │ │ │ │ │ ├── LauncherCmdHelper.java │ │ │ │ │ ├── LauncherExecutor.kt │ │ │ │ │ ├── LauncherTask.kt │ │ │ │ │ ├── ShutdownHook.java │ │ │ │ │ └── tasks.kt │ │ │ │ ├── package.html │ │ │ │ ├── process/ │ │ │ │ │ ├── AbstractProcess.java │ │ │ │ │ ├── DebugProcessListener.java │ │ │ │ │ ├── ExecutionFinishListener.java │ │ │ │ │ ├── ExecutorUtils.java │ │ │ │ │ ├── LocalProcess.java │ │ │ │ │ ├── ProcessListener.java │ │ │ │ │ ├── ProcessListenerList.java │ │ │ │ │ ├── ProcessOutputMonitor.java │ │ │ │ │ ├── ProcessRunner.java │ │ │ │ │ └── package.html │ │ │ │ ├── profiler/ │ │ │ │ │ └── Profiler.java │ │ │ │ ├── shell/ │ │ │ │ │ ├── Shell.java │ │ │ │ │ ├── ShellEncodingListener.java │ │ │ │ │ ├── ShellHistoryConstants.java │ │ │ │ │ ├── ShellHistoryListener.java │ │ │ │ │ ├── ShellHistoryManager.java │ │ │ │ │ ├── ShellHistoryReader.java │ │ │ │ │ ├── ShellHistoryWriter.java │ │ │ │ │ └── package.html │ │ │ │ ├── tools/ │ │ │ │ │ ├── AdbTool.java │ │ │ │ │ ├── AvrAssemblerCommandsHelper.java │ │ │ │ │ ├── ExternalTool.java │ │ │ │ │ └── FileMergeTool.java │ │ │ │ ├── ui/ │ │ │ │ │ ├── PreloadedJFrame.java │ │ │ │ │ ├── action/ │ │ │ │ │ │ ├── AbstractActionDescriptor.java │ │ │ │ │ │ ├── AcceleratorMap.java │ │ │ │ │ │ ├── ActionCategory.java │ │ │ │ │ │ ├── ActionDescriptor.java │ │ │ │ │ │ ├── ActionFactory.java │ │ │ │ │ │ ├── ActionKeymap.java │ │ │ │ │ │ ├── ActionKeymapIO.java │ │ │ │ │ │ ├── ActionKeymapReader.java │ │ │ │ │ │ ├── ActionKeymapWriter.java │ │ │ │ │ │ ├── ActionManager.java │ │ │ │ │ │ ├── ActionParameters.java │ │ │ │ │ │ ├── ActionProperties.java │ │ │ │ │ │ ├── AwtActionProxy.java │ │ │ │ │ │ ├── InvokesDialog.java │ │ │ │ │ │ ├── TcAction.java │ │ │ │ │ │ ├── impl/ │ │ │ │ │ │ │ ├── AbstractViewerAction.java │ │ │ │ │ │ │ ├── ActiveTabAction.java │ │ │ │ │ │ │ ├── AddBookmarkAction.java │ │ │ │ │ │ │ ├── AddTabAction.java │ │ │ │ │ │ │ ├── BatchRenameAction.java │ │ │ │ │ │ │ ├── BringAllToFrontAction.java │ │ │ │ │ │ │ ├── CalculateChecksumAction.java │ │ │ │ │ │ │ ├── CalculatorAction.java │ │ │ │ │ │ │ ├── ChangeDateAction.java │ │ │ │ │ │ │ ├── ChangeLocationAction.java │ │ │ │ │ │ │ ├── ChangePermissionsAction.java │ │ │ │ │ │ │ ├── ChangeReplicationAction.java │ │ │ │ │ │ │ ├── CheckForUpdatesAction.java │ │ │ │ │ │ │ ├── CloneTabToOtherPanelAction.java │ │ │ │ │ │ │ ├── CloseDuplicateTabsAction.java │ │ │ │ │ │ │ ├── CloseOtherTabsAction.java │ │ │ │ │ │ │ ├── CloseTabAction.java │ │ │ │ │ │ │ ├── CloseWindowAction.java │ │ │ │ │ │ │ ├── CombineFilesAction.java │ │ │ │ │ │ │ ├── CommandAction.java │ │ │ │ │ │ │ ├── CompareFilesAction.java │ │ │ │ │ │ │ ├── CompareFolderFilesAction.java │ │ │ │ │ │ │ ├── CompareFoldersAction.java │ │ │ │ │ │ │ ├── ConnectToServerAction.java │ │ │ │ │ │ │ ├── CopyAction.java │ │ │ │ │ │ │ ├── CopyFileBaseNamesAction.java │ │ │ │ │ │ │ ├── CopyFileNamesAction.java │ │ │ │ │ │ │ ├── CopyFilePathsAction.java │ │ │ │ │ │ │ ├── CopyFilesToClipboardAction.java │ │ │ │ │ │ │ ├── CreateSymlinkAction.java │ │ │ │ │ │ │ ├── CustomizeCommandBarAction.java │ │ │ │ │ │ │ ├── CutFilesToClipboardAction.java │ │ │ │ │ │ │ ├── DeleteAction.java │ │ │ │ │ │ │ ├── DonateAction.java │ │ │ │ │ │ │ ├── DuplicateTabAction.java │ │ │ │ │ │ │ ├── EditAction.java │ │ │ │ │ │ │ ├── EditAsAction.java │ │ │ │ │ │ │ ├── EditBookmarksAction.java │ │ │ │ │ │ │ ├── EditCommandsAction.java │ │ │ │ │ │ │ ├── EditCredentialsAction.java │ │ │ │ │ │ │ ├── EjectDriveAction.java │ │ │ │ │ │ │ ├── EmailAction.java │ │ │ │ │ │ │ ├── EmptyTrashAction.java │ │ │ │ │ │ │ ├── ExploreBookmarksAction.java │ │ │ │ │ │ │ ├── FileAction.java │ │ │ │ │ │ │ ├── FindFileAction.java │ │ │ │ │ │ │ ├── FocusNextAction.java │ │ │ │ │ │ │ ├── FocusPreviousAction.java │ │ │ │ │ │ │ ├── GarbageCollectAction.java │ │ │ │ │ │ │ ├── GoBackAction.java │ │ │ │ │ │ │ ├── GoForwardAction.java │ │ │ │ │ │ │ ├── GoToDocumentationAction.java │ │ │ │ │ │ │ ├── GoToForumsAction.java │ │ │ │ │ │ │ ├── GoToHomeAction.java │ │ │ │ │ │ │ ├── GoToParentAction.java │ │ │ │ │ │ │ ├── GoToParentInBothPanelsAction.java │ │ │ │ │ │ │ ├── GoToParentInOtherPanelAction.java │ │ │ │ │ │ │ ├── GoToRootAction.java │ │ │ │ │ │ │ ├── GoToWebsiteAction.java │ │ │ │ │ │ │ ├── InternalEditAction.java │ │ │ │ │ │ │ ├── InternalViewAction.java │ │ │ │ │ │ │ ├── InvertSelectionAction.java │ │ │ │ │ │ │ ├── LeftArrowAction.java │ │ │ │ │ │ │ ├── LocalCopyAction.java │ │ │ │ │ │ │ ├── LocateSymlinkAction.java │ │ │ │ │ │ │ ├── MarkAllAction.java │ │ │ │ │ │ │ ├── MarkBackwardAction.java │ │ │ │ │ │ │ ├── MarkEmptyFilesAction.java │ │ │ │ │ │ │ ├── MarkExtensionAction.java │ │ │ │ │ │ │ ├── MarkForwardAction.java │ │ │ │ │ │ │ ├── MarkGroupAction.java │ │ │ │ │ │ │ ├── MarkNextBlockAction.java │ │ │ │ │ │ │ ├── MarkNextPageAction.java │ │ │ │ │ │ │ ├── MarkNextRowAction.java │ │ │ │ │ │ │ ├── MarkPreviousBlockAction.java │ │ │ │ │ │ │ ├── MarkPreviousPageAction.java │ │ │ │ │ │ │ ├── MarkPreviousRowAction.java │ │ │ │ │ │ │ ├── MarkSelectedFileAction.java │ │ │ │ │ │ │ ├── MarkToFirstRowAction.java │ │ │ │ │ │ │ ├── MarkToLastRowAction.java │ │ │ │ │ │ │ ├── MaximizeWindowAction.java │ │ │ │ │ │ │ ├── MinimizeWindowAction.java │ │ │ │ │ │ │ ├── MkdirAction.java │ │ │ │ │ │ │ ├── MkfileAction.java │ │ │ │ │ │ │ ├── MoveAction.java │ │ │ │ │ │ │ ├── MoveTabToOtherPanelAction.java │ │ │ │ │ │ │ ├── MuteProxyAction.java │ │ │ │ │ │ │ ├── NewWindowAction.java │ │ │ │ │ │ │ ├── NextTabAction.java │ │ │ │ │ │ │ ├── OpenAction.java │ │ │ │ │ │ │ ├── OpenAsAction.java │ │ │ │ │ │ │ ├── OpenInBothPanelsAction.java │ │ │ │ │ │ │ ├── OpenInNewTabAction.java │ │ │ │ │ │ │ ├── OpenInOtherPanelAction.java │ │ │ │ │ │ │ ├── OpenLeftInRightPanelAction.java │ │ │ │ │ │ │ ├── OpenLocationAction.java │ │ │ │ │ │ │ ├── OpenNativelyAction.java │ │ │ │ │ │ │ ├── OpenRightInLeftPanelAction.java │ │ │ │ │ │ │ ├── OpenTrashAction.java │ │ │ │ │ │ │ ├── OpenURLInBrowserAction.java │ │ │ │ │ │ │ ├── PackAction.java │ │ │ │ │ │ │ ├── ParentFolderAction.java │ │ │ │ │ │ │ ├── PasteClipboardFilesAction.java │ │ │ │ │ │ │ ├── PasteFromArchiveToFilesFromClipboardAction.java │ │ │ │ │ │ │ ├── PermanentDeleteAction.java │ │ │ │ │ │ │ ├── PopupLeftDriveButtonAction.java │ │ │ │ │ │ │ ├── PopupRightDriveButtonAction.java │ │ │ │ │ │ │ ├── PreviousTabAction.java │ │ │ │ │ │ │ ├── ProxyAction.java │ │ │ │ │ │ │ ├── QuitAction.java │ │ │ │ │ │ │ ├── RecallNextWindowAction.java │ │ │ │ │ │ │ ├── RecallPreviousWindowAction.java │ │ │ │ │ │ │ ├── RecallWindow10Action.java │ │ │ │ │ │ │ ├── RecallWindow1Action.java │ │ │ │ │ │ │ ├── RecallWindow2Action.java │ │ │ │ │ │ │ ├── RecallWindow3Action.java │ │ │ │ │ │ │ ├── RecallWindow4Action.java │ │ │ │ │ │ │ ├── RecallWindow5Action.java │ │ │ │ │ │ │ ├── RecallWindow6Action.java │ │ │ │ │ │ │ ├── RecallWindow7Action.java │ │ │ │ │ │ │ ├── RecallWindow8Action.java │ │ │ │ │ │ │ ├── RecallWindow9Action.java │ │ │ │ │ │ │ ├── RecallWindowAction.java │ │ │ │ │ │ │ ├── RefreshAction.java │ │ │ │ │ │ │ ├── RenameAction.java │ │ │ │ │ │ │ ├── ReportBugAction.java │ │ │ │ │ │ │ ├── RevealInDesktopAction.java │ │ │ │ │ │ │ ├── ReverseSortOrderAction.java │ │ │ │ │ │ │ ├── RightArrowAction.java │ │ │ │ │ │ │ ├── RunCommandAction.java │ │ │ │ │ │ │ ├── SelectBackwardAction.java │ │ │ │ │ │ │ ├── SelectFirstRowAction.java │ │ │ │ │ │ │ ├── SelectForwardAction.java │ │ │ │ │ │ │ ├── SelectLastRowAction.java │ │ │ │ │ │ │ ├── SelectNextBlockAction.java │ │ │ │ │ │ │ ├── SelectNextPageAction.java │ │ │ │ │ │ │ ├── SelectNextRowAction.java │ │ │ │ │ │ │ ├── SelectPreviousBlockAction.java │ │ │ │ │ │ │ ├── SelectPreviousPageAction.java │ │ │ │ │ │ │ ├── SelectPreviousRowAction.java │ │ │ │ │ │ │ ├── SelectedFileAction.java │ │ │ │ │ │ │ ├── SelectedFilesAction.java │ │ │ │ │ │ │ ├── SetSameFolderAction.java │ │ │ │ │ │ │ ├── SetTabTitleAction.java │ │ │ │ │ │ │ ├── ShowAboutAction.java │ │ │ │ │ │ │ ├── ShowBookmarksQLAction.java │ │ │ │ │ │ │ ├── ShowDebugConsoleAction.java │ │ │ │ │ │ │ ├── ShowEditorBookmarksQLAction.java │ │ │ │ │ │ │ ├── ShowFilePopupMenuAction.java │ │ │ │ │ │ │ ├── ShowFilePropertiesAction.java │ │ │ │ │ │ │ ├── ShowFoldersSizeAction.java │ │ │ │ │ │ │ ├── ShowKeyboardShortcutsAction.java │ │ │ │ │ │ │ ├── ShowParentFoldersQLAction.java │ │ │ │ │ │ │ ├── ShowPreferencesAction.java │ │ │ │ │ │ │ ├── ShowQuickListAction.java │ │ │ │ │ │ │ ├── ShowRecentEditedFilesQLAction.java │ │ │ │ │ │ │ ├── ShowRecentExecutedFilesQLAction.java │ │ │ │ │ │ │ ├── ShowRecentLocationsQLAction.java │ │ │ │ │ │ │ ├── ShowRecentViewedFilesQLAction.java │ │ │ │ │ │ │ ├── ShowRootFoldersQLAction.java │ │ │ │ │ │ │ ├── ShowServerConnectionsAction.java │ │ │ │ │ │ │ ├── ShowTabsQLAction.java │ │ │ │ │ │ │ ├── SortByAction.java │ │ │ │ │ │ │ ├── SortByDateAction.java │ │ │ │ │ │ │ ├── SortByExtensionAction.java │ │ │ │ │ │ │ ├── SortByGroupAction.java │ │ │ │ │ │ │ ├── SortByNameAction.java │ │ │ │ │ │ │ ├── SortByOwnerAction.java │ │ │ │ │ │ │ ├── SortByPermissionsAction.java │ │ │ │ │ │ │ ├── SortBySizeAction.java │ │ │ │ │ │ │ ├── SplitEquallyAction.java │ │ │ │ │ │ │ ├── SplitFileAction.java │ │ │ │ │ │ │ ├── SplitHorizontallyAction.java │ │ │ │ │ │ │ ├── SplitVerticallyAction.java │ │ │ │ │ │ │ ├── StopAction.java │ │ │ │ │ │ │ ├── SwapFoldersAction.java │ │ │ │ │ │ │ ├── SwitchActiveTableAction.java │ │ │ │ │ │ │ ├── TerminalAction.java │ │ │ │ │ │ │ ├── TerminalAltAction.java │ │ │ │ │ │ │ ├── TerminalPanelAction.java │ │ │ │ │ │ │ ├── TextEditorsListAction.java │ │ │ │ │ │ │ ├── ToggleAutoSizeAction.java │ │ │ │ │ │ │ ├── ToggleColumnAction.java │ │ │ │ │ │ │ ├── ToggleCommandBarAction.java │ │ │ │ │ │ │ ├── ToggleDateColumnAction.java │ │ │ │ │ │ │ ├── ToggleExtensionColumnAction.java │ │ │ │ │ │ │ ├── ToggleFoldersAlwaysAlphabeticalAction.java │ │ │ │ │ │ │ ├── ToggleGroupColumnAction.java │ │ │ │ │ │ │ ├── ToggleHiddenFilesAction.java │ │ │ │ │ │ │ ├── ToggleLockTabAction.java │ │ │ │ │ │ │ ├── ToggleOwnerColumnAction.java │ │ │ │ │ │ │ ├── TogglePanelPreviewModeAction.java │ │ │ │ │ │ │ ├── TogglePermissionsColumnAction.java │ │ │ │ │ │ │ ├── ToggleShowFoldersFirstAction.java │ │ │ │ │ │ │ ├── ToggleSinglePanelAction.java │ │ │ │ │ │ │ ├── ToggleSizeColumnAction.java │ │ │ │ │ │ │ ├── ToggleStatusBarAction.java │ │ │ │ │ │ │ ├── ToggleTableViewModeCompactAction.java │ │ │ │ │ │ │ ├── ToggleTableViewModeFullAction.java │ │ │ │ │ │ │ ├── ToggleTableViewModeShortAction.java │ │ │ │ │ │ │ ├── ToggleToolBarAction.java │ │ │ │ │ │ │ ├── ToggleTreeAction.java │ │ │ │ │ │ │ ├── UnmarkAllAction.java │ │ │ │ │ │ │ ├── UnmarkGroupAction.java │ │ │ │ │ │ │ ├── UnpackAction.java │ │ │ │ │ │ │ ├── UserMenuAction.java │ │ │ │ │ │ │ ├── ViewAction.java │ │ │ │ │ │ │ └── ViewAsAction.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── autocomplete/ │ │ │ │ │ │ ├── AutocompleterTextComponent.java │ │ │ │ │ │ ├── BasicAutocompleterTextComponent.java │ │ │ │ │ │ ├── CompleterFactory.java │ │ │ │ │ │ ├── CompletionType.java │ │ │ │ │ │ ├── EditableComboboxCompletion.java │ │ │ │ │ │ ├── OtherTextComponentCompletion.java │ │ │ │ │ │ ├── TextFieldCompletion.java │ │ │ │ │ │ ├── TypicalAutocompleterEditableCombobox.java │ │ │ │ │ │ └── completers/ │ │ │ │ │ │ ├── ComboboxOptionsCompleter.java │ │ │ │ │ │ ├── Completer.java │ │ │ │ │ │ ├── LocationCompleter.java │ │ │ │ │ │ ├── PathCompleter.java │ │ │ │ │ │ ├── ServiceFactory.java │ │ │ │ │ │ └── services/ │ │ │ │ │ │ ├── AllFilesService.java │ │ │ │ │ │ ├── BookmarksService.java │ │ │ │ │ │ ├── CompletionService.java │ │ │ │ │ │ ├── FilesService.java │ │ │ │ │ │ ├── FilteredFilesService.java │ │ │ │ │ │ ├── PrefixFilter.java │ │ │ │ │ │ ├── SystemVariablesService.java │ │ │ │ │ │ └── VolumesService.java │ │ │ │ │ ├── border/ │ │ │ │ │ │ ├── MutableLineBorder.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── button/ │ │ │ │ │ │ ├── ArrowButton.java │ │ │ │ │ │ ├── ButtonChoicePanel.java │ │ │ │ │ │ ├── CollapseExpandButton.java │ │ │ │ │ │ ├── HelpButton.java │ │ │ │ │ │ ├── HelpButtonPanel.java │ │ │ │ │ │ ├── NonFocusableButton.java │ │ │ │ │ │ ├── PopupButton.java │ │ │ │ │ │ ├── RolloverButtonAdapter.java │ │ │ │ │ │ ├── ToolbarMoreButton.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── chooser/ │ │ │ │ │ │ ├── ColorChangeEvent.java │ │ │ │ │ │ ├── ColorChangeListener.java │ │ │ │ │ │ ├── ColorChooser.java │ │ │ │ │ │ ├── ColorPicker.java │ │ │ │ │ │ ├── FontChooser.java │ │ │ │ │ │ ├── IntegerChooser.java │ │ │ │ │ │ ├── KeyboardShortcutChooser.java │ │ │ │ │ │ ├── PreviewLabel.java │ │ │ │ │ │ ├── SizeChooser.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── combobox/ │ │ │ │ │ │ ├── AutocompleteEditableCombobox.java │ │ │ │ │ │ ├── ComboBoxCellRenderer.java │ │ │ │ │ │ ├── ComboBoxListener.java │ │ │ │ │ │ ├── EditableComboBox.java │ │ │ │ │ │ ├── EditableComboBoxListener.java │ │ │ │ │ │ ├── SaneComboBox.java │ │ │ │ │ │ ├── TcComboBox.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── dialog/ │ │ │ │ │ │ ├── DialogOwner.java │ │ │ │ │ │ ├── DialogToolkit.java │ │ │ │ │ │ ├── FocusDialog.java │ │ │ │ │ │ ├── InformationDialog.java │ │ │ │ │ │ ├── PasswordDialog.java │ │ │ │ │ │ ├── QuestionDialog.java │ │ │ │ │ │ ├── about/ │ │ │ │ │ │ │ ├── AboutDialog.java │ │ │ │ │ │ │ ├── LicenseDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ ├── AuthDialog.java │ │ │ │ │ │ │ ├── EditCredentialsDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── bookmark/ │ │ │ │ │ │ │ ├── AddBookmarkDialog.kt │ │ │ │ │ │ │ ├── BookmarkParentComboBox.java │ │ │ │ │ │ │ ├── EditBookmarksDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── commands/ │ │ │ │ │ │ │ ├── CommandsPanel.kt │ │ │ │ │ │ │ └── EditCommandsDialog.kt │ │ │ │ │ │ ├── customization/ │ │ │ │ │ │ │ ├── CommandBarDialog.java │ │ │ │ │ │ │ └── CustomizeDialog.java │ │ │ │ │ │ ├── debug/ │ │ │ │ │ │ │ ├── DebugConsoleAppender.java │ │ │ │ │ │ │ ├── DebugConsoleDialog.java │ │ │ │ │ │ │ └── LoggingEvent.java │ │ │ │ │ │ ├── file/ │ │ │ │ │ │ │ ├── AbstractCopyDialog.java │ │ │ │ │ │ │ ├── BatchRenameConfirmationDialog.java │ │ │ │ │ │ │ ├── BatchRenameDialog.java │ │ │ │ │ │ │ ├── BatchRenameSelectRange.java │ │ │ │ │ │ │ ├── CalculateChecksumDialog.java │ │ │ │ │ │ │ ├── ChangeDateDialog.java │ │ │ │ │ │ │ ├── ChangePermissionsDialog.java │ │ │ │ │ │ │ ├── ChangeReplicationDialog.java │ │ │ │ │ │ │ ├── CombineFilesDialog.java │ │ │ │ │ │ │ ├── CopyDialog.java │ │ │ │ │ │ │ ├── DeleteDialog.java │ │ │ │ │ │ │ ├── DownloadDialog.java │ │ │ │ │ │ │ ├── EmailFilesDialog.java │ │ │ │ │ │ │ ├── FileCollisionDialog.java │ │ │ │ │ │ │ ├── FileCollisionRenameDialog.java │ │ │ │ │ │ │ ├── FileSelectionDialog.java │ │ │ │ │ │ │ ├── FindFileDialog.java │ │ │ │ │ │ │ ├── FindFileResultRenderer.java │ │ │ │ │ │ │ ├── JobDialog.java │ │ │ │ │ │ │ ├── LocalCopyDialog.java │ │ │ │ │ │ │ ├── MakeDirectoryFileDialog.java │ │ │ │ │ │ │ ├── MoveDialog.java │ │ │ │ │ │ │ ├── PackDialog.java │ │ │ │ │ │ │ ├── PathFieldContent.java │ │ │ │ │ │ │ ├── ProgressDialog.java │ │ │ │ │ │ │ ├── PropertiesDialog.java │ │ │ │ │ │ │ ├── SplitFileDialog.java │ │ │ │ │ │ │ ├── TransferDestinationDialog.java │ │ │ │ │ │ │ ├── UnpackDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── help/ │ │ │ │ │ │ │ ├── ShortcutsDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ ├── pref/ │ │ │ │ │ │ │ ├── PreferencesDialog.java │ │ │ │ │ │ │ ├── PreferencesPanel.java │ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ │ ├── PrefCheckBox.java │ │ │ │ │ │ │ │ ├── PrefComboBox.java │ │ │ │ │ │ │ │ ├── PrefComponent.java │ │ │ │ │ │ │ │ ├── PrefEncodingSelectBox.java │ │ │ │ │ │ │ │ ├── PrefFilePathField.java │ │ │ │ │ │ │ │ ├── PrefRadioButton.java │ │ │ │ │ │ │ │ ├── PrefTable.java │ │ │ │ │ │ │ │ └── PrefTextField.java │ │ │ │ │ │ │ ├── general/ │ │ │ │ │ │ │ │ ├── AppearancePanel.java │ │ │ │ │ │ │ │ ├── FoldersPanel.java │ │ │ │ │ │ │ │ ├── GeneralPanel.java │ │ │ │ │ │ │ │ ├── GeneralPreferencesDialog.java │ │ │ │ │ │ │ │ ├── MailPanel.java │ │ │ │ │ │ │ │ ├── MiscPanel.java │ │ │ │ │ │ │ │ ├── ShortcutsPanel.java │ │ │ │ │ │ │ │ ├── ShortcutsTable.java │ │ │ │ │ │ │ │ ├── ThemeNameDialog.java │ │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ │ └── theme/ │ │ │ │ │ │ │ ├── ColorButton.java │ │ │ │ │ │ │ ├── CopyFilePanelColorsButton.java │ │ │ │ │ │ │ ├── FileEditorPanel.java │ │ │ │ │ │ │ ├── FileGroupsPanel.java │ │ │ │ │ │ │ ├── FilePanel.java │ │ │ │ │ │ │ ├── FilePanelColorIds.java │ │ │ │ │ │ │ ├── FilePreviewPanel.java │ │ │ │ │ │ │ ├── FolderPanePanel.java │ │ │ │ │ │ │ ├── LocationBarPanel.java │ │ │ │ │ │ │ ├── QuickListPanel.java │ │ │ │ │ │ │ ├── ShellPanel.java │ │ │ │ │ │ │ ├── StatusBarPanel.java │ │ │ │ │ │ │ ├── ThemeEditorDialog.java │ │ │ │ │ │ │ ├── ThemeEditorPanel.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── server/ │ │ │ │ │ │ │ ├── FTPPanel.java │ │ │ │ │ │ │ ├── HDFSPanel.java │ │ │ │ │ │ │ ├── HTTPPanel.java │ │ │ │ │ │ │ ├── NFSPanel.java │ │ │ │ │ │ │ ├── S3Panel.java │ │ │ │ │ │ │ ├── SFTPPanel.java │ │ │ │ │ │ │ ├── SMBPanel.java │ │ │ │ │ │ │ ├── ServerConnectDialog.java │ │ │ │ │ │ │ ├── ServerPanel.java │ │ │ │ │ │ │ ├── ShowServerConnectionsDialog.java │ │ │ │ │ │ │ ├── VSpherePanel.java │ │ │ │ │ │ │ ├── WebDAVPanel.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── shell/ │ │ │ │ │ │ │ ├── RunDialog.java │ │ │ │ │ │ │ ├── ShellComboBox.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── shutdown/ │ │ │ │ │ │ │ ├── QuitDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── startup/ │ │ │ │ │ │ │ ├── CheckVersionDialog.java │ │ │ │ │ │ │ ├── InitialSetupDialog.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── symlink/ │ │ │ │ │ │ │ ├── CreateSymLinkDialog.java │ │ │ │ │ │ │ └── EditSymlinkDialog.java │ │ │ │ │ │ └── tab/ │ │ │ │ │ │ └── TabTitleDialog.kt │ │ │ │ │ ├── dnd/ │ │ │ │ │ │ ├── ClipboardNotifier.java │ │ │ │ │ │ ├── ClipboardOperations.java │ │ │ │ │ │ ├── ClipboardSupport.java │ │ │ │ │ │ ├── DnDContext.java │ │ │ │ │ │ ├── FileDragSourceListener.java │ │ │ │ │ │ ├── FileDropTargetListener.java │ │ │ │ │ │ ├── TransferableFileSet.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── encoding/ │ │ │ │ │ │ ├── EncodingListener.java │ │ │ │ │ │ ├── EncodingMenu.java │ │ │ │ │ │ ├── EncodingPreferences.java │ │ │ │ │ │ ├── EncodingSelectBox.java │ │ │ │ │ │ └── PreferredEncodingsDialog.java │ │ │ │ │ ├── event/ │ │ │ │ │ │ ├── ActivePanelListener.java │ │ │ │ │ │ ├── LocationAdapter.java │ │ │ │ │ │ ├── LocationEvent.java │ │ │ │ │ │ ├── LocationListener.java │ │ │ │ │ │ ├── LocationManager.java │ │ │ │ │ │ └── TableSelectionListener.java │ │ │ │ │ ├── helper/ │ │ │ │ │ │ ├── FocusRequester.java │ │ │ │ │ │ ├── MenuToolkit.java │ │ │ │ │ │ ├── MnemonicHelper.java │ │ │ │ │ │ ├── ScreenServices.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── icon/ │ │ │ │ │ │ ├── AnimatedIcon.java │ │ │ │ │ │ ├── CustomFileIconProvider.java │ │ │ │ │ │ ├── EmptyIcon.java │ │ │ │ │ │ ├── FileIcons.java │ │ │ │ │ │ ├── IconManager.java │ │ │ │ │ │ └── SpinningDial.java │ │ │ │ │ ├── layout/ │ │ │ │ │ │ ├── AsyncPanel.java │ │ │ │ │ │ ├── CompareImagesPanel.java │ │ │ │ │ │ ├── FluentPanel.java │ │ │ │ │ │ ├── InformationPane.java │ │ │ │ │ │ ├── ProportionalGridPanel.java │ │ │ │ │ │ ├── ProportionalSplitPane.java │ │ │ │ │ │ ├── SpringUtilities.java │ │ │ │ │ │ ├── XAlignedComponentPanel.java │ │ │ │ │ │ ├── XBoxPanel.java │ │ │ │ │ │ ├── YBoxPanel.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── list/ │ │ │ │ │ │ ├── DynamicHorizontalWrapList.java │ │ │ │ │ │ ├── DynamicList.java │ │ │ │ │ │ ├── FileList.java │ │ │ │ │ │ ├── SortableListPanel.kt │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── macosx/ │ │ │ │ │ │ ├── AppleScript.java │ │ │ │ │ │ ├── AppleScriptBuilder.java │ │ │ │ │ │ ├── EAWTHandler.java │ │ │ │ │ │ ├── IMacOsWindow.java │ │ │ │ │ │ ├── OSXIntegration.java │ │ │ │ │ │ ├── TabbedPaneUICustomizer.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── main/ │ │ │ │ │ │ ├── BreadcrumbBar.kt │ │ │ │ │ │ ├── ConfigurableFolderFilter.java │ │ │ │ │ │ ├── DSStoreFileFilter.java │ │ │ │ │ │ ├── DrivePopupButton.java │ │ │ │ │ │ ├── FolderPanel.java │ │ │ │ │ │ ├── LocationBar.kt │ │ │ │ │ │ ├── LocationTextField.java │ │ │ │ │ │ ├── MainFrame.java │ │ │ │ │ │ ├── PreviewPanel.java │ │ │ │ │ │ ├── QuickLists.java │ │ │ │ │ │ ├── SplashScreen.java │ │ │ │ │ │ ├── SystemFileFilter.java │ │ │ │ │ │ ├── WindowManager.java │ │ │ │ │ │ ├── commandbar/ │ │ │ │ │ │ │ ├── CommandBar.java │ │ │ │ │ │ │ ├── CommandBarAttributes.java │ │ │ │ │ │ │ ├── CommandBarAttributesListener.java │ │ │ │ │ │ │ ├── CommandBarButton.java │ │ │ │ │ │ │ ├── CommandBarButtonForDisplay.java │ │ │ │ │ │ │ ├── CommandBarIO.java │ │ │ │ │ │ │ ├── CommandBarReader.java │ │ │ │ │ │ │ └── CommandBarWriter.java │ │ │ │ │ │ ├── frame/ │ │ │ │ │ │ │ ├── ClonedMainFrameBuilder.java │ │ │ │ │ │ │ ├── CommandLineMainFrameBuilder.java │ │ │ │ │ │ │ ├── DefaultMainFramesBuilder.java │ │ │ │ │ │ │ └── MainFrameBuilder.java │ │ │ │ │ │ ├── menu/ │ │ │ │ │ │ │ ├── MainMenuBar.java │ │ │ │ │ │ │ ├── OpenAsMenu.java │ │ │ │ │ │ │ ├── OpenWithMenu.java │ │ │ │ │ │ │ ├── TablePopupMenu.java │ │ │ │ │ │ │ ├── UserPopupMenu.java │ │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ │ └── usermenu/ │ │ │ │ │ │ │ ├── LoadUserMenuException.java │ │ │ │ │ │ │ ├── UserMenuItem.java │ │ │ │ │ │ │ └── UserPopupMenuLoader.java │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ ├── quicklist/ │ │ │ │ │ │ │ ├── BookmarksQL.java │ │ │ │ │ │ │ ├── EditAsQL.java │ │ │ │ │ │ │ ├── EditorBookmarksQL.java │ │ │ │ │ │ │ ├── ParentFoldersQL.java │ │ │ │ │ │ │ ├── RecentEditedQL.java │ │ │ │ │ │ │ ├── RecentExecutedFilesQL.java │ │ │ │ │ │ │ ├── RecentLocationsQL.java │ │ │ │ │ │ │ ├── RecentViewedQL.java │ │ │ │ │ │ │ ├── RootFoldersQL.java │ │ │ │ │ │ │ ├── TabsQL.java │ │ │ │ │ │ │ ├── ViewAsQL.java │ │ │ │ │ │ │ └── ViewedAndEditedFilesQL.java │ │ │ │ │ │ ├── statusbar/ │ │ │ │ │ │ │ ├── FileWindowsListButton.java │ │ │ │ │ │ │ ├── HeapIndicator.java │ │ │ │ │ │ │ ├── StatusBar.java │ │ │ │ │ │ │ ├── TaskPanel.java │ │ │ │ │ │ │ ├── TaskWidget.java │ │ │ │ │ │ │ ├── TrashPopupButton.java │ │ │ │ │ │ │ └── VolumeSpaceLabel.java │ │ │ │ │ │ ├── table/ │ │ │ │ │ │ │ ├── CalculateDirectorySizeWorker.java │ │ │ │ │ │ │ ├── CellLabel.java │ │ │ │ │ │ │ ├── Column.java │ │ │ │ │ │ │ ├── FileGroupResolver.java │ │ │ │ │ │ │ ├── FileTable.java │ │ │ │ │ │ │ ├── FileTableHeader.java │ │ │ │ │ │ │ ├── FileTableHeaderRenderer.java │ │ │ │ │ │ │ ├── FileTableWrapperForDisplay.java │ │ │ │ │ │ │ ├── SortInfo.java │ │ │ │ │ │ │ ├── TransparentCellLabel.java │ │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ │ └── views/ │ │ │ │ │ │ │ ├── BaseCellRenderer.java │ │ │ │ │ │ │ ├── BaseFileTableModel.java │ │ │ │ │ │ │ ├── TableViewMode.java │ │ │ │ │ │ │ ├── compact/ │ │ │ │ │ │ │ │ ├── CompactFileTableCellRenderer.java │ │ │ │ │ │ │ │ ├── CompactFileTableColumnModel.java │ │ │ │ │ │ │ │ └── CompactFileTableModel.java │ │ │ │ │ │ │ └── full/ │ │ │ │ │ │ │ ├── FileTableCellRenderer.java │ │ │ │ │ │ │ ├── FileTableColumnModel.java │ │ │ │ │ │ │ ├── FileTableConfiguration.java │ │ │ │ │ │ │ └── FileTableModel.java │ │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ │ ├── ClonedFileTableTabFactory.java │ │ │ │ │ │ │ ├── ConfFileTableTab.java │ │ │ │ │ │ │ ├── DefaultFileTableTabFactory.java │ │ │ │ │ │ │ ├── DefaultFileTableTabHeaderFactory.java │ │ │ │ │ │ │ ├── FileTableTab.java │ │ │ │ │ │ │ ├── FileTableTabHeader.java │ │ │ │ │ │ │ ├── FileTableTabHeaderFactory.java │ │ │ │ │ │ │ ├── FileTableTabPopupMenu.java │ │ │ │ │ │ │ ├── FileTableTabbedPane.java │ │ │ │ │ │ │ ├── FileTableTabs.java │ │ │ │ │ │ │ ├── FileTableTabsWithHeadersViewerFactory.java │ │ │ │ │ │ │ ├── FileTableTabsWithoutHeadersViewerFactory.java │ │ │ │ │ │ │ ├── NotClosableFileTableTabHeaderFactory.java │ │ │ │ │ │ │ └── PrintableFileTableTabFactory.java │ │ │ │ │ │ ├── toolbar/ │ │ │ │ │ │ │ ├── ToolBar.java │ │ │ │ │ │ │ ├── ToolBarAttributes.java │ │ │ │ │ │ │ ├── ToolBarAttributesListener.java │ │ │ │ │ │ │ ├── ToolBarIO.java │ │ │ │ │ │ │ ├── ToolBarReader.java │ │ │ │ │ │ │ └── ToolBarWriter.java │ │ │ │ │ │ └── tree/ │ │ │ │ │ │ ├── AbstractIOThreadManager.java │ │ │ │ │ │ ├── CachedDirectory.java │ │ │ │ │ │ ├── CachedDirectoryListener.java │ │ │ │ │ │ ├── DirectoryCache.java │ │ │ │ │ │ ├── FilesTreeModel.java │ │ │ │ │ │ ├── FoldersTreePanel.java │ │ │ │ │ │ ├── FoldersTreeRenderer.java │ │ │ │ │ │ ├── IOThread.java │ │ │ │ │ │ ├── TreeIOThreadManager.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── menu/ │ │ │ │ │ │ ├── JScrollMenu.java │ │ │ │ │ │ └── JScrollPopupMenu.java │ │ │ │ │ ├── notifier/ │ │ │ │ │ │ ├── AbstractNotifier.java │ │ │ │ │ │ ├── GrowlNotifier.java │ │ │ │ │ │ ├── NotificationType.java │ │ │ │ │ │ ├── SystemTrayNotifier.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── popup/ │ │ │ │ │ │ └── TcActionsPopupMenu.kt │ │ │ │ │ ├── progress/ │ │ │ │ │ │ ├── ProgressTextField.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── quicklist/ │ │ │ │ │ │ ├── QuickList.java │ │ │ │ │ │ ├── QuickListContainer.java │ │ │ │ │ │ ├── QuickListWithDataList.java │ │ │ │ │ │ ├── QuickListWithEmptyMsg.java │ │ │ │ │ │ ├── QuickListWithIcons.java │ │ │ │ │ │ ├── QuickListWithoutIcons.java │ │ │ │ │ │ └── item/ │ │ │ │ │ │ ├── QuickListDataList.java │ │ │ │ │ │ ├── QuickListDataListWithIcons.java │ │ │ │ │ │ ├── QuickListDataModel.java │ │ │ │ │ │ ├── QuickListEmptyMessageItem.java │ │ │ │ │ │ ├── QuickListHeaderItem.java │ │ │ │ │ │ └── QuickListItem.java │ │ │ │ │ ├── quicksearch/ │ │ │ │ │ │ └── QuickSearch.java │ │ │ │ │ ├── table/ │ │ │ │ │ │ ├── CenteredTableHeaderRenderer.java │ │ │ │ │ │ ├── EditableHeader.java │ │ │ │ │ │ ├── EditableHeaderTableColumn.java │ │ │ │ │ │ └── EditableHeaderUI.java │ │ │ │ │ ├── tabs/ │ │ │ │ │ │ ├── ActiveTabListener.java │ │ │ │ │ │ ├── HideableTabbedPane.java │ │ │ │ │ │ ├── NullableTabsViewer.java │ │ │ │ │ │ ├── Tab.java │ │ │ │ │ │ ├── TabFactory.java │ │ │ │ │ │ ├── TabUpdater.java │ │ │ │ │ │ ├── TabWithoutHeaderViewer.java │ │ │ │ │ │ ├── TabbedPane.java │ │ │ │ │ │ ├── TabsCollection.java │ │ │ │ │ │ ├── TabsEventListener.java │ │ │ │ │ │ ├── TabsViewer.java │ │ │ │ │ │ ├── TabsViewerFactory.java │ │ │ │ │ │ └── TabsWithHeaderViewer.java │ │ │ │ │ ├── terminal/ │ │ │ │ │ │ ├── JediTerminalPanelEx.kt │ │ │ │ │ │ ├── TcTerminal.kt │ │ │ │ │ │ ├── TcTerminalTtyConnector.kt │ │ │ │ │ │ └── TerminalSettingsProvider.kt │ │ │ │ │ ├── text/ │ │ │ │ │ │ ├── FileLabel.java │ │ │ │ │ │ ├── FilePathField.java │ │ │ │ │ │ ├── FilePathFieldKeyListener.java │ │ │ │ │ │ ├── FontUtils.java │ │ │ │ │ │ ├── KeyStrokeUtils.java │ │ │ │ │ │ ├── MultiLineLabel.java │ │ │ │ │ │ ├── RecordingKeyStrokeTextField.java │ │ │ │ │ │ └── SizeConstrainedDocument.java │ │ │ │ │ ├── theme/ │ │ │ │ │ │ ├── ColorChangedEvent.java │ │ │ │ │ │ ├── ComponentMapper.java │ │ │ │ │ │ ├── DefaultColor.java │ │ │ │ │ │ ├── DefaultFont.java │ │ │ │ │ │ ├── EditorTheme.java │ │ │ │ │ │ ├── FixedDefaultColor.java │ │ │ │ │ │ ├── FixedDefaultFont.java │ │ │ │ │ │ ├── FontChangedEvent.java │ │ │ │ │ │ ├── LinkedDefaultColor.java │ │ │ │ │ │ ├── LinkedDefaultFont.java │ │ │ │ │ │ ├── SystemDefaultColor.java │ │ │ │ │ │ ├── SystemDefaultFont.java │ │ │ │ │ │ ├── Theme.java │ │ │ │ │ │ ├── ThemeCache.java │ │ │ │ │ │ ├── ThemeData.java │ │ │ │ │ │ ├── ThemeId.java │ │ │ │ │ │ ├── ThemeListener.java │ │ │ │ │ │ ├── ThemeManager.java │ │ │ │ │ │ ├── ThemeReader.java │ │ │ │ │ │ ├── ThemeWriter.java │ │ │ │ │ │ ├── ThemeXmlConstants.java │ │ │ │ │ │ └── package.html │ │ │ │ │ ├── tools/ │ │ │ │ │ │ ├── ToolsEnvironment.java │ │ │ │ │ │ └── ToolsSetupDialog.java │ │ │ │ │ ├── viewer/ │ │ │ │ │ │ ├── EditorFactory.java │ │ │ │ │ │ ├── EditorFrame.java │ │ │ │ │ │ ├── EditorRegistrar.java │ │ │ │ │ │ ├── FileEditor.java │ │ │ │ │ │ ├── FileFrame.java │ │ │ │ │ │ ├── FileFrameCreateListener.java │ │ │ │ │ │ ├── FilePreloadWorker.java │ │ │ │ │ │ ├── FilePresenter.java │ │ │ │ │ │ ├── FileViewer.java │ │ │ │ │ │ ├── FileViewersList.java │ │ │ │ │ │ ├── UserCancelledException.java │ │ │ │ │ │ ├── ViewerFactory.java │ │ │ │ │ │ ├── ViewerFrame.java │ │ │ │ │ │ ├── ViewerRegistrar.java │ │ │ │ │ │ ├── WarnUserException.java │ │ │ │ │ │ ├── audio/ │ │ │ │ │ │ │ ├── AudioFactory.java │ │ │ │ │ │ │ ├── AudioPlayer.java │ │ │ │ │ │ │ ├── AudioViewer.java │ │ │ │ │ │ │ └── StatusBar.java │ │ │ │ │ │ ├── djvu/ │ │ │ │ │ │ │ ├── DjvuFactory.java │ │ │ │ │ │ │ └── DjvuViewer.java │ │ │ │ │ │ ├── hex/ │ │ │ │ │ │ │ ├── FindDialog.kt │ │ │ │ │ │ │ ├── GotoDialog.kt │ │ │ │ │ │ │ ├── HexFactory.kt │ │ │ │ │ │ │ ├── HexViewer.java │ │ │ │ │ │ │ └── StatusBar.kt │ │ │ │ │ │ ├── html/ │ │ │ │ │ │ │ ├── HtmlFactory.java │ │ │ │ │ │ │ └── HtmlViewer.java │ │ │ │ │ │ ├── image/ │ │ │ │ │ │ │ ├── ImageFactory.java │ │ │ │ │ │ │ ├── ImageViewer.java │ │ │ │ │ │ │ ├── StatusBar.java │ │ │ │ │ │ │ ├── ZxSpectrumScrImage.java │ │ │ │ │ │ │ └── package.html │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ ├── pdf/ │ │ │ │ │ │ │ ├── PdfFactory.java │ │ │ │ │ │ │ └── PdfViewer.java │ │ │ │ │ │ └── text/ │ │ │ │ │ │ ├── FileType.java │ │ │ │ │ │ ├── GotoLineDialog.java │ │ │ │ │ │ ├── StatusBar.java │ │ │ │ │ │ ├── TextArea.java │ │ │ │ │ │ ├── TextEditor.java │ │ │ │ │ │ ├── TextEditorCaretListener.java │ │ │ │ │ │ ├── TextEditorImpl.java │ │ │ │ │ │ ├── TextEditorUtils.java │ │ │ │ │ │ ├── TextFactory.java │ │ │ │ │ │ ├── TextFilesHistory.java │ │ │ │ │ │ ├── TextLineNumbersPanel.java │ │ │ │ │ │ ├── TextMenuHelper.java │ │ │ │ │ │ ├── TextViewer.java │ │ │ │ │ │ ├── package.html │ │ │ │ │ │ ├── search/ │ │ │ │ │ │ │ ├── AbstractSearchDialog.java │ │ │ │ │ │ │ ├── FindDialog.java │ │ │ │ │ │ │ ├── FindReplaceButtonsEnableResult.java │ │ │ │ │ │ │ ├── ReplaceDialog.java │ │ │ │ │ │ │ ├── SearchEvent.java │ │ │ │ │ │ │ └── SearchListener.java │ │ │ │ │ │ ├── tools/ │ │ │ │ │ │ │ ├── ExecOutputTextPane.java │ │ │ │ │ │ │ ├── ExecPanel.java │ │ │ │ │ │ │ ├── ExecUtils.java │ │ │ │ │ │ │ ├── OnClickFileHandler.java │ │ │ │ │ │ │ └── ProcessParams.java │ │ │ │ │ │ └── utils/ │ │ │ │ │ │ ├── CodeFormatException.java │ │ │ │ │ │ └── CodeFormatter.kt │ │ │ │ │ └── widgets/ │ │ │ │ │ └── render/ │ │ │ │ │ └── BasicComboBoxRenderer.java │ │ │ │ ├── updates/ │ │ │ │ │ ├── NightlyChecker.java │ │ │ │ │ ├── SelfUpdateUtils.java │ │ │ │ │ └── VersionChecker.java │ │ │ │ └── utils/ │ │ │ │ ├── Convert.java │ │ │ │ ├── FileIconsCache.kt │ │ │ │ ├── TcLogging.java │ │ │ │ ├── text/ │ │ │ │ │ ├── CustomDateFormat.java │ │ │ │ │ ├── DurationFormat.java │ │ │ │ │ ├── SizeFormat.java │ │ │ │ │ └── Translator.java │ │ │ │ └── xml/ │ │ │ │ ├── XmlAttributes.java │ │ │ │ ├── XmlWriter.java │ │ │ │ └── package.html │ │ │ ├── sshtools/ │ │ │ │ └── sftp/ │ │ │ │ ├── SftpFileInputStreamEx.java │ │ │ │ └── SftpFileOutputStreamEx.java │ │ │ └── sun/ │ │ │ └── jna/ │ │ │ └── platform/ │ │ │ └── mac/ │ │ │ └── XAttrUtils.java │ │ ├── org/ │ │ │ └── fife/ │ │ │ └── ui/ │ │ │ └── rtextarea/ │ │ │ └── GutterEx.java │ │ ├── ru/ │ │ │ └── trolsoft/ │ │ │ ├── calculator/ │ │ │ │ ├── Calculator.kt │ │ │ │ ├── CalculatorDialog.kt │ │ │ │ ├── HistoryComboBox.kt │ │ │ │ └── eval/ │ │ │ │ ├── Calculable.kt │ │ │ │ ├── CommandlineInterpreter.java │ │ │ │ ├── CustomFunction.java │ │ │ │ ├── CustomOperator.java │ │ │ │ ├── ExpressionBuilder.java │ │ │ │ ├── RPNConverter.kt │ │ │ │ ├── RPNExpression.java │ │ │ │ ├── Tokenizer.kt │ │ │ │ ├── exceptions/ │ │ │ │ │ ├── InvalidCustomFunctionException.kt │ │ │ │ │ ├── UnknownFunctionException.java │ │ │ │ │ └── UnparsableExpressionException.java │ │ │ │ ├── tokens/ │ │ │ │ │ ├── CalculationToken.kt │ │ │ │ │ ├── FunctionSeparatorToken.kt │ │ │ │ │ ├── FunctionToken.kt │ │ │ │ │ ├── NumberToken.kt │ │ │ │ │ ├── OperatorToken.kt │ │ │ │ │ ├── ParenthesesToken.kt │ │ │ │ │ ├── Token.kt │ │ │ │ │ └── VariableToken.kt │ │ │ │ └── utils.kt │ │ │ ├── hexeditor/ │ │ │ │ ├── data/ │ │ │ │ │ ├── AbstractByteBuffer.java │ │ │ │ │ ├── FileByteBuffer.java │ │ │ │ │ ├── MemoryByteBuffer.java │ │ │ │ │ └── TrolCommanderByteBuffer.java │ │ │ │ ├── events/ │ │ │ │ │ ├── OffsetChangeListener.java │ │ │ │ │ └── SelectionChangeListener.java │ │ │ │ ├── search/ │ │ │ │ │ └── ByteBufferSearchUtils.java │ │ │ │ └── ui/ │ │ │ │ ├── HexTable.java │ │ │ │ └── ViewerHexTableModel.java │ │ │ ├── jni/ │ │ │ │ └── NativeFileUtils.java │ │ │ ├── macosx/ │ │ │ │ ├── FileLabelCache.java │ │ │ │ └── RetinaImageIcon.java │ │ │ ├── ui/ │ │ │ │ ├── InputField.java │ │ │ │ ├── TCheckBoxMenuItem.java │ │ │ │ ├── TMenuSeparator.java │ │ │ │ ├── TProgressBar.java │ │ │ │ ├── TRadioButtonMenuItem.java │ │ │ │ └── ZxSpectrumLoadPane.java │ │ │ └── utils/ │ │ │ ├── FileUtils.java │ │ │ ├── ImageSizeDetector.java │ │ │ ├── JavaClassVersionDetector.java │ │ │ ├── StrUtils.java │ │ │ ├── StringStream.java │ │ │ └── search/ │ │ │ ├── BytesSearchPattern.java │ │ │ ├── InputStreamSource.java │ │ │ ├── SearchException.java │ │ │ ├── SearchPattern.java │ │ │ ├── SearchSourceStream.java │ │ │ ├── SearchUtils.java │ │ │ ├── StringCaseInsensitiveSearchPattern.java │ │ │ └── StringCaseSensitiveSearchPattern.java │ │ └── se/ │ │ └── vidstige/ │ │ └── jadb/ │ │ ├── AdbFilterInputStream.java │ │ ├── AdbFilterOutputStream.java │ │ ├── AdbServerLauncher.java │ │ ├── ConnectionToRemoteDeviceException.java │ │ ├── DeviceDetectionListener.java │ │ ├── DeviceWatcher.java │ │ ├── HostConnectToRemoteTcpDevice.java │ │ ├── HostDisconnectFromRemoteTcpDevice.java │ │ ├── ITransportFactory.java │ │ ├── JadbConnection.java │ │ ├── JadbDevice.java │ │ ├── JadbException.java │ │ ├── LookBackFilteringOutputStream.java │ │ ├── RemoteFile.java │ │ ├── RemoteFileRecord.java │ │ ├── Stream.java │ │ ├── Subprocess.java │ │ ├── SyncTransport.java │ │ ├── Transport.java │ │ ├── managers/ │ │ │ ├── Bash.java │ │ │ ├── Package.java │ │ │ ├── PackageManager.java │ │ │ └── PropertyManager.java │ │ └── server/ │ │ ├── AdbDeviceResponder.java │ │ ├── AdbProtocolHandler.java │ │ ├── AdbResponder.java │ │ ├── AdbServer.java │ │ └── SocketServer.java │ └── resources/ │ ├── META-INF/ │ │ └── LICENSE │ ├── avr/ │ │ ├── avr_commands.properties │ │ └── avrdude.csv │ ├── com/ │ │ └── mucommander/ │ │ └── commons/ │ │ └── file/ │ │ └── mime.types │ ├── dictionary.properties │ ├── dictionary_ar_SA.properties │ ├── dictionary_be_BY.properties │ ├── dictionary_ca_ES.properties │ ├── dictionary_cs_CZ.properties │ ├── dictionary_da_DA.properties │ ├── dictionary_de_DE.properties │ ├── dictionary_en_GB.properties │ ├── dictionary_en_US.properties │ ├── dictionary_es_ES.properties │ ├── dictionary_fr_FR.properties │ ├── dictionary_hu_HU.properties │ ├── dictionary_it_IT.properties │ ├── dictionary_ja_JP.properties │ ├── dictionary_ko_KR.properties │ ├── dictionary_nl_NL.properties │ ├── dictionary_no_NO.properties │ ├── dictionary_pl_PL.properties │ ├── dictionary_pt_BR.properties │ ├── dictionary_ro_RO.properties │ ├── dictionary_ru_RU.properties │ ├── dictionary_sk_SK.properties │ ├── dictionary_sl_SL.properties │ ├── dictionary_sv_SV.properties │ ├── dictionary_uk_UA.properties │ ├── dictionary_zh_CN.properties │ ├── dictionary_zh_TW.properties │ ├── license.txt │ ├── logback.xml │ └── themes/ │ ├── ClassicCommander.xml │ ├── Native.xml │ ├── RetroCommander.xml │ ├── Striped.xml │ ├── Trol.xml │ └── editor/ │ ├── Dark.xml │ ├── Default-alt.xml │ ├── Default.xml │ ├── Eclipse.xml │ ├── Idea.xml │ └── VisualStudio.xml └── test/ ├── java/ │ ├── com/ │ │ └── mucommander/ │ │ ├── XmlResourceTest.java │ │ ├── command/ │ │ │ └── CommandTest.java │ │ ├── commons/ │ │ │ ├── conf/ │ │ │ │ ├── BufferedConfigurationExplorerTest.java │ │ │ │ ├── ConfigurationEventTest.java │ │ │ │ ├── ConfigurationExplorerTest.java │ │ │ │ ├── ConfigurationSectionTest.java │ │ │ │ ├── FileConfigurationSourceTest.java │ │ │ │ ├── ValueIteratorTest.java │ │ │ │ └── ValueListTest.java │ │ │ ├── file/ │ │ │ │ ├── AbstractFileTest.java │ │ │ │ ├── DefaultFileURLTest.java │ │ │ │ ├── DefaultPathCanonizerTest.java │ │ │ │ ├── FileFactoryTest.java │ │ │ │ ├── FileURLTestCase.java │ │ │ │ ├── SimpleFileAttributesTest.java │ │ │ │ ├── archiver/ │ │ │ │ │ └── ISOArchiverTest.java │ │ │ │ ├── filter/ │ │ │ │ │ └── ExtensionFilenameFilterTest.java │ │ │ │ ├── impl/ │ │ │ │ │ ├── ProxyFileTest.java │ │ │ │ │ ├── TestFile.java │ │ │ │ │ ├── ftp/ │ │ │ │ │ │ ├── FTPFileTest.java │ │ │ │ │ │ └── FTPFileURLTest.java │ │ │ │ │ ├── hadoop/ │ │ │ │ │ │ ├── HDFSFileTest.java │ │ │ │ │ │ └── HDFSFileURLTest.java │ │ │ │ │ ├── http/ │ │ │ │ │ │ ├── HTTPFileURLTest.java │ │ │ │ │ │ └── HTTPSFileURLTest.java │ │ │ │ │ ├── iso/ │ │ │ │ │ │ └── MuCreateISOTest.java │ │ │ │ │ ├── local/ │ │ │ │ │ │ ├── LocalFileTest.java │ │ │ │ │ │ ├── LocalFileURLTest.java │ │ │ │ │ │ └── WindowsTest.java │ │ │ │ │ ├── nfs/ │ │ │ │ │ │ └── NFSFileURLTest.java │ │ │ │ │ ├── s3/ │ │ │ │ │ │ ├── S3FileTest.java │ │ │ │ │ │ └── S3FileURLTest.java │ │ │ │ │ ├── sftp/ │ │ │ │ │ │ ├── SFTPFileTest.java │ │ │ │ │ │ └── SFTPFileURLTest.java │ │ │ │ │ ├── smb/ │ │ │ │ │ │ ├── SMBFileTest.java │ │ │ │ │ │ └── SMBFileURLTest.java │ │ │ │ │ └── zip/ │ │ │ │ │ └── ZipArchiveFileTest.java │ │ │ │ └── util/ │ │ │ │ ├── FileComparatorTest.java │ │ │ │ ├── FileMonitorTest.java │ │ │ │ ├── PathTokenizerTest.java │ │ │ │ ├── PathUtilsTest.java │ │ │ │ └── ResourceLoaderTest.java │ │ │ ├── io/ │ │ │ │ ├── BoundedInputStreamTest.java │ │ │ │ ├── BoundedOutputStreamTest.java │ │ │ │ ├── BoundedReaderTest.java │ │ │ │ ├── BufferPoolTest.java │ │ │ │ ├── base64/ │ │ │ │ │ └── Base64Test.java │ │ │ │ ├── bom/ │ │ │ │ │ └── BOMTest.java │ │ │ │ └── compound/ │ │ │ │ ├── CompoundInputStreamTest.java │ │ │ │ └── CompoundReaderTest.java │ │ │ └── util/ │ │ │ └── StringUtilsTest.java │ │ ├── ui/ │ │ │ ├── action/ │ │ │ │ └── impl/ │ │ │ │ └── PasteClipboardFilesActionTest.java │ │ │ ├── dialog/ │ │ │ │ └── file/ │ │ │ │ ├── BatchRenameTest.java │ │ │ │ └── RenamePropertiesTest.java │ │ │ ├── icon/ │ │ │ │ └── CustomFileIconProviderTest.java │ │ │ └── macosx/ │ │ │ └── AppleScriptTest.java │ │ └── utils/ │ │ ├── text/ │ │ │ └── SizeFormatTest.java │ │ └── xml/ │ │ ├── XmlAttributesTest.java │ │ └── XmlWriterTest.java │ └── ru/ │ └── trolsoft/ │ ├── hexeditor/ │ │ └── search/ │ │ └── ByteBufferSearchUtilsTest.java │ └── test/ │ ├── VariableArgumentsProvider.java │ └── VariableSource.java ├── kotlin/ │ ├── com/ │ │ └── mucommander/ │ │ └── ui/ │ │ └── viewer/ │ │ └── image/ │ │ └── SvgImageSupportTest.kt │ └── ru/ │ └── trolsoft/ │ └── calculator/ │ └── CalculatorTest.kt └── resources/ ├── logback.xml └── mockito-extensions/ └── org.mockito.plugins.MockMaker ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *-ide-plugin.xml *.class *.ear *.ec *.egg-info *.iml *.ipr *.iws *.MainThread-* *.pid *.pid.lock *.pyc *.sqlite *.swp *.war *.bak *class *.log *~ .classpath .DS_Store .gradle .idea .idea/ .idea/* .installed.cfg .project .settings .settings/ .svn .svn/* /bin /bin/ bin build develop-eggs dist dist/* docs/ eggs Gemfile.lock git.properties ivy*jar logs parts reports reports/* target/ test/.coverage test/coverage.xml test/nosetests.xml test-output/ tmp tmp/* ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/workspace.xml .idea/tasks.xml .idea/dictionaries .idea/vcs.xml .idea/jsLibraryMappings.xml # Sensitive or high-churn files: .idea/dataSources.ids .idea/dataSources.xml .idea/dataSources.local.xml .idea/sqlDataSources.xml .idea/dynamic.xml .idea/uiDesigner.xml # Gradle: .idea/gradle.xml .idea/libraries # Mongo Explorer plugin: .idea/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ trolCommander ============= Fork of muCommander file manager See a full description at https://trolsoft.ru/en/soft/trolcommander/ How to build ============ Build jar file: `gradlew clean build` Nightly builds ============== Latest jar file: https://trolsoft.ru/content/soft/trolcommander/nightly/trolcommander.jar Latest MacOS dmg: https://trolsoft.ru/content/soft/trolcommander/nightly/trolCommander.dmg ================================================ FILE: build.gradle ================================================ plugins { id 'java' id 'application' id "idea" id 'com.gradleup.shadow' version '9.2.0' id 'org.jetbrains.kotlin.jvm' version '2.3.20' id 'com.github.ben-manes.versions' version '0.53.0' id 'org.jetbrains.kotlin.plugin.lombok' version '2.3.20' } group = 'ru.trolsoft' version = '1.0.0' java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } def opensJvmApps = [ '--add-opens', 'java.base/java.io=ALL-UNNAMED', '--add-opens', 'java.desktop/sun.awt.X11=ALL-UNNAMED', '--add-opens', 'java.base/java.net=ALL-UNNAMED', '--add-opens', 'java.desktop/javax.swing.plaf.basic=ALL-UNNAMED', '--add-opens', 'java.desktop/sun.awt.X11=ALL-UNNAMED', '--add-opens', 'java.transaction.xa/javax.transaction.xa=ALL-UNNAMED', '--add-opens', 'java.management/javax.management=ALL-UNNAMED', '--add-opens', 'java.rmi/java.rmi=ALL-UNNAMED', '--add-opens', 'java.security.jgss/org.ietf.jgss=ALL-UNNAMED', '--add-opens', 'java.sql/java.sql=ALL-UNNAMED', '--add-opens', 'java.base/sun.net.www.protocol.http=ALL-UNNAMED', '--add-opens', 'java.base/sun.net.www.protocol.https=ALL-UNNAMED', '--add-opens', 'jdk.httpserver/com.sun.net.httpserver=ALL-UNNAMED', '--add-opens', 'java.compiler/javax.lang.model.element=ALL-UNNAMED' ] def macosJvmArgs = [ '--add-opens', 'java.desktop/com.apple.laf=ALL-UNNAMED', '--add-opens', 'java.desktop/com.apple.laf.AquaLookAndFeel=ALL-UNNAMED', '--add-exports', 'java.desktop/com.apple.laf=ALL-UNNAMED', '--add-exports', 'java.desktop/com.apple.eawt=ALL-UNNAMED', '--add-opens', 'java.desktop/com.apple.eawt=ALL-UNNAMED', '-Dcom.apple.smallTabs=true', '-Dcom.apple.hwaccel', '-Dapple.laf.useScreenMenuBar=true', '-Xdock:name=trolCommander' ] def isMac = System.getProperty('os.name').toLowerCase().contains('mac') application { mainClass = 'com.mucommander.TrolCommander' applicationDefaultJvmArgs = [ '-Xmx128m', '-Xms128m', '-XX:ReservedCodeCacheSize=64m', '-XX:+IgnoreUnrecognizedVMOptions', '-Djava.system.class.loader=com.mucommander.commons.file.AbstractFileClassLoader', '-Dslf4j.provider=ch.qos.logback.classic.spi.LogbackServiceProvider', '-Dfile.encoding=UTF-8' ] + opensJvmApps + (isMac ? macosJvmArgs : []) } repositories { mavenCentral() flatDir { dirs 'lib/runtime', 'lib/compile', 'lib/test' } } dependencies { compileOnly 'org.projectlombok:lombok:1.18.36' annotationProcessor 'org.projectlombok:lombok:1.18.36' testCompileOnly 'org.projectlombok:lombok:1.18.36' testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' implementation name: 'apple/AppleJavaExtensions-1.6' implementation name: 'jediterm/jediterm-ui-3.66-SNAPSHOT' implementation name: 'jediterm/jediterm-core-3.66-SNAPSHOT' implementation name: 'jediterm/JediTerm-3.66-SNAPSHOT' implementation name: 'jediterm/pty4j-0.13.4' implementation name: 'jediterm/annotations' implementation name: 'jediterm/jzlib-1.1.1' implementation name: 'jediterm/purejavacomm' implementation name: 'jets3t/jets3t-0.7.2' implementation name: 'vmware/vim25-2.5' implementation name: 'quaqua/quaqua-native' implementation name: 'quaqua/quaqua' implementation name: 'jets3t/jets3t-0.7.2' implementation name: 'jcifs/jcifs-1.3.18-kohsuke-2-SNAPSHOT' implementation name: 'java-iso-tools/iso9660-writer-2.0.0' implementation name: 'java-iso-tools/sabre-2.0.0' implementation name: 'image4j' implementation name: 'javadjvu' implementation name: 'jftp-1.60-trol1' implementation name: 'jide-oss-3.7.4' implementation name: 'rsyntaxtextarea-3.6.3-SNAPSHOT' implementation name: 'sardine-5.3' implementation name: 'sevenzipjbinding-AllPlatforms' implementation name: 'sevenzipjbinding-Mac-arm64' implementation name: 'sevenzipjbinding' implementation name: 'trolcommander-native' implementation name: 'trolsoft' implementation name: 'VAqua13' // lib.compile compileOnly name: 'jfxrt' if (!isMac) { compileOnly name: 'apple/ui' } implementation 'org.apache.xmlgraphics:batik-anim:1.19' implementation 'org.apache.xmlgraphics:batik-awt-util:1.19' implementation 'org.apache.xmlgraphics:batik-bridge:1.19' implementation 'org.apache.xmlgraphics:batik-codec:1.19' implementation 'org.apache.xmlgraphics:batik-css:1.19' implementation 'org.apache.xmlgraphics:batik-dom:1.19' implementation 'org.apache.xmlgraphics:batik-ext:1.19' implementation 'org.apache.xmlgraphics:batik-extension:1.19' implementation 'org.apache.xmlgraphics:batik-gui-util:1.19' implementation 'org.apache.xmlgraphics:batik-gvt:1.19' implementation 'org.apache.xmlgraphics:batik-parser:1.19' implementation 'org.apache.xmlgraphics:batik-script:1.19' implementation 'org.apache.xmlgraphics:batik-svg-dom:1.19' implementation 'org.apache.xmlgraphics:batik-svggen:1.19' implementation 'org.apache.xmlgraphics:batik-swing:1.19' implementation 'org.apache.xmlgraphics:batik-transcoder:1.19' implementation "org.apache.xmlgraphics:batik-util:1.19" implementation "org.apache.xmlgraphics:batik-xml:1.19" implementation 'xml-apis:xml-apis:2.0.2' implementation 'org.apache.xmlgraphics:xmlgraphics-commons:2.11' implementation 'commons-net:commons-net:3.9.0' implementation 'commons-collections:commons-collections:3.2.2' implementation 'commons-logging:commons-logging:1.2' implementation 'commons-io:commons-io:2.20.0' implementation 'commons-collections:commons-collections:3.2.2' implementation 'org.apache.commons:commons-collections4:4.5.0' implementation 'commons-beanutils:commons-beanutils:1.9.4' implementation 'org.apache.commons:commons-imaging:1.0-alpha2' implementation 'org.apache.hadoop:hadoop-common:2.6.0' implementation 'xerces:xercesImpl:2.12.2' implementation 'ch.qos.logback:logback-classic:1.5.32' implementation 'ch.qos.logback:logback-core:1.5.32' implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'com.formdev:flatlaf:3.7.1' // JNA // implementation 'net.java.dev.jna:jna:5.13.0' implementation 'oro:oro:2.0.8' implementation 'org.yaml:snakeyaml:2.4' implementation 'com.twelvemonkeys.imageio:imageio-core:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-psd:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-webp:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-dds:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-hdr:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-icns:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-iff:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-pcx:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-pnm:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-sgi:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-tga:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-webp:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-xwd:3.13.1' implementation 'com.twelvemonkeys.imageio:imageio-metadata:3.13.1' implementation 'com.twelvemonkeys.common:common-lang:3.13.' implementation 'com.twelvemonkeys.common:common-io:3.13.1' implementation 'net.java.dev.jna:jna:5.18.1' implementation 'net.java.dev.jna:jna-platform:5.18.1' implementation 'com.fifesoft.rtext:fife.common:6.0.3' implementation 'org.jmdns:jmdns:3.6.3' implementation 'com.sshtools:j2ssh-maverick:1.5.5' implementation 'org.slf4j:jcl-over-slf4j:2.0.17' implementation 'org.slf4j:jul-to-slf4j:2.0.17' implementation 'org.slf4j:slf4j-api:2.0.17' implementation 'com.googlecode.plist:dd-plist:1.29' implementation 'com.github.pcorless.icepdf:icepdf-core:7.3.2' implementation 'com.github.pcorless.icepdf:icepdf-viewer:7.3.2' testImplementation 'org.junit.jupiter:junit-jupiter:6.0.3' testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.3' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:6.0.3' testImplementation 'org.mockito:mockito-core:5.23.0' testImplementation 'org.mockito:mockito-junit-jupiter:5.23.0' testImplementation 'org.assertj:assertj-core:3.27.7' testImplementation "io.kotest:kotest-assertions-core:6.1.9" } sourceSets { main { java { srcDirs = ['src/main/java'] } resources { srcDirs = ['src/main/resources'] exclude '**/*.java' } } test { java { srcDirs = ['src/test/java'] } resources { srcDirs = ['src/test/resources'] } } } jar { manifest { attributes( 'Main-Class': application.mainClass, 'Specification-Title': 'trolCommander', 'Specification-Vendor': 'Oleg Trifonov', 'Specification-Version': project.version, 'Implementation-Title': 'trolCommander', 'Implementation-Vendor': 'Oleg Trifonov', 'Implementation-Version': project.version, 'Build-Date': new Date().format('yyyy-MM-dd'), 'Build-Time': new Date().format('hh:mm:ss'), 'Build-URL': 'http://www.trolsoft.ru/content/soft/trolcommander/version.xml', 'Class-Path': configurations.runtimeClasspath.files.collect { "lib/${it.name}" }.join(' ') ) } // from('res/runtime') { // include '**/*' // exclude '**/*.java' // } } tasks.withType(Jar).configureEach { exclude('META-INF/maven/**', 'docs/**') exclude { def name = it.name.toLowerCase() def path = it.relativePath.pathString ((name.contains('license') && name.endsWith('.txt')) || name.equals('readme.txt')) && path != 'license.txt' } } // ==================== SHADOW JAR (fat-jar со всеми зависимостями) ==================== shadowJar { archiveBaseName.set('trolcommander') archiveClassifier.set('') archiveVersion.set(project.version) manifest { inheritFrom jar.manifest } // Исключение подписей зависимостей (чтобы не было конфликтов) exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' // Объединение сервис-файлов mergeServiceFiles() } tasks.named('shadowDistZip') { archiveFileName.set("trolCommander-${project.version}.zip") from("src/main/resources/images/trolcommander") { include "icon*.png" into "trolCommander-${project.version}/icons" } } // Задача для сборки с локальными зависимостями tasks.register('buildWithLocalLibs') { group = 'build' description = 'Сборка с использованием локальных JAR из lib/' dependsOn 'shadowJar' } // Задача для сборки с миграцией на Maven tasks.register('buildWithMaven') { group = 'build' description = 'Сборка с приоритетом Maven-зависимостей' dependsOn 'shadowJar' } // Задача для запуска приложения tasks.register('runApp', JavaExec) { group = 'application' description = 'Запуск trolCommander' mainClass = application.mainClass classpath = sourceSets.main.runtimeClasspath jvmArgs = application.applicationDefaultJvmArgs } // ==================== УТИЛИТЫ ДЛЯ МИГРАЦИИ ==================== // Задача: показать, какие зависимости берутся из локальных файлов tasks.register('listLocalDependencies') { group = 'help' description = 'Показать зависимости из локальной папки lib/' doLast { def localDeps = configurations.runtimeClasspath.files.findAll { it.absolutePath.contains('/lib/') } if (localDeps.isEmpty()) { println "✓ Все зависимости загружаются из Maven-репозиториев" } else { println "⚠ Локальные зависимости:" localDeps.each { println " - ${it.name}" } println "\n💡 Подсказка: замените 'implementation name: ...' на 'implementation \"group:artifact:version\"" } } } test { useJUnitPlatform() // dependsOn 'compileJava', 'processResources' jvmArgs = [ '-Djava.awt.headless=true', '-Djava.system.class.loader=com.mucommander.commons.file.AbstractFileClassLoader' ] testLogging { //events 'passed', 'skipped', 'failed' events 'skipped', 'failed' exceptionFormat = 'full' showStandardStreams = true } } // Отключение предупреждений о кодировке tasks.withType(JavaCompile) { options.encoding = 'UTF-8' options.compilerArgs += ['-Xlint:unchecked', '-Xlint:deprecation'] options.annotationProcessorPath = configurations.annotationProcessor if (isMac) { options.compilerArgs += [ '--add-exports', 'java.desktop/com.apple.laf=ALL-UNNAMED', '--add-exports', 'java.desktop/com.apple.eawt=ALL-UNNAMED', '--add-opens', 'java.desktop/com.apple.laf=ALL-UNNAMED', '--add-opens', 'java.desktop/com.apple.eawt=ALL-UNNAMED' ] } } // Копирование библиотек в output для классического запуска (опционально) tasks.register('copyLibs', Copy) { from configurations.runtimeClasspath into "$buildDir/libs" } //tasks.named('startScripts') { // dependsOn 'shadowJar' // // Перенастроить класспасс на shadowJar // classpath = files(shadowJar.archiveFile) //} // //// И использовать другой classifier для shadowJar, чтобы не конфликтовать //shadowJar { // archiveClassifier = 'all' // будет trolcommander-0.9.9-all.jar //} // Отключить стандартные задачи дистрибуции (конфликтуют с shadowJar) tasks.named('distZip') { enabled = false } tasks.named('distTar') { enabled = false } tasks.named('startScripts') { enabled = false } // Отключить стандартный jar, если используете только shadowJar tasks.named('jar') { enabled = false } // Сделать shadowJar задачей сборки по умолчанию tasks.named('assemble') { dependsOn 'shadowJar' } tasks.named('compileJava') { dependsOn 'processResources' } ================================================ FILE: build.properties ================================================ # - Build configuration ------------------------------------------------------------------------------------------------ # ---------------------------------------------------------------------------------------------------------------------- # If set to true, the build will fail whenever a release artifact can't be generated because an external tool isn't # available. build.pedantic=false # If set to true, the generated artifacts are not considered to be part of an official release. # This impacts file names (trolcommander-current.. instead of trolcommander-${app.version}..) as well as the metadata of # some artifacts (the debian version number will use a timestamp, for example). build.snapshot=false # - Application configuration ------------------------------------------------------------------------------------------ # ---------------------------------------------------------------------------------------------------------------------- # Name of the application. # This should probably not be modified. app.name=trolCommander # Copyright holder. # This should probably not be modified. app.vendor=Oleg Trifonov # trolCommander version. app.version=0.9.9 # Fully qualified name of the class that starts trolCommander. app.main=com.mucommander.TrolCommander # trolCommander copyright line. app.copyright=2002-2023 # - Source code configuration ------------------------------------------------------------------------------------------ # ---------------------------------------------------------------------------------------------------------------------- # Java version the source was written for. source.version=17 # Encoding of the source code. source.encoding=UTF-8 # - Application URLs --------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------- # trolCommander homepage. url.homepage=http://www.trolsoft.ru/en/soft/trolcommander # URL that trolCommander should connect to to check for new versions. url.version=http://www.trolsoft.ru/content/soft/trolcommander/version.xml # URL that users can go to to download the latest trolCommander version. url.download=http://www.trolsoft.ru/en/soft/trolcommander/#download # URL of the latest trolCommander JAR file. url.jar=http://www.trolsoft.ru/content/soft/trolcommander/download/trolcommander.jar # URL of the trolCommander JNLP. url.jnlp=http://www.trolsoft.ru/soft/trolcommander/webstart/ # - External tools ----------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------- # Path to the 7z executable. # If this is not set, compression will be done using standard ZIP algorithms. 7za.executable=/Users/trol/Bin/7za # Password for the JAR keystore. # If you're not Maxence, you needn't bother with this. store.pass= # Path to the NSIS installation dir. # If not set, Windows executables won't be generated. nsis.dir=/usr/local/Cellar/makensis/3.0/share/nsis # Path to the NSIS executable file. # This will more often than not be ${nsis.dir}/makensis. # If not set, Windows executables won't be generated. nsis.bin=/usr/local/Cellar/makensis/3.0/bin/makensis # Path to the launch4j installation dir. # If not set, Windows executable won't be generated. launch4j.dir=/Users/trol/Bin/launch4j ================================================ FILE: build_template.properties ================================================ # - Build configuration ------------------------------------------------------------------------------------------------ # ---------------------------------------------------------------------------------------------------------------------- # If set to true, the build will fail whenever a release artifact can't be generated because an external tool isn't # available. build.pedantic=false # If set to true, the generated artifacts are not considered to be part of an official release. # This impacts file names (mucommander-current.. instead of mucommander-${app.version}..) as well as the metadata of # some artifacts (the debian version number will use a timestamp, for example). build.snapshot=true # - Application configuration ------------------------------------------------------------------------------------------ # ---------------------------------------------------------------------------------------------------------------------- # Name of the application. # This should probably not be modified. app.name=muCommander # Copyright holder. # This should probably not be modified. app.vendor=Maxence Bernard # muCommander version. app.version=0.9.0 # Fully qualified name of the class that starts muCommander. app.main=com.mucommander.TrolCommander # muCommander copyright line. app.copyright=2002-2012 # - Source code configuration ------------------------------------------------------------------------------------------ # ---------------------------------------------------------------------------------------------------------------------- # Java version the source was written for. source.version=1.5 # Encoding of the source code. source.encoding=UTF-8 # - Application URLs --------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------- # muCommander homepage. url.homepage=http://www.mucommander.com # URL that muCommander should connect to to check for new versions. url.version=http://www.mucommander.com/version/version.xml # URL that users can go to to download the latest muCommander version. url.download=http://www.mucommander.com/#download # URL of the latest muCommander JAR file. url.jar=http://www.mucommander.com/download/mucommander.jar # URL of the muCommander JNLP. url.jnlp=http://www.mucommander.com/webstart/ # - External tools ----------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------- # Path to the 7z executable. # If this is not set, compression will be done using standard ZIP algorithms. 7za.executable= # Password for the JAR keystore. # If you're not Maxence, you needn't bother with this. store.pass= # Path to the NSIS installation dir. # If not set, Windows executables won't be generated. nsis.dir= # Path to the NSIS executable file. # This will more often than not be ${nsis.dir}/makensis. # If not set, Windows executables won't be generated. nsis.bin= # Path to the launch4j installation dir. # If not set, Windows executable won't be generated. launch4j.dir= ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Настройки сборки org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 org.gradle.parallel=true org.gradle.caching=true # Версия приложения app.version=1.0.0 app.vendor=Oleg Trifonov # Путь к локальным библиотекам (для гибкости) lib.runtime.dir=lib/runtime lib.compile.dir=lib/compile lib.test.dir=lib/test ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # 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 # # https://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. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH= @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: jni/Makefile ================================================ all: ./build.sh ================================================ FILE: jni/build.sh ================================================ export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin/" -o libtrolsoft.jnilib -shared ru_trolsoft_jni_NativeFileUtils.c cp -f libtrolsoft.jnilib ../ jar cvf ../lib/runtime/trolsoft.jar libtrolsoft.jnilib ================================================ FILE: jni/ru_trolsoft_jni_NativeFileUtils.c ================================================ #include #include "ru_trolsoft_jni_NativeFileUtils.h" #include #include #include #include #include #include #define VERSION 2 #define FA_MASK_EXISTS 1 #define FA_MASK_DIRECTORY 2 #define FA_MASK_HIDDEN 4 // https://www.gnu.org/software/libc/manual/html_node/Testing-File-Type.html static bool is_regular_file(const char *path) { struct stat path_stat; stat(path, &path_stat); return S_ISREG(path_stat.st_mode); } static bool is_directory(const char *path) { struct stat path_stat; stat(path, &path_stat); return S_ISDIR(path_stat.st_mode); } static bool is_executable_file(const char *path) { struct stat path_stat; return (stat(path, &path_stat) == 0 && path_stat.st_mode & S_IXUSR); } static bool is_hidden_file(const char *path) { char *name = strrchr(path, '/'); char folderName[PATH_MAX]; if (name && name[0] == '/' && name[1] == 0) { uint len = strlen(path); if (!len || len < 2) { return false; } folderName[len-1] = 0; for (uint i = len-2; i != 0; i--) { folderName[i] = path[i]; if (path[i] == '/') { name = &folderName[i]; break; } } } if (name) { if (name[1] == 0) { return false; } else if (name[1] == '.') { return true; } else if (name[2] == 0) { uint len = strlen(path); if (len < 2) { return false; } name = NULL; for (uint i = len-2; i != 0; i--) { if (path[i] == '/') { return path[i+1] == '.'; } } } return false; } return path[0] == '.'; } JNIEXPORT jint JNICALL Java_ru_trolsoft_jni_NativeFileUtils_getLibraryVersion (JNIEnv *env, jclass class) { return VERSION; } JNIEXPORT jint JNICALL Java_ru_trolsoft_jni_NativeFileUtils_getLocalFileAttributes (JNIEnv *env, jclass class, jstring path) { if (path == NULL) { return 0; } const char *pathUtf = (*env)->GetStringUTFChars(env, path, NULL); bool exists = access(pathUtf, F_OK) != -1; bool isDirectory = is_directory(pathUtf); bool isHidden = is_hidden_file(pathUtf); jint res = 0; if (exists) { res |= FA_MASK_EXISTS; } if (isDirectory) { res |= FA_MASK_DIRECTORY; } if (isHidden) { res |= FA_MASK_HIDDEN; } (*env)->ReleaseStringUTFChars(env, path, pathUtf); return res; } JNIEXPORT jboolean JNICALL Java_ru_trolsoft_jni_NativeFileUtils_isLocalFileHidden (JNIEnv *env, jclass class, jstring path) { if (path == NULL) { return false; } const char *pathUtf = (*env)->GetStringUTFChars(env, path, NULL); bool result = is_hidden_file(pathUtf); (*env)->ReleaseStringUTFChars(env, path, pathUtf); return result; } JNIEXPORT jboolean JNICALL Java_ru_trolsoft_jni_NativeFileUtils_isLocalFileExecutable (JNIEnv *env, jclass class, jstring path) { if (path == NULL) { return false; } const char *pathUtf = (*env)->GetStringUTFChars(env, path, NULL); bool result = is_executable_file(pathUtf); (*env)->ReleaseStringUTFChars(env, path, pathUtf); return result; } JNIEXPORT jboolean JNICALL Java_ru_trolsoft_jni_NativeFileUtils_isLocalDirectory (JNIEnv *env, jclass class, jstring path) { if (path == NULL) { return false; } const char *pathUtf = (*env)->GetStringUTFChars(env, path, NULL); bool result = is_directory(pathUtf); (*env)->ReleaseStringUTFChars(env, path, pathUtf); return result; } ================================================ FILE: jni/ru_trolsoft_jni_NativeFileUtils.h ================================================ /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class ru_trolsoft_jni_FileUtils */ #ifndef _Included_ru_trolsoft_jni_NativeFileUtils #define _Included_ru_trolsoft_jni_NativeFileUtils #ifdef __cplusplus extern "C" { #endif JNIEXPORT jint JNICALL Java_ru_trolsoft_jni_NativeFileUtils_getLibraryVersion (JNIEnv *, jclass); /* * Class: ru_trolsoft_jni_FileUtils * Method: getLocalFileAttributes * Signature: (Ljava/lang/String;)I */ JNIEXPORT jint JNICALL Java_ru_trolsoft_jni_NativeFileUtils_getLocalFileAttributes (JNIEnv *, jclass, jstring); JNIEXPORT jboolean JNICALL Java_ru_trolsoft_jni_NativeFileUtils_isLocalFileHidden (JNIEnv *, jclass, jstring); JNIEXPORT jboolean JNICALL Java_ru_trolsoft_jni_NativeFileUtils_isLocalFileExecutable (JNIEnv *, jclass, jstring); JNIEXPORT jboolean JNICALL Java_ru_trolsoft_jni_NativeFileUtils_isLocalDirectory (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif ================================================ FILE: lib/compile/jfxrt.jar ================================================ [File too large to display: 14.6 MB] ================================================ FILE: lib/tools/version.txt ================================================ proguard.jar -> 4.11 jdeb.jar -> 1.2 antdoclet.jar -> 1.1 (patched to disable Velocity logging). velocity.jar -> 1.3.1 simian.jar -> 2.2.21 javancss.jar -> 28.49 jdepend.jar -> 2.9 doccheck.jar -> 1.2b2 antcontrib.jar -> 1.0b3 muant.jar -> 1.0 (compiled from original extracted mucommander source rev. 3628) ================================================ FILE: readme.txt ================================================ ████████╗██████╗ ██████╗ ██╗ ╚══██╔══╝██╔══██╗██╔═══██╗██║ ██║ ██████╔╝██║ ██║██║ ██║ ██╔══██╗██║ ██║██║ ██║ ██║ ██║╚██████╔╝███████╗ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ ███████╗██████╗ ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝██╔══██╗ ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║█████╗ ██████╔╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗ ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝███████╗██║ ██║ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝ -------------------- trolCommander v1.0.0 -------------------- trolCommander is a lightweight, cross-platform file manager with a dual-pane interface. It runs on any operating system with Java support (Mac OS X, Windows, Linux, *BSD, Solaris...). Documentation can be found at http://trac.mucommander.com/ . Please visit the muCommander forums (http://www.mucommander.com/forums) to ask questions, suggest features or report a bug. Your feedback is always welcome! Official website: http://trolsoft.ru/en/soft/trolcommander Copyright (C) 2002-2014 Maxence Bernard. Copyright (C) 2013-2026 Oleg Trifonov. Requirements ------------ A Java Runtime Environment (JRE) 1.7 or later is required to run trolCommander. Java 1.8 is recommended, you can download it at http://java.com. Mac OS X users: your favorite OS already comes with a Java runtime so you're good to go! If you're having problems launching trolCommander, make sure the JAVA_HOME environment variable points to the directory where your Java runtime is installed. What's new since v0.9 ? ----------------------- New features: - Lock tab capability, which prevents closing/moving the tab or changing its location. - New quick list that presents open tabs in the current panel, mapped onto Alt+6 by default (ticket #450). - Added the option to set fixed title for tab. - Added the following actions: add tab, duplicate tab, clone tab to other panel. - Added support for VMware vSphere virtual machines file system, contributed by Yuval Kohavi Improvements: - The state of all windows from last run is now restored on startup. - Added the ability to copy the base name of files (ticket #462), contributed by Chen Rozenes. - User can choose to always display tabs headers from preferences dialog (even when the panel contains single tab). - Add the application name to window title on all OSs except Mac OS X (ticket #501). - The visited locations history is now saved per-tab. - The recently visited locations quick list now presents the visited locations on all tabs and windows. - The content of recently visited locations quick list is now restored from previous run on startup (ticket #471). - Added fullscreen support for Mac OS X Lion (ticket #468). - Text file editor/viewer restore the full screen mode of last used (closed) editor/viewer on startup. - 'Bonjour' support is now disabled by default on Mac OS (on fresh installation, i.e, with no previous preferences) to prevent firewall dialog which keeps popping up on startup (workaround for ticket #339). - Added 'ctrl+m' keystroke to toggle text file editor/viewer full screen mode. - Tab can be closed by clicking on its header with middle mouse button. - Assign 'ctrl+page_down' keystroke for switching to next tab, and 'ctrl+page_up' for switching to previous tab (the keystrokes that were previously assigned to those actions remain as alternative keystrokes). - Improved names and descriptions presented for tab-related actions. - Added new category of actions in the 'shortcuts dialog' for tab-related actions. - Changed tab's not-fixed-title to be in the pattern ':' - Show backward/forward locations list when pressing with right click on the back/forward buttons in the toolbar instead of trigger back/forward actions - Keyboard shortcuts can now be set for commands defined at commands.xml (ticket #456), contributed by Jarek Czekalski. - Show empty name in the make file/directory dialog when it is opened (ticket #512), contributed by hclsiva. - Mac OS X: enabled high-resolution rendering on Retina displays (ticket #518), contributed Alexey Lysiuk. - Added Windows 8 and Mac OS X 10.8 to the OS versions. - System files can now be filtered also on windows, contributed by Markus Bullmann. Localization: - Turkish translation has been updated. Bug fixes: - Prevent deadlock which caused the application to freeze while switching tabs on MAC OS. - Recycle Bin is now working on Windows 64-bit with a 64-bit Java runtime (ticket #234). - Key combinations that contain the TAB key can be set as shortcuts (ticket #465). - Fix installation via software center on Ubuntu. - Symbolic links cannot be opened (ticket #467). - Encoding of text file is changed after being modified by the viewer/editor (ticket #438). - Cannot connect to some FTP/SFTP bookmarks if there are more than 4 of them (ticket #525), contributed by Ondrej Dusek. - Quick lists on the right panel sometimes not being focused (ticket #552), contributed by Jarek Czekalski. Known issues: - Some translations may not be up-to-date. Refer to http://trac.mucommander.com/wiki/Translations for more information. - Mac OS X: "Do you want the application "muCommander.app" to accept incoming network connections?" dialog keeps popping up on startup even if the dialog has been previously accepted (ticket #339), when 'Bonjour' support is enabled. - Executable permissions on local files are not properly preserved when running a unix-based OS with Java 1.5. - SMB support may not work properly on non multi-language JRE. - 'Copy files to clipboard' not working with some applications (files are not pasted). - Mac OS X: some keyboard shortcuts may conflict with global system shortcuts. - Authentication issues when using several sets of credentials (login/password) for the same server (see ticket #76). - Untrusted HTTPS connections are allowed without a warning. - Windows Vista/7: "java.net.SocketException: Permission denied: recv failed" error can appear when trying to access FTP sites. This seems to be a Windows firewall problem, with a possible workaround: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7077696 - Unpacking files from 7z archive files can be slow. License ------- muCommander is released under the terms of the GNU General Public License. Please refer to the 'license.txt' file bundled with muCommander. muCommander uses the following great third party works : - the Ant library released under the Apache License. Ant can be found at http://ant.apache.org . - Apache Commons libraries released under the Apache License. Apache Commons can be found at http://commons.apache.org . - Apache Hadoop released under the Apache License. Apache Hadoop can be found at http://hadoop.apache.org . - the Furbelow library released under the GNU LGPL. Furbelow can be found at http://sourceforge.net/projects/furbelow . - the ICU4J library released under the ICU License. the ICU project can be found at http://icu-project.org . - the J2SSH library released under the GNU LGPL. J2SSH can be found at http://www.sshtools.com . - the J7Zip library released under the GNU LGPL. J7Zip can be found at http://sourceforge.net/projects/p7zip/ . - the jCIFS library released under the GNU LGPL. jCIFS can be found at http://jcifs.samba.org . - the JetS3t library released under the Apache License. JetS3t can be found at http://jets3t.s3.amazonaws.com/index.html . - the JmDNS library released under the GNU LGPL. JmDNS can be found at http://jmdns.sourceforge.net . - the JNA library released under the GNU LGPL. JmDNS can be found at http://jna.dev.java.net . - the JUnRar library released as Freeware. JUnRar can be found at http://sourceforge.net/projects/java-unrar . - the Yanfs library released under the BSD license. Yanfs can be found at http://yanfs.dev.java.net . - Icons by Mark James released under the Creative Commons Attribution License. Mark James can be found at http://famfamfam.com . Credits ------- Core developers: - Maxence Bernard - Nicolas Rinaudo - Arik Hadas - Mariusz Jakubowski Contributors: - Ivan Baidakov - Vassil Dichev - Karel Klic - David Kovar - Joshua Lebo - LeO - Xavier Martin - Alejandro Scandroli - Alexander Yerenkow - Johann Schmitz Translators: - Frank Berger and Tony Klüver (German) - Marcos Cobeña and Xavi Miró (Spanish) - Jaromír Mára and Peter Vasko (Czech) - Kent Hsu (Traditional Chinese) - Jioh L. Jung (Korean) - Andrzej Kosiński (Polish) - György Varga and Tamás Balogh-Walder (Hungarian) - 4X_Pro and Evgeny Morozov (Russian) - whiteriver and Woodie (Simplified Chinese) - Joze Kovacic (Slovenian) - Catalin Hritcu (Romanian) - Roberto Angeletti (Italian) - Cristiano Duarte (Brazilian Portuguese) - Pieter Kristensen (Dutch) - Ján Ľudvík (Slovak) - Jonathan Murphy (British English) - Nardog (Japanese) - Jakob Ekström (Swedish) - Jeppe Toustrup (Danish) - Mykola Bilovus (Ukrainian) - ChArLoK_16 (Arabic) - vboo (Belarusian) - Ingrid Amundsen (Norwegian) - Emre Aytaç (Turkish) - Jordi Plantalech (Catalan) Special thanks: - Semyon Filippov (muCommander icon) - Stefano Perelli (former muCommander icon) Many thanks to all of you who suggested new features, reported bugs, sent warm emails or generously donated to the project ! Command Line Interface ---------------------- muCommander comes with a few command line switches. The following options are available: -a FILE, --assoc FILE Load associations from FILE. -b FILE, --bookmarks FILE Load bookmarks from FILE. -c FILE, --configuration FILE Load configuration from FILE -C FILE, --commandbar FILE Load command bar from FILE. -e FOLDER, --extensions FOLDER Load extensions from FOLDER. -f FILE, --commands FILE Load custom commands from FILE. -i, --ignore-warnings Do not fail on warnings (default). -k FILE, --keymap FILE Load keymap from FILE -p FOLDER, --preferences FOLDER Store configuration files in FOLDER -S, --silent Do not print verbose error messages -s FILE, --shell-history FILE Load shell history from FILE -t FILE, --toolbar FILE Load toolbar from FILE -u FILE, --credentials FILE Load credentials from FILE -h, --help Print the help text and exit -v, --version Print the version and exit -V, --verbose Print verbose error messages (default) -w, --fail-on-warnings Quits when a warning is encountered during the boot process. In addition to these, muCommander will interpret anything that comes after the last switch as a URI and load it in its windows. So for example: mucommander -b ~/.bookmarks.xml ftp://user@myftp.com ~/dev http://slashdot.org Will: - read bookmarks from ~/bookmarks.xml - load a connection to myftp.com in the left panel of the main window - load ~/dev in the right panel of the main window - open a second window and load http://slashdot.org in its left panel - load the default directory in the second window's fourth panel Documentation ------------- Documentation on how to use, customize and extend muCommander is available at: http://trac.mucommander.com ================================================ FILE: release-linux.sh ================================================ VERSION='1.0.0' TMP_OUT_PATH=dist #TMP_RES_PATH=dist/resources OUT_PATH=dist ARCH=$(uname -m) set -e ./gradlew clean build mkdir -p $TMP_OUT_PATH #mkdir -p $TMP_RES_PATH cp build/libs/trolcommander-$VERSION.jar $TMP_OUT_PATH/trolcommander-linux.jar zip -d $TMP_OUT_PATH/trolcommander-linux.jar "Windows-amd64/*" "Windows-x86/*" "Mac-arm64/*" "Mac-x86_64/*" "win/*" "jtermios/freebsd/*" "jtermios/Mac-x86_64/*" "jtermios/solaris/*" "jtermios/windows/*" zip -d $TMP_OUT_PATH/trolcommander-linux.jar "com/sun/jna/freebsd-amd64/*" "com/sun/jna/freebsd-i386/*" "com/sun/jna/platform/win32/*" "com/sun/jna/platform/wince/*" "com/sun/jna/sunos-amd64/*" "com/sun/jna/sunos-sparc/*" "com/sun/jna/sunos-sparcv9/*" "com/sun/jna/sunos-x86/*" "com/sun/jna/w32ce-arm/*" "com/sun/jna/win32/*" "com/sun/jna/win32-amd64/*" "com/sun/jna/win32-x86/*" export JVM_OPENS="--add-opens java.desktop/com.apple.eawt=ALL-UNNAMED --add-opens java.desktop/com.apple.laf=ALL-UNNAMED --add-opens java.desktop/com.apple.eio=ALL-UNNAMED --add-opens java.desktop/com.apple.laf.AquaLookAndFeel=ALL-UNNAMED" export JVM_PARAMS="-Xmx128m -Xms128m -Dfile.encoding=UTF-8 -XX:ReservedCodeCacheSize=64m -XX:+IgnoreUnrecognizedVMOptions" export JVM_LOGING="-Dslf4j.provider=ch.qos.logback.classic.spi.LogbackServiceProvider" export JVM_OPTS="$JVM_OPENS $JVM_PARAMS $JVM_LOGING -Djava.system.class.loader=com.mucommander.commons.file.AbstractFileClassLoader" #cp res/package/osx/icon.icns $TMP_RES_PATH/trolCommander.icns jpackage --input "$TMP_OUT_PATH/" \ --name trolCommander \ --app-version $VERSION \ --main-jar trolcommander-linux.jar \ --main-class com.mucommander.TrolCommander \ --resource-dir "$TMP_RES_PATH" \ --java-options "$JVM_OPTS" \ --type rpm \ --dest "${OUT_PATH}" jpackage --input "$TMP_OUT_PATH/" \ --name trolCommander \ --app-version $VERSION \ --main-jar trolcommander-linux.jar \ --main-class com.mucommander.TrolCommander \ --resource-dir "$TMP_RES_PATH" \ --java-options "$JVM_OPTS" \ --type deb \ --dest $OUT_PATH # --runtime-image 'jre/linux' \ #mv $OUT_PATH/trolCommander-$VERSION.dmg $OUT_PATH/trolCommander-$ARCH-$VERSION.dmg #rm $TMP_RES_PATH/trolCommander.icns #mv $TMP_OUT_PATH/trolcommander-lunux.jar $OUT_PATH/trolcommander-linux.jar #rmdir $TMP_RES_PATH #rmdir $TMP_OUT_PATH #rmdir dist/macos ================================================ FILE: release.sh ================================================ VERSION='1.0.0' JAVA_HOME_MAC_X64=./tools/jdk21-macos-x64/Contents/Home/bin/ JAVA_HOME_WINDOWS=./tools/jdk21-windows/bin OUT_PATH=dist TMP_OUT_PATH_MAC=dist/macos/jar TMP_OUT_PATH_WIN=dist/windows/jar TMP_RES_PATH=dist/resources OUT_PATH=dist #ARCH=$(uname -m) MAIN_CLASS="com.mucommander.TrolCommander" CLASS_LOADER="com.mucommander.commons.file.AbstractFileClassLoader" APP_NAME="trolCommander" set -e ./gradlew clean build mkdir -p $TMP_OUT_PATH_MAC mkdir -p $TMP_OUT_PATH_WIN mkdir -p $TMP_RES_PATH # ------- macos jar ------------- cp build/libs/trolcommander-$VERSION.jar $TMP_OUT_PATH_MAC/trolcommander-macosx.jar zip -d $TMP_OUT_PATH_MAC/trolcommander-macosx.jar "Windows-amd64/*" "Windows-x86/*" "Linux-amd64/*" "Linux-i386/*" "win/*" "linux/*" "jtermios/freebsd/*" "jtermios/linux/*" "jtermios/solaris/*" "jtermios/windows/*" zip -d $TMP_OUT_PATH_MAC/trolcommander-macosx.jar "com/sun/jna/freebsd-amd64/*" "com/sun/jna/freebsd-i386/*" "com/sun/jna/linux-amd64/*" "com/sun/jna/linux-arm/*" "com/sun/jna/linux-i386/*" "com/sun/jna/linux-ia64/*" "com/sun/jna/linux-ppc/*" "com/sun/jna/linux-ppc64/*" "com/sun/jna/platform/win32/*" "com/sun/jna/platform/wince/*" "com/sun/jna/sunos-amd64/*" "com/sun/jna/sunos-sparc/*" "com/sun/jna/sunos-sparcv9/*" "com/sun/jna/sunos-x86/*" "com/sun/jna/w32ce-arm/*" "com/sun/jna/win32/*" "com/sun/jna/win32-amd64/*" "com/sun/jna/win32-x86/*" # -------- Windows jar ---------- cp build/libs/trolcommander-$VERSION.jar $TMP_OUT_PATH_WIN/trolcommander-windows.jar zip -d $TMP_OUT_PATH_WIN/trolcommander-windows.jar "Mac-arm64/*" "Mac-x86_64/*" "jtermios/Mac-x86_64/*" "jtermios/solaris/*" zip -d $TMP_OUT_PATH_WIN/trolcommander-windows.jar "jtermios/freebsd/*" "com/sun/jna/freebsd-amd64/*" "com/sun/jna/freebsd-i386/*" "com/sun/jna/platform/wince/*" "com/sun/jna/sunos-amd64/*" "com/sun/jna/sunos-sparc/*" "com/sun/jna/sunos-sparcv9/*" "com/sun/jna/sunos-x86/*" "com/sun/jna/w32ce-arm/*" zip -d $TMP_OUT_PATH_WIN/trolcommander-windows.jar "Linux-amd64/*" "Linux-i386/*" "linux/*" "jtermios/freebsd/*" "jtermios/linux/*" "jtermios/solaris/*" zip -d $TMP_OUT_PATH_WIN/trolcommander-windows.jar "com/sun/jna/linux-amd64/*" "com/sun/jna/linux-arm/*" "com/sun/jna/linux-i386/*" "com/sun/jna/linux-ia64/*" "com/sun/jna/linux-ppc/*" "com/sun/jna/linux-ppc64/*" "com/sun/jna/platform/wince/*" "com/sun/jna/sunos-amd64/*" "com/sun/jna/sunos-sparc/*" "com/sun/jna/sunos-sparcv9/*" "com/sun/jna/sunos-x86/*" JVM_OPENS="--add-opens java.desktop/javax.swing.plaf.basic=ALL-UNNAMED\ --add-opens java.base/java.io=ALL-UNNAMED\ --add-opens java.base/java.net=ALL-UNNAMED\ --add-opens java.transaction.xa/javax.transaction.xa=ALL-UNNAMED\ --add-opens java.management/javax.management=ALL-UNNAMED\ --add-opens java.rmi/java.rmi=ALL-UNNAMED\ --add-opens java.security.jgss/org.ietf.jgss=ALL-UNNAMED\ --add-opens java.sql/java.sql=ALL-UNNAMED\ --add-opens java.base/sun.net.www.protocol.http=ALL-UNNAMED\ --add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED\ --add-opens java.compiler/javax.lang.model.element=ALL-UNNAMED" JVM_OPENS_APPLE="--add-opens java.desktop/com.apple.eawt=ALL-UNNAMED\ --add-opens java.desktop/com.apple.laf=ALL-UNNAMED\ --add-opens java.desktop/com.apple.eio=ALL-UNNAMED\ --add-opens java.desktop/com.apple.laf.AquaLookAndFeel=ALL-UNNAMED" JVM_PARAMS="-Xmx128m -Xms128m -Dfile.encoding=UTF-8 -XX:ReservedCodeCacheSize=64m -XX:+IgnoreUnrecognizedVMOptions" JVM_LOGING="-Dslf4j.provider=ch.qos.logback.classic.spi.LogbackServiceProvider" JVM_OPTS="$JVM_OPENS $JVM_OPENS_APPLE $JVM_PARAMS $JVM_LOGING -Djava.system.class.loader=$CLASS_LOADER -Dlog.stdout=false" # ------------ shadow cp build/distributions/trolCommander-$VERSION.zip $OUT_PATH/ # ---------- macos ------------------- cp res/package/osx/icon.icns $TMP_RES_PATH/trolCommander.icns ARCH="arm64" jpackage --input "$TMP_OUT_PATH_MAC/" \ --name $APP_NAME \ --app-version $VERSION \ --main-jar trolcommander-macosx.jar \ --main-class $MAIN_CLASS \ --resource-dir "$TMP_RES_PATH" \ --java-options "$JVM_OPTS" \ --type app-image \ --dest "${OUT_PATH}/macos-$ARCH" jpackage --input "$TMP_OUT_PATH_MAC/" \ --name $APP_NAME \ --app-version $VERSION \ --main-jar trolcommander-macosx.jar \ --main-class $MAIN_CLASS \ --resource-dir "$TMP_RES_PATH" \ --java-options "$JVM_OPTS" \ --type dmg \ --dest $OUT_PATH mv $OUT_PATH/trolCommander-$VERSION.dmg $OUT_PATH/trolCommander-$ARCH-$VERSION.dmg ARCH="x64" $JAVA_HOME_MAC_X64/jpackage --input "$TMP_OUT_PATH_MAC/" \ --name $APP_NAME \ --app-version $VERSION \ --main-jar trolcommander-macosx.jar \ --main-class $MAIN_CLASS \ --resource-dir "$TMP_RES_PATH" \ --java-options "$JVM_OPTS" \ --type app-image \ --dest "${OUT_PATH}/macos-$ARCH" $JAVA_HOME_MAC_X64/jpackage --input "$TMP_OUT_PATH_MAC/" \ --name $APP_NAME \ --app-version $VERSION \ --main-jar trolcommander-macosx.jar \ --main-class $MAIN_CLASS \ --resource-dir "$TMP_RES_PATH" \ --java-options "$JVM_OPTS" \ --type dmg \ --dest $OUT_PATH mv $OUT_PATH/trolCommander-$VERSION.dmg $OUT_PATH/trolCommander-$ARCH-$VERSION.dmg rm $TMP_RES_PATH/trolCommander.icns mv $TMP_OUT_PATH_MAC/trolcommander-macosx.jar $OUT_PATH/trolcommander-macosx.jar rmdir $TMP_RES_PATH rmdir $TMP_OUT_PATH_MAC rmdir dist/macos # ----------- windows JVM_OPTS="$JVM_OPENS $JVM_PARAMS $JVM_LOGING -Djava.system.class.loader=$CLASS_LOADER" wine $JAVA_HOME_WINDOWS/jpackage.exe \ --type app-image \ --input "$TMP_OUT_PATH_WIN/" \ --name $APP_NAME \ --app-version $VERSION \ --main-jar trolcommander-windows.jar \ --main-class $MAIN_CLASS \ --java-options "$JVM_OPTS" \ --resource-dir "package/windows" \ --dest ${OUT_PATH} cd $OUT_PATH zip -rm trolCommander-windows-${VERSION}.zip trolCommander cd .. mv $TMP_OUT_PATH_WIN/trolcommander-windows.jar $OUT_PATH/trolcommander-windows.jar rmdir $TMP_OUT_PATH_WIN rmdir dist/windows ================================================ FILE: res/jar/services/services/org.apache.hadoop.crypto.key.KeyProviderFactory ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.crypto.key.JavaKeyStoreProvider$Factory org.apache.hadoop.crypto.key.UserProvider$Factory org.apache.hadoop.crypto.key.kms.KMSClientProvider$Factory ================================================ FILE: res/jar/services/services/org.apache.hadoop.fs.FileSystem ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.fs.LocalFileSystem org.apache.hadoop.fs.viewfs.ViewFileSystem org.apache.hadoop.fs.ftp.FTPFileSystem org.apache.hadoop.fs.HarFileSystem org.apache.hadoop.hdfs.DistributedFileSystem org.apache.hadoop.hdfs.web.HftpFileSystem org.apache.hadoop.hdfs.web.HsftpFileSystem org.apache.hadoop.hdfs.web.WebHdfsFileSystem org.apache.hadoop.hdfs.web.SWebHdfsFileSystem ================================================ FILE: res/jar/services/services/org.apache.hadoop.io.compress.CompressionCodec ================================================ # # 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. # org.apache.hadoop.io.compress.BZip2Codec org.apache.hadoop.io.compress.DefaultCodec org.apache.hadoop.io.compress.DeflateCodec org.apache.hadoop.io.compress.GzipCodec org.apache.hadoop.io.compress.Lz4Codec org.apache.hadoop.io.compress.SnappyCodec ================================================ FILE: res/jar/services/services/org.apache.hadoop.security.SecurityInfo ================================================ # # 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. # org.apache.hadoop.security.AnnotatedSecurityInfo ================================================ FILE: res/jar/services/services/org.apache.hadoop.security.alias.CredentialProviderFactory ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.security.alias.JavaKeyStoreProvider$Factory org.apache.hadoop.security.alias.UserProvider$Factory ================================================ FILE: res/jar/services/services/org.apache.hadoop.security.token.TokenIdentifier ================================================ # # 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. # org.apache.hadoop.hdfs.security.token.block.BlockTokenIdentifier org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier$WebHdfsDelegationTokenIdentifier org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier$SWebHdfsDelegationTokenIdentifier ================================================ FILE: res/jar/services/services/org.apache.hadoop.security.token.TokenRenewer ================================================ # # 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. # org.apache.hadoop.hdfs.DFSClient$Renewer org.apache.hadoop.hdfs.security.token.block.BlockTokenIdentifier$Renewer org.apache.hadoop.hdfs.web.TokenAspect$TokenManager ================================================ FILE: res/jar/servicesCOMMON/org.apache.hadoop.crypto.key.KeyProviderFactory ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.crypto.key.JavaKeyStoreProvider$Factory org.apache.hadoop.crypto.key.UserProvider$Factory org.apache.hadoop.crypto.key.kms.KMSClientProvider$Factory ================================================ FILE: res/jar/servicesCOMMON/org.apache.hadoop.fs.FileSystem ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.fs.LocalFileSystem org.apache.hadoop.fs.viewfs.ViewFileSystem org.apache.hadoop.fs.ftp.FTPFileSystem org.apache.hadoop.fs.HarFileSystem ================================================ FILE: res/jar/servicesCOMMON/org.apache.hadoop.io.compress.CompressionCodec ================================================ # # 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. # org.apache.hadoop.io.compress.BZip2Codec org.apache.hadoop.io.compress.DefaultCodec org.apache.hadoop.io.compress.DeflateCodec org.apache.hadoop.io.compress.GzipCodec org.apache.hadoop.io.compress.Lz4Codec org.apache.hadoop.io.compress.SnappyCodec ================================================ FILE: res/jar/servicesCOMMON/org.apache.hadoop.security.SecurityInfo ================================================ # # 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. # org.apache.hadoop.security.AnnotatedSecurityInfo ================================================ FILE: res/jar/servicesCOMMON/org.apache.hadoop.security.alias.CredentialProviderFactory ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.security.alias.JavaKeyStoreProvider$Factory org.apache.hadoop.security.alias.UserProvider$Factory ================================================ FILE: res/jar/servicesHDFS/org.apache.hadoop.fs.FileSystem ================================================ # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You 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. org.apache.hadoop.hdfs.DistributedFileSystem org.apache.hadoop.hdfs.web.HftpFileSystem org.apache.hadoop.hdfs.web.HsftpFileSystem org.apache.hadoop.hdfs.web.WebHdfsFileSystem org.apache.hadoop.hdfs.web.SWebHdfsFileSystem ================================================ FILE: res/jar/servicesHDFS/org.apache.hadoop.security.token.TokenIdentifier ================================================ # # 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. # org.apache.hadoop.hdfs.security.token.block.BlockTokenIdentifier org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier$WebHdfsDelegationTokenIdentifier org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier$SWebHdfsDelegationTokenIdentifier ================================================ FILE: res/jar/servicesHDFS/org.apache.hadoop.security.token.TokenRenewer ================================================ # # 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. # org.apache.hadoop.hdfs.DFSClient$Renewer org.apache.hadoop.hdfs.security.token.block.BlockTokenIdentifier$Renewer org.apache.hadoop.hdfs.web.TokenAspect$TokenManager ================================================ FILE: res/package/unix/deb/control ================================================ Package: mucommander Version: @VERSION@ Section: utils Priority: optional Recommends: java-virtual-machine, java2-runtime Suggests: sun-java6-jre Architecture: all Installed-Size: @SIZE@ Maintainer: Oleg Trifonov Description: a lightweight, cross-platform file manager trolCommander is a lightweight, cross-platform file manager running on any operating system with Java support. It features a dual-pane interface in the tradition of Norton Commander and other commanders, allowing to quickly and efficiently manage your files. . trolCommander comes with built-in support for a variety of file protocols (Local, FTP, SFTP, SMB, NFS, HTTP) and archive formats (ZIP, TAR, GZIP, BZIP2, ISO, NRG, AR, DEB, LST, 7Z, RAR) and is available in 18 languages. . Java 1.7 or higher is required to run trolCommander. . trolCommander can be found at http://www.trolsoft.ru/trolcommander ================================================ FILE: res/package/unix/deb/postinst ================================================ #! /bin/sh ln -s /usr/share/trolcommander/trolcommander.sh /usr/bin/trolcommander ================================================ FILE: res/package/unix/deb/postrm ================================================ #! /bin/sh if [ -h /usr/bin/trolcommander ] ; then rm /usr/bin/trolcommander fi ================================================ FILE: res/package/unix/trolcommander.desktop ================================================ [Desktop Entry] Name=muCommander Comment=A lightweight, cross-platform file manager Exec=mucommander Icon=mucommander Terminal=false Type=Application Categories=Application;FileManager;Utility; StartupNotify=true ================================================ FILE: res/package/unix/trolcommander.sh ================================================ #! /bin/sh TROLCOMMANDER_ARGS="@ARGS@" JAVA_ARGS="@JAVA_ARGS@" # Locates the java executable. if [ "$JAVA_HOME" != "" ] ; then JAVA=$JAVA_HOME/bin/java else JAVACMD=`which java 2> /dev/null ` if [ -z "$JAVACMD" ] ; then echo "Error: cannot find java VM." exit 1 else JAVA=java fi fi # Resolve the path to the trolcommander.jar located in the same directory as this script if [ -h $0 ] then # This script has been invoked from a symlink, resolve the link's target (i.e. the path to this script) TROLCOMMANDER_SH=`ls -l "$0"` TROLCOMMANDER_SH=${TROLCOMMANDER_SH#*-> } else TROLCOMMANDER_SH=$0 fi CURRENT_DIR=`dirname "$TROLCOMMANDER_SH"` TROLCOMMANDER_JAR=$CURRENT_DIR/trolcommander.jar OPEN_ARGS="--add-opens java.base/java.io=ALL-UNNAMED --add-opens java.desktop/sun.awt.X11=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.desktop/javax.swing.plaf.basic=ALL-UNNAMED --add-opens java.transaction.xa/javax.transaction.xa=ALL-UNNAMED --add-opens java.management/javax.management=ALL-UNNAMED --add-opens java.rmi/java.rmi=ALL-UNNAMED --add-opens java.security.jgss/org.ietf.jgss=ALL-UNNAMED --add-opens java.sql/java.sql=ALL-UNNAMED --add-opens java.base/sun.net.www.protocol.http=ALL-UNNAMED --add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED --add-opens jdk.httpserver/com.sun.net.httpserver=ALL-UNNAMED --add-opens java.compiler/javax.lang.model.element=ALL-UNNAMED" if [ ! -f $TROLCOMMANDER_JAR ] then echo "Error: cannot find file trolcommander.jar in directory $CURRENT_DIR" exit 1 fi # Starts trolcommander. $JAVA $JAVA_ARGS $OPEN_ARGS -DGNOME_DESKTOP_SESSION_ID=$GNOME_DESKTOP_SESSION_ID -DKDE_FULL_SESSION=$KDE_FULL_SESSION -DKDE_SESSION_VERSION=$KDE_SESSION_VERSION -jar $TROLCOMMANDER_JAR $TROLCOMMANDER_ARGS $@ ================================================ FILE: res/package/version.xml ================================================ @VERSION@ @DATE@ @DOWNLOAD_URL@ @JAR_URL@ ================================================ FILE: res/package/windows/trolcommander.nsi ================================================ ; -*- coding: utf-8 -*- ; trolcommander install script ; ; Include Modern UI !include MUI2.nsh ; The name of the installer Name "trolCommander @MU_VERSION@" ; The file to write OutFile @MU_OUT@ ; Installer icon !define MUI_ICON @MU_ICON@ !define MUI_UNICON @MU_ICON@ ; The default installation directory InstallDir $PROGRAMFILES\trolCommander ; Registry key to check for directory (so if you install again, it will ; overwrite the old one automatically) InstallDirRegKey HKLM SOFTWARE\trolCommander "Install_Dir" ; Specifies the requested execution level for Windows Vista. ; Necessary for correct uninstallation of Start menu shortcuts. RequestExecutionLevel admin ; Pages !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_DIRECTORY !define MUI_COMPONENTSPAGE_NODESC !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_INSTFILES !define MUI_FINISHPAGE_RUN "$INSTDIR\trolCommander.exe" !define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED !define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\readme.txt" !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_WELCOME !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH ; Languages ; Installer should support same languages as trolCommander. !insertmacro MUI_LANGUAGE "English" ; first language is the default language !insertmacro MUI_LANGUAGE "French" !insertmacro MUI_LANGUAGE "Spanish" !insertmacro MUI_LANGUAGE "SpanishInternational" !insertmacro MUI_LANGUAGE "German" !insertmacro MUI_LANGUAGE "Czech" !insertmacro MUI_LANGUAGE "SimpChinese" !insertmacro MUI_LANGUAGE "TradChinese" !insertmacro MUI_LANGUAGE "Polish" !insertmacro MUI_LANGUAGE "Hungarian" !insertmacro MUI_LANGUAGE "Russian" !insertmacro MUI_LANGUAGE "Slovenian" !insertmacro MUI_LANGUAGE "Romanian" !insertmacro MUI_LANGUAGE "Italian" !insertmacro MUI_LANGUAGE "Korean" !insertmacro MUI_LANGUAGE "Portuguese" !insertmacro MUI_LANGUAGE "PortugueseBR" !insertmacro MUI_LANGUAGE "Dutch" !insertmacro MUI_LANGUAGE "Slovak" !insertmacro MUI_LANGUAGE "Japanese" !insertmacro MUI_LANGUAGE "Swedish" !insertmacro MUI_LANGUAGE "Danish" ; The stuff to install Section "trolCommander @MU_VERSION@ (required)" ; Read only section. It will always be set to install. SectionIn RO ; Set output path to the installation directory. SetOutPath $INSTDIR ; Copy trolCommander files File /oname=trolCommander.exe @MU_EXE@ File /oname=trolcommander.jar @MU_JAR@ File /oname=readme.txt @MU_README@ File /oname=license.txt @MU_LICENSE@ ; Write the installation path into the registry WriteRegStr HKLM SOFTWARE\trolCommander "Install_Dir" "$INSTDIR" ; Write the uninstall keys for Windows WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\trolCommander" "DisplayName" "trolCommander (remove only)" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\trolCommander" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\trolCommander" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\trolCommander" "NoRepair" 1 WriteUninstaller "uninstall.exe" ; Create Start Menu directory and shortcuts CreateDirectory "$SMPROGRAMS\trolCommander" CreateShortCut "$SMPROGRAMS\trolCommander\trolCommander.lnk" "$INSTDIR\trolCommander.exe" "" "" 0 SW_SHOWMINIMIZED CreateShortCut "$SMPROGRAMS\trolCommander\Read Me.lnk" "$INSTDIR\readme.txt" "" "" 0 CreateShortCut "$SMPROGRAMS\trolCommander\License.lnk" "$INSTDIR\license.txt" "" "" 0 CreateShortCut "$SMPROGRAMS\trolCommander\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "" 0 SectionEnd ; Quick launch shortcut (optional section) Section "Quick Launch shortcut" CreateShortCut "$QUICKLAUNCH\trolCommander.lnk" "$INSTDIR\trolCommander.exe" "" "" 0 SW_SHOWMINIMIZED SectionEnd ; Desktop shortcut (optional section) Section "Desktop shortcut" CreateShortCut "$DESKTOP\trolCommander.lnk" "$INSTDIR\trolCommander.exe" "" "" 0 SW_SHOWMINIMIZED SectionEnd ; Special uninstall section. Section "Uninstall" ; remove registry keys DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\trolCommander" DeleteRegKey HKLM SOFTWARE\trolCommander ; remove files Delete $INSTDIR\trolCommander.exe Delete $INSTDIR\trolcommander.jar Delete $INSTDIR\trolCommander.lnk Delete $INSTDIR\readme.txt Delete $INSTDIR\license.txt ; MUST REMOVE UNINSTALLER, too Delete $INSTDIR\uninstall.exe ; remove shortcuts, if any. Delete "$SMPROGRAMS\trolCommander\*.*" Delete "$QUICKLAUNCH\trolCommander.lnk" Delete "$DESKTOP\trolCommander.lnk" ; remove directories used. RMDir "$SMPROGRAMS\trolCommander" RMDir "$INSTDIR" SectionEnd ; eof ================================================ FILE: settings.gradle ================================================ rootProject.name = 'trolcommander' // Включение локальных модулей (если нужно) // include ':libs:local-lib1' // project(':libs:local-lib1').projectDir = file('lib/local-lib1') ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetDetector.java ================================================ /* ******************************************************************************* * Copyright (C) 2005-2013, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.text; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * CharsetDetector provides a facility for detecting the * charset or encoding of character data in an unknown format. * The input data can either be from an input stream or an array of bytes. * The result of the detection operation is a list of possibly matching * charsets, or, for simple use, you can just ask for a Java Reader that * will will work over the input data. *

* Character set detection is at best an imprecise operation. The detection * process will attempt to identify the charset that best matches the characteristics * of the byte data, but the process is partly statistical in nature, and * the results can not be guaranteed to always be correct. *

* For best accuracy in charset detection, the input data should be primarily * in a single language, and a minimum of a few hundred bytes worth of plain text * in the language are needed. The detection process will attempt to * ignore html or xml style markup that could otherwise obscure the content. * stable ICU 3.4 */ public class CharsetDetector { // Question: Should we have getters corresponding to the setters for input text // and declared encoding? // A thought: If we were to create our own type of Java Reader, we could defer // figuring out an actual charset for data that starts out with too much English // only ASCII until the user actually read through to something that didn't look // like 7 bit English. If nothing else ever appeared, we would never need to // actually choose the "real" charset. All assuming that the application just // wants the data, and doesn't care about a char set name. /** * Constructor * */ public CharsetDetector() { } /** * Set the declared encoding for charset detection. * The declared encoding of an input text is an encoding obtained * from an http header or xml declaration or similar source that * can be provided as additional information to the charset detector. * A match between a declared encoding and a possible detected encoding * will raise the quality of that detected encoding by a small delta, * and will also appear as a "reason" for the match. *

* A declared encoding that is incompatible with the input data being * analyzed will not be added to the list of possible encodings. * * @param encoding The declared encoding * @return this * */ public CharsetDetector setDeclaredEncoding(String encoding) { fDeclaredEncoding = encoding; return this; } /** * Set the input text (byte) data whose charset is to be detected. * * @param in the input text of unknown encoding * * @return This CharsetDetector * */ public CharsetDetector setText(byte [] in) { fRawInput = in; fRawLength = in.length; return this; } private static final int kBufSize = 8000; /** * Set the input text (byte) data whose charset is to be detected. *

* The input stream that supplies the character data must have markSupported() * == true; the charset detection process will read a small amount of data, * then return the stream to its original position via * the InputStream.reset() operation. The exact amount that will * be read depends on the characteristics of the data itself. * * @param in the input text of unknown encoding * * @return This CharsetDetector * * @throws IOException if an I/O error occurs. * */ public CharsetDetector setText(InputStream in) throws IOException { fInputStream = in; fInputStream.mark(kBufSize); fRawInput = new byte[kBufSize]; // Always make a new buffer because the // previous one may have come from the caller, // in which case we can't touch it. fRawLength = 0; int remainingLength = kBufSize; while (remainingLength > 0 ) { // read() may give data in smallish chunks, esp. for remote sources. Hence, this loop. int bytesRead = fInputStream.read(fRawInput, fRawLength, remainingLength); if (bytesRead <= 0) { break; } fRawLength += bytesRead; remainingLength -= bytesRead; } fInputStream.reset(); return this; } /** * Return the charset that best matches the supplied input data. * Note though, that because the detection * only looks at the start of the input data, * there is a possibility that the returned charset will fail to handle * the full set of input data. *

* Raise an exception if *

    *
  • no charset appears to match the data.
  • *
  • no input text has been provided
  • *
* * @return a CharsetMatch object representing the best matching charset, or * null if there are no matches. * */ public CharsetMatch detect() { // TODO: A better implementation would be to copy the detect loop from // detectAll(), and cut it short as soon as a match with a high confidence // is found. This is something to be done later, after things are otherwise // working. CharsetMatch[] matches = detectAll(); if (matches == null || matches.length == 0) { return null; } return matches[0]; } /** * Return an array of all charsets that appear to be plausible * matches with the input data. The array is ordered with the * best quality match first. *

* Raise an exception if *

    *
  • no charsets appear to match the input data.
  • *
  • no input text has been provided
  • *
* * @return An array of CharsetMatch objects representing possibly matching charsets. * */ public CharsetMatch[] detectAll() { ArrayList matches = new ArrayList<>(); MungeInput(); // Strip html markup, collect byte stats. // Iterate over all possible charsets, remember all that // give a match quality > 0. for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { CSRecognizerInfo rcInfo = ALL_CS_RECOGNIZERS.get(i); boolean active = (fEnabledRecognizers != null) ? fEnabledRecognizers[i] : rcInfo.isDefaultEnabled; if (active) { CharsetMatch m = rcInfo.recognizer.match(this); if (m != null) { matches.add(m); } } } Collections.sort(matches); // CharsetMatch compares on confidence Collections.reverse(matches); // Put best match first. CharsetMatch [] resultArray = new CharsetMatch[matches.size()]; resultArray = matches.toArray(resultArray); return resultArray; } /** * Autodetect the charset of an inputStream, and return a Java Reader * to access the converted input data. *

* This is a convenience method that is equivalent to * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getReader(); *

* For the input stream that supplies the character data, markSupported() * must be true; the charset detection will read a small amount of data, * then return the stream to its original position via * the InputStream.reset() operation. The exact amount that will * be read depends on the characteristics of the data itself. *

* Raise an exception if no charsets appear to match the input data. * * @param in The source of the byte data in the unknown charset. * * @param declaredEncoding A declared encoding for the data, if available, * or null or an empty string if none is available. * * @return Reader to access the converted input data * */ public Reader getReader(InputStream in, String declaredEncoding) { fDeclaredEncoding = declaredEncoding; try { setText(in); CharsetMatch match = detect(); if (match == null) { return null; } return match.getReader(); } catch (IOException e) { return null; } } /** * Autodetect the charset of an inputStream, and return a String * containing the converted input data. *

* This is a convenience method that is equivalent to * this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getString(); *

* Raise an exception if no charsets appear to match the input data. * * @param in The source of the byte data in the unknown charset. * * @param declaredEncoding A declared encoding for the data, if available, * or null or an empty string if none is available. * * @return a String containing the converted input data * */ public String getString(byte[] in, String declaredEncoding) { fDeclaredEncoding = declaredEncoding; try { setText(in); CharsetMatch match = detect(); if (match == null) { return null; } return match.getString(-1); } catch (IOException e) { return null; } } /** * Get the names of all charsets supported by CharsetDetector class. *

* Note: Multiple different charset encodings in a same family may use * a single shared name in this implementation. For example, this method returns * an array including "ISO-8859-1" (ISO Latin 1), but not including "windows-1252" * (Windows Latin 1). However, actual detection result could be "windows-1252" * when the input data matches Latin 1 code points with any points only available * in "windows-1252". * * @return an array of the names of all charsets supported by * CharsetDetector class. * */ public static String[] getAllDetectableCharsets() { String[] allCharsetNames = new String[ALL_CS_RECOGNIZERS.size()]; for (int i = 0; i < allCharsetNames.length; i++) { allCharsetNames[i] = ALL_CS_RECOGNIZERS.get(i).recognizer.getName(); } return allCharsetNames; } /** * Test whether input filtering is enabled. * * @return true if input text will be filtered. * * @see #enableInputFilter * */ public boolean inputFilterEnabled() { return fStripTags; } /** * Enable filtering of input text. If filtering is enabled, * text within angle brackets ("<" and ">") will be removed * before detection. * * @param filter true to enable input text filtering. * * @return The previous setting. * */ public boolean enableInputFilter(boolean filter) { boolean previous = fStripTags; fStripTags = filter; return previous; } /* * MungeInput - after getting a set of raw input data to be analyzed, preprocess * it by removing what appears to be html markup. */ private void MungeInput() { boolean inMarkup = false; int openTags = 0; int badTags = 0; // // html / xml markup stripping. // quick and dirty, not 100% accurate, but hopefully good enough, statistically. // discard everything within < brackets > // Count how many total '<' and illegal (nested) '<' occur, so we can make some // guess whether the input was actually marked up at all. int srci; if (fStripTags) { int dsti = 0; for (srci = 0; srci < fRawLength && dsti < fInputBytes.length; srci++) { byte b = fRawInput[srci]; if (b == (byte)'<') { if (inMarkup) { badTags++; } inMarkup = true; openTags++; } if (!inMarkup) { fInputBytes[dsti++] = b; } if (b == (byte)'>') { inMarkup = false; } } fInputLen = dsti; } // If it looks like this input wasn't marked up, or if it looks like it's // essentially nothing but markup abandon the markup stripping. // Detection will have to work on the unstripped input. if (openTags < 5 || openTags/5 < badTags || (fInputLen < 100 && fRawLength>600)) { int limit = fRawLength; if (limit > kBufSize) { limit = kBufSize; } for (srci=0; srci ALL_CS_RECOGNIZERS = List.of( new CSRecognizerInfo(new CharsetRecog_UTF8(), true), new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_BE(), true), new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_LE(), true), new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_BE(), true), new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_LE(), true), new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_sjis(), true), new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022JP(), true), new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022CN(), true), new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022KR(), true), new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_gb_18030(), true), new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_jp(), true), new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_kr(), true), new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_big5(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_1(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_2(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_5_ru(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_6_ar(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_7_el(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_I_he(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_he(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1251(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1256(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_KOI8_R(), true), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_9_tr(), true), // IBM 420/424 recognizers are disabled by default new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_rtl(), false), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_ltr(), false), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_rtl(), false), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_ltr(), false), new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_cp866(), true) ); /** * Get the names of charsets that can be recognized by this CharsetDetector instance. * * @return an array of the names of charsets that can be recognized by this CharsetDetector * instance. * internal * @deprecated This API is ICU internal only. */ public String[] getDetectableCharsets() { List csnames = new ArrayList<>(ALL_CS_RECOGNIZERS.size()); for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i); boolean active = (fEnabledRecognizers == null) ? rcinfo.isDefaultEnabled : fEnabledRecognizers[i]; if (active) { csnames.add(rcinfo.recognizer.getName()); } } return csnames.toArray(new String[0]); } /** * Enable or disable individual charset encoding. * A name of charset encoding must be included in the names returned by * {@link #getAllDetectableCharsets()}. * * @param encoding the name of charset encoding. * @param enabled true to enable, or false to disable the * charset encoding. * @return A reference to this CharsetDetector. * @throws IllegalArgumentException when the name of charset encoding is * not supported. * internal * @deprecated This API is ICU internal only. */ public CharsetDetector setDetectableCharset(String encoding, boolean enabled) { int modIdx = -1; boolean isDefaultVal = false; for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { CSRecognizerInfo csrinfo = ALL_CS_RECOGNIZERS.get(i); if (csrinfo.recognizer.getName().equals(encoding)) { modIdx = i; isDefaultVal = (csrinfo.isDefaultEnabled == enabled); break; } } if (modIdx < 0) { // No matching encoding found throw new IllegalArgumentException("Invalid encoding: " + "\"" + encoding + "\""); } if (fEnabledRecognizers == null && !isDefaultVal) { // create an array storing the non default setting fEnabledRecognizers = new boolean[ALL_CS_RECOGNIZERS.size()]; // Initialize the array with default info for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) { fEnabledRecognizers[i] = ALL_CS_RECOGNIZERS.get(i).isDefaultEnabled; } } if (fEnabledRecognizers != null) { fEnabledRecognizers[modIdx] = enabled; } return this; } } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetMatch.java ================================================ /* ******************************************************************************* * Copyright (C) 2005-2012, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.text; import org.jetbrains.annotations.NotNull; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; /** * This class represents a charset that has been identified by a CharsetDetector * as a possible encoding for a set of input data. From an instance of this * class, you can ask for a confidence level in the charset identification, * or for Java Reader or String to access the original byte data in Unicode form. *

* Instances of this class are created only by CharsetDetectors. *

* Note: this class has a natural ordering that is inconsistent with equals. * The natural ordering is based on the match confidence value. * * stable ICU 3.4 */ public class CharsetMatch implements Comparable { /** * create a java.io.Reader for reading the Unicode character data corresponding * to the original byte data supplied to the Charset detect operation. *

* CAUTION: if the source of the byte data was an InputStream, a Reader * can be created for only one matching char set using this method. If more * than one charset needs to be tried, the caller will need to reset * the InputStream and create InputStreamReaders itself, based on the charset name. * * @return the Reader for the Unicode character data. * */ public Reader getReader() { InputStream inputStream = fInputStream; if (inputStream == null) { inputStream = new ByteArrayInputStream(fRawInput, 0, fRawLength); } try { inputStream.reset(); return new InputStreamReader(inputStream, getName()); } catch (IOException e) { return null; } } /** * reate a Java String from Unicode character data corresponding * to the original byte data supplied to the Charset detect operation. * * @return a String created from the converted input data. * * @throws IOException if an IO error occurs. * */ public String getString() throws IOException { return getString(-1); } /** * create a Java String from Unicode character data corresponding * to the original byte data supplied to the Charset detect operation. * The length of the returned string is limited to the specified size; * the string will be trunctated to this length if necessary. A limit value of * zero or less is ignored, and treated as no limit. * * @param maxLength The maximium length of the String to be created when the * source of the data is an input stream, or -1 for * unlimited length. * @return a String created from the converted input data. * * @throws IOException if an IO error occurs. * */ String getString(int maxLength) throws IOException { String result; if (fInputStream != null) { StringBuilder sb = new StringBuilder(); char[] buffer = new char[1024]; Reader reader = getReader(); int max = maxLength < 0? Integer.MAX_VALUE : maxLength; int bytesRead; while ((bytesRead = reader.read(buffer, 0, Math.min(max, 1024))) >= 0) { sb.append(buffer, 0, bytesRead); max -= bytesRead; } reader.close(); return sb.toString(); } else { String name = getName(); /* * getName() may return a name with a suffix 'rtl' or 'ltr'. This cannot * be used to open a charset (e.g. IBM424_rtl). The ending '_rtl' or 'ltr' * should be stripped off before creating the string. */ int startSuffix = !name.contains("_rtl") ? name.indexOf("_ltr") : name.indexOf("_rtl"); if (startSuffix > 0) { name = name.substring(0, startSuffix); } result = new String(fRawInput, name); } return result; } /** * Get an indication of the confidence in the charset detected. * Confidence values range from 0-100, with larger numbers indicating * a better match of the input data to the characteristics of the charset. * @return the confidence in the charset match */ public int getConfidence() { return fConfidence; } /** * Get the name of the detected charset. * The name will be one that can be used with other APIs on the * platform that accept charset names. It is the "Canonical name" * as defined by the class java.nio.charset.Charset; for * charsets that are registered with the IANA charset registry, * this is the MIME-preferred registered name. * * @see java.nio.charset.Charset * @see java.io.InputStreamReader * * @return The name of the charset. * */ public String getName() { return fCharsetName; } /** * Get the ISO code for the language of the detected charset. * * @return The ISO code for the language or null if the language cannot be determined. * */ public String getLanguage() { return fLang; } /** * Compare to other CharsetMatch objects. * Comparison is based on the match confidence value, which * allows CharsetDetector.detectAll() to order its results. * * @param other the CharsetMatch object to compare against. * @return a negative integer, zero, or a positive integer as the * confidence level of this CharsetMatch * is less than, equal to, or greater than that of * the argument. * @throws ClassCastException if the argument is not a CharsetMatch. */ public int compareTo (@NotNull CharsetMatch other) { if (this.fConfidence > other.fConfidence) { return 1; } else if (this.fConfidence < other.fConfidence) { return -1; } return 0; } /* * Constructor. Implementation internal */ CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf) { fConfidence = conf; // The references to the original application input data must be copied out // of the charset recognizer to here, in case the application resets the // recognizer before using this CharsetMatch. if (det.fInputStream == null) { // We only want the existing input byte data if it came straight from the user, // not if is just the head of a stream. fRawInput = det.fRawInput; fRawLength = det.fRawLength; } fInputStream = det.fInputStream; fCharsetName = rec.getName(); fLang = rec.getLanguage(); } /* * Constructor. Implementation internal */ CharsetMatch(CharsetDetector det, int conf, String csName, String lang) { fConfidence = conf; // The references to the original application input data must be copied out of the charset recognizer to here, // in case the application resets the recognizer before using this CharsetMatch. if (det.fInputStream == null) { // We only want the existing input byte data if it came straight from the user, // not if is just the head of a stream. fRawInput = det.fRawInput; fRawLength = det.fRawLength; } fInputStream = det.fInputStream; fCharsetName = csName; fLang = lang; } private final int fConfidence; private byte[] fRawInput; // Original, untouched input bytes. If user gave us a byte array, this is it. private int fRawLength; // Length of data in fRawInput array. private final InputStream fInputStream; // User's input stream, or null if the user gave us a byte array. private final String fCharsetName; // The name of the charset this CharsetMatch represents. Filled in by the recognizer. private final String fLang; // The language, if one was determined by the recognizer during the detect operation. } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetRecog_2022.java ================================================ /* ******************************************************************************* * Copyright (C) 2005 - 2012, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.text; /** * class CharsetRecog_2022 part of the ICU charset detection implementation. * This is a superclass for the individual detectors for each of the detectable members of the ISO 2022 family of encodings. * The separate classes are nested within this class. */ abstract class CharsetRecog_2022 extends CharsetRecognizer { /** * Matching function shared among the 2022 detectors JP, CN and KR Counts up the number of legal an * unrecognized escape sequences in the sample of text, and computes a score based on the total number & * the proportion that fit the encoding. * * @param text the byte buffer containing text to analyse * @param textLen the size of the text in the byte. * @param escapeSequences the byte escape sequences to test for. * @return match quality, in the range of 0-100. */ int match(byte[] text, int textLen, byte[][] escapeSequences) { int hits = 0; int misses = 0; int shifts = 0; int quality; scanInput: for (int i = 0; i < textLen; i++) { if (text[i] == 0x1b) { checkEscapes: for (byte[] seq : escapeSequences) { if ((textLen - i) < seq.length) { continue checkEscapes; } for (int j = 1; j < seq.length; j++) { if (seq[j] != text[i + j]) { continue checkEscapes; } } hits++; i += seq.length - 1; continue scanInput; } misses++; } if (text[i] == 0x0e || text[i] == 0x0f) { // Shift in/out shifts++; } } if (hits == 0) { return 0; } // Initial quality is based on relative proportion of recognized vs. // unrecognized escape sequences. // All good: quality = 100; // half or less good: quality = 0; // linear inbetween. quality = (100 * hits - 100 * misses) / (hits + misses); // Back off quality if there were too few escape sequences seen. // Include shifts in this computation, so that KR does not get penalized // for having only a single Escape sequence, but many shifts. if (hits + shifts < 5) { quality -= (5 - (hits + shifts)) * 10; } if (quality < 0) { quality = 0; } return quality; } static class CharsetRecog_2022JP extends CharsetRecog_2022 { private final byte[][] escapeSequences = { {0x1b, 0x24, 0x28, 0x43}, // KS X 1001:1992 {0x1b, 0x24, 0x28, 0x44}, // JIS X 212-1990 {0x1b, 0x24, 0x40}, // JIS C 6226-1978 {0x1b, 0x24, 0x41}, // GB 2312-80 {0x1b, 0x24, 0x42}, // JIS X 208-1983 {0x1b, 0x26, 0x40}, // JIS X 208 1990, 1997 {0x1b, 0x28, 0x42}, // ASCII {0x1b, 0x28, 0x48}, // JIS-Roman {0x1b, 0x28, 0x49}, // Half-width katakana {0x1b, 0x28, 0x4a}, // JIS-Roman {0x1b, 0x2e, 0x41}, // ISO 8859-1 {0x1b, 0x2e, 0x46} // ISO 8859-7 }; String getName() { return "ISO-2022-JP"; } CharsetMatch match(CharsetDetector det) { int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_2022KR extends CharsetRecog_2022 { private final byte[][] escapeSequences = { {0x1b, 0x24, 0x29, 0x43} }; String getName() { return "ISO-2022-KR"; } CharsetMatch match(CharsetDetector det) { int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_2022CN extends CharsetRecog_2022 { private final byte[][] escapeSequences = { {0x1b, 0x24, 0x29, 0x41}, // GB 2312-80 {0x1b, 0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1 {0x1b, 0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2 {0x1b, 0x24, 0x29, 0x45}, // ISO-IR-165 {0x1b, 0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3 {0x1b, 0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4 {0x1b, 0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5 {0x1b, 0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6 {0x1b, 0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7 {0x1b, 0x4e}, // SS2 {0x1b, 0x4f}, // SS3 }; String getName() { return "ISO-2022-CN"; } CharsetMatch match(CharsetDetector det) { int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetRecog_UTF8.java ================================================ /* ******************************************************************************* * Copyright (C) 2005 - 2012, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.text; /** * Charset recognizer for UTF-8 */ class CharsetRecog_UTF8 extends CharsetRecognizer { String getName() { return "UTF-8"; } /* (non-Javadoc) * @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector) */ CharsetMatch match(CharsetDetector det) { boolean hasBOM = false; int numValid = 0; int numInvalid = 0; byte[] input = det.fRawInput; int trailBytes; if (det.fRawLength >= 3 && (input[0] & 0xFF) == 0xef && (input[1] & 0xFF) == 0xbb && (input[2] & 0xFF) == 0xbf) { hasBOM = true; } // Scan for multibyte sequences for (int i = 0; i < det.fRawLength; i++) { int b = input[i]; if ((b & 0x80) == 0) { continue; // ASCII } // High bit on char found. Figure out how long the sequence should be if ((b & 0x0e0) == 0x0c0) { trailBytes = 1; } else if ((b & 0x0f0) == 0x0e0) { trailBytes = 2; } else if ((b & 0x0f8) == 0xf0) { trailBytes = 3; } else { numInvalid++; if (numInvalid > 5) { break; } trailBytes = 0; } // Verify that we've got the right number of trail bytes in the sequence for (; ; ) { i++; if (i >= det.fRawLength) { break; } b = input[i]; if ((b & 0xc0) != 0x080) { numInvalid++; break; } if (--trailBytes == 0) { numValid++; break; } } } // Cook up some sort of confidence score, based on presense of a BOM and the existence of valid // and/or invalid multibyte sequences. int confidence = 0; if (hasBOM && numInvalid == 0) { confidence = 100; } else if (hasBOM && numValid > numInvalid * 10) { confidence = 80; } else if (numValid > 3 && numInvalid == 0) { confidence = 100; } else if (numValid > 0 && numInvalid == 0) { confidence = 80; } else if (numValid == 0 && numInvalid == 0) { // Plain ASCII. confidence = 10; } else if (numValid > numInvalid * 10) { // Probably corrupt utf-8 data. Valid sequences aren't likely by chance. confidence = 25; } return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetRecog_Unicode.java ================================================ /* ******************************************************************************* * Copyright (C) 1996-2012, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* * */ package com.ibm.icu.text; /** * This class matches UTF-16 and UTF-32, both big- and little-endian. The BOM will be used if it is present. */ abstract class CharsetRecog_Unicode extends CharsetRecognizer { @Override abstract String getName(); @Override abstract CharsetMatch match(CharsetDetector det); static class CharsetRecog_UTF_16_BE extends CharsetRecog_Unicode { String getName() { return "UTF-16BE"; } CharsetMatch match(CharsetDetector det) { byte[] input = det.fRawInput; if (input.length >= 2 && ((input[0] & 0xFF) == 0xFE && (input[1] & 0xFF) == 0xFF)) { int confidence = 100; return new CharsetMatch(det, this, confidence); } // TODO: Do some statistics to check for unsigned UTF-16BE return null; } } static class CharsetRecog_UTF_16_LE extends CharsetRecog_Unicode { String getName() { return "UTF-16LE"; } CharsetMatch match(CharsetDetector det) { byte[] input = det.fRawInput; if (input.length >= 2 && ((input[0] & 0xFF) == 0xFF && (input[1] & 0xFF) == 0xFE)) { // An LE BOM is present. if (input.length >= 4 && input[2] == 0x00 && input[3] == 0x00) { // It is probably UTF-32 LE, not UTF-16 return null; } int confidence = 100; return new CharsetMatch(det, this, confidence); } // TODO: Do some statistics to check for unsigned UTF-16LE return null; } } static abstract class CharsetRecog_UTF_32 extends CharsetRecog_Unicode { abstract int getChar(byte[] input, int index); abstract String getName(); CharsetMatch match(CharsetDetector det) { byte[] input = det.fRawInput; int limit = (det.fRawLength / 4) * 4; int numValid = 0; int numInvalid = 0; boolean hasBOM = false; int confidence = 0; if (limit == 0) { return null; } if (getChar(input, 0) == 0x0000FEFF) { hasBOM = true; } for (int i = 0; i < limit; i += 4) { int ch = getChar(input, i); if (ch < 0 || ch >= 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) { numInvalid += 1; } else { numValid += 1; } } // Cook up some sort of confidence score, based on presence of a BOM // and the existence of valid and/or invalid multibyte sequences. if (hasBOM && numInvalid == 0) { confidence = 100; } else if (hasBOM && numValid > numInvalid * 10) { confidence = 80; } else if (numValid > 3 && numInvalid == 0) { confidence = 100; } else if (numValid > 0 && numInvalid == 0) { confidence = 80; } else if (numValid > numInvalid * 10) { // Probably corrupt UTF-32BE data. Valid sequences aren't likely by chance. confidence = 25; } return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_UTF_32_BE extends CharsetRecog_UTF_32 { int getChar(byte[] input, int index) { return (input[index + 0] & 0xFF) << 24 | (input[index + 1] & 0xFF) << 16 | (input[index + 2] & 0xFF) << 8 | (input[index + 3] & 0xFF); } String getName() { return "UTF-32BE"; } } static class CharsetRecog_UTF_32_LE extends CharsetRecog_UTF_32 { int getChar(byte[] input, int index) { return (input[index + 3] & 0xFF) << 24 | (input[index + 2] & 0xFF) << 16 | (input[index + 1] & 0xFF) << 8 | (input[index + 0] & 0xFF); } String getName() { return "UTF-32LE"; } } } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetRecog_mbcs.java ================================================ /* **************************************************************************** * Copyright (C) 2005-2012, International Business Machines Corporation and * * others. All Rights Reserved. * **************************************************************************** * */ package com.ibm.icu.text; import java.util.Arrays; /** * CharsetRecognizer implemenation for Asian - double or multi-byte - charsets. * Match is determined mostly by the input data adhering to the * encoding scheme for the charset, and, optionally, * frequency-of-occurence of characters. *

* Instances of this class are singletons, one per encoding * being recognized. They are created in the main * CharsetDetector class and kept in the global list of available * encodings to be checked. The specific encoding being recognized * is determined by subclass. */ abstract class CharsetRecog_mbcs extends CharsetRecognizer { /** * Get the IANA name of this charset. * @return the charset name. */ abstract String getName() ; /** * Test the match of this charset with the input text data * which is obtained via the CharsetDetector object. * * @param det The CharsetDetector, which contains the input text * to be checked for being in this charset. * @return Two values packed into one int (Damn java, anyhow) *
* bits 0-7: the match confidence, ranging from 0-100 *
* bits 8-15: The match reason, an enum-like value. */ int match(CharsetDetector det, int [] commonChars) { @SuppressWarnings("unused") int singleByteCharCount = 0; //TODO Do we really need this? int doubleByteCharCount = 0; int commonCharCount = 0; int badCharCount = 0; int totalCharCount = 0; int confidence = 0; iteratedChar iter = new iteratedChar(); detectBlock: { for (iter.reset(); nextChar(iter, det);) { totalCharCount++; if (iter.error) { badCharCount++; } else { long cv = iter.charValue & 0xFFFFFFFFL; if (cv <= 0xff) { singleByteCharCount++; } else { doubleByteCharCount++; if (commonChars != null) { // NOTE: This assumes that there are no 4-byte common chars. if (Arrays.binarySearch(commonChars, (int) cv) >= 0) { commonCharCount++; } } } } if (badCharCount >= 2 && badCharCount*5 >= doubleByteCharCount) { // Bail out early if the byte data is not matching the encoding scheme. break detectBlock; } } if (doubleByteCharCount <= 10 && badCharCount== 0) { // Not many multi-byte chars. if (doubleByteCharCount == 0 && totalCharCount < 10) { // There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes. // We don't have enough data to have any confidence. // Statistical analysis of single byte non-ASCII charcters would probably help here. confidence = 0; } else { // ASCII or ISO file? It's probably not our encoding, // but is not incompatible with our encoding, so don't give it a zero. confidence = 10; } break detectBlock; } // // No match if there are too many characters that don't fit the encoding scheme. // (should we have zero tolerance for these?) // if (doubleByteCharCount < 20*badCharCount) { confidence = 0; break detectBlock; } if (commonChars == null) { // We have no statistics on frequently occuring characters. // Assess confidence purely on having a reasonable number of // multi-byte characters (the more the better confidence = 30 + doubleByteCharCount - 20*badCharCount; if (confidence > 100) { confidence = 100; } }else { // // Frequency of occurence statistics exist. // double maxVal = Math.log((float)doubleByteCharCount / 4); double scaleFactor = 90.0 / maxVal; confidence = (int)(Math.log(commonCharCount+1) * scaleFactor + 10); confidence = Math.min(confidence, 100); } } // end of detectBlock: return confidence; } // "Character" iterated character class. // Recognizers for specific mbcs encodings make their "characters" available // by providing a nextChar() function that fills in an instance of iteratedChar // with the next char from the input. // The returned characters are not converted to Unicode, but remain as the raw // bytes (concatenated into an int) from the codepage data. // // For Asian charsets, use the raw input rather than the input that has been // stripped of markup. Detection only considers multi-byte chars, effectively // stripping markup anyway, and double byte chars do occur in markup too. // static class iteratedChar { int charValue = 0; // 1-4 bytes from the raw input data int index = 0; int nextIndex = 0; boolean error = false; boolean done = false; void reset() { charValue = 0; index = -1; nextIndex = 0; error = false; done = false; } int nextByte(CharsetDetector det) { if (nextIndex >= det.fRawLength) { done = true; return -1; } return (int)det.fRawInput[nextIndex++] & 0x00ff; } } /** * Get the next character (however many bytes it is) from the input data * Subclasses for specific charset encodings must implement this function * to get characters according to the rules of their encoding scheme. * This function is not a method of class iteratedChar only because * that would require a lot of extra derived classes, which is awkward. * @param it The iteratedChar "struct" into which the returned char is placed. * @param det The charset detector, which is needed to get at the input byte data * being iterated over. * @return True if a character was returned, false at end of input. */ abstract boolean nextChar(iteratedChar it, CharsetDetector det); /** * Shift-JIS charset recognizer. * */ static class CharsetRecog_sjis extends CharsetRecog_mbcs { static int [] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0, 0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5, 0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc, 0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341, 0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389, 0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa}; boolean nextChar(iteratedChar it, CharsetDetector det) { it.index = it.nextIndex; it.error = false; int firstByte; firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { return false; } if (firstByte <= 0x7f || (firstByte>0xa0 && firstByte<=0xdf)) { return true; } int secondByte = it.nextByte(det); if (secondByte < 0) { return false; } it.charValue = (firstByte << 8) | secondByte; if (! ((secondByte>=0x40 && secondByte<=0x7f) || (secondByte>=0x80 && secondByte<=0xff))) { // Illegal second byte value. it.error = true; } return true; } CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } String getName() { return "Shift_JIS"; } public String getLanguage() { return "ja"; } } /** * Big5 charset recognizer. * */ static class CharsetRecog_big5 extends CharsetRecog_mbcs { static int [] commonChars = // TODO: This set of data comes from the character frequency- // of-occurence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446, 0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3, 0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548, 0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8, 0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da, 0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3, 0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59, 0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c, 0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44, 0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f}; boolean nextChar(iteratedChar it, CharsetDetector det) { it.index = it.nextIndex; it.error = false; int firstByte; firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { return false; } if (firstByte <= 0x7f || firstByte==0xff) { // single byte character. return true; } int secondByte = it.nextByte(det); if (secondByte < 0) { return false; } it.charValue = (it.charValue << 8) | secondByte; if (secondByte < 0x40 || secondByte ==0x7f || secondByte == 0xff) { it.error = true; } return true; } CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } String getName() { return "Big5"; } public String getLanguage() { return "zh"; } } /** * EUC charset recognizers. One abstract class that provides the common function * for getting the next character according to the EUC encoding scheme, * and nested derived classes for EUC_KR, EUC_JP, EUC_CN. * */ abstract static class CharsetRecog_euc extends CharsetRecog_mbcs { /* * (non-Javadoc) * Get the next character value for EUC based encodings. * Character "value" is simply the raw bytes that make up the character * packed into an int. */ boolean nextChar(iteratedChar it, CharsetDetector det) { it.index = it.nextIndex; it.error = false; int firstByte; int secondByte; int thirdByte; buildChar: { firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { // Ran off the end of the input data it.done = true; break buildChar; } if (firstByte <= 0x8d) { // single byte char break buildChar; } secondByte = it.nextByte(det); it.charValue = (it.charValue << 8) | secondByte; if (firstByte >= 0xA1 && firstByte <= 0xfe) { // Two byte Char if (secondByte < 0xa1) { it.error = true; } break buildChar; } if (firstByte == 0x8e) { // Code Set 2. // In EUC-JP, total char size is 2 bytes, only one byte of actual char value. // In EUC-TW, total char size is 4 bytes, three bytes contribute to char value. // We don't know which we've got. // Treat it like EUC-JP. If the data really was EUC-TW, the following two // bytes will look like a well formed 2 byte char. if (secondByte < 0xa1) { it.error = true; } break buildChar; } if (firstByte == 0x8f) { // Code set 3. // Three byte total char size, two bytes of actual char value. thirdByte = it.nextByte(det); it.charValue = (it.charValue << 8) | thirdByte; if (thirdByte < 0xa1) { it.error = true; } } } return !it.done; } /** * The charset recognize for EUC-JP. A singleton instance of this class * is created and kept by the public CharsetDetector class */ static class CharsetRecog_euc_jp extends CharsetRecog_euc { static int [] commonChars = // TODO: This set of data comes from the character frequency- // of-occurrence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2, 0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3, 0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4, 0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de, 0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef, 0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af, 0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7, 0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1, 0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee, 0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1}; String getName() { return "EUC-JP"; } CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } public String getLanguage() { return "ja"; } } /** * The charset recognize for EUC-KR. A singleton instance of this class * is created and kept by the public CharsetDetector class */ static class CharsetRecog_euc_kr extends CharsetRecog_euc { static int [] commonChars = // TODO: This set of data comes from the character frequency- // of-occurrence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc, 0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9, 0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce, 0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce, 0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba, 0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee, 0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7, 0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6, 0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6, 0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad}; String getName() { return "EUC-KR"; } CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } public String getLanguage() { return "ko"; } } } /** * * GB-18030 recognizer. Uses simplified Chinese statistics. * */ static class CharsetRecog_gb_18030 extends CharsetRecog_mbcs { /* * (non-Javadoc) * Get the next character value for EUC based encodings. * Character "value" is simply the raw bytes that make up the character * packed into an int. */ boolean nextChar(iteratedChar it, CharsetDetector det) { it.index = it.nextIndex; it.error = false; int firstByte; int secondByte; int thirdByte; int fourthByte; buildChar: { firstByte = it.charValue = it.nextByte(det); if (firstByte < 0) { // Ran off the end of the input data it.done = true; break buildChar; } if (firstByte <= 0x80) { // single byte char break buildChar; } secondByte = it.nextByte(det); it.charValue = (it.charValue << 8) | secondByte; if (firstByte >= 0x81 && firstByte <= 0xFE) { // Two byte Char if ((secondByte >= 0x40 && secondByte <= 0x7E) || (secondByte >=80 && secondByte <=0xFE)) { break buildChar; } // Four byte char if (secondByte >= 0x30 && secondByte <= 0x39) { thirdByte = it.nextByte(det); if (thirdByte >= 0x81 && thirdByte <= 0xFE) { fourthByte = it.nextByte(det); if (fourthByte >= 0x30 && fourthByte <= 0x39) { it.charValue = (it.charValue << 16) | (thirdByte << 8) | fourthByte; break buildChar; } } } it.error = true; break buildChar; } } return !it.done; } static int [] commonChars = // TODO: This set of data comes from the character frequency- // of-occurrence analysis tool. The data needs to be moved // into a resource and loaded from there. {0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac, 0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4, 0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4, 0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6, 0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6, 0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7, 0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7, 0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5, 0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2, 0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0}; String getName() { return "GB18030"; } CharsetMatch match(CharsetDetector det) { int confidence = match(det, commonChars); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } public String getLanguage() { return "zh"; } } } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetRecog_sbcs.java ================================================ /* **************************************************************************** * Copyright (C) 2005-2013, International Business Machines Corporation and * * others. All Rights Reserved. * ************************************************************************** * * * Modified by Oleg Trifonov * added cp866 support */ package com.ibm.icu.text; /** * This class recognizes single-byte encodings. Because the encoding scheme is so * simple, language statistics are used to do the matching. */ abstract class CharsetRecog_sbcs extends CharsetRecognizer { /* (non-Javadoc) * @see com.ibm.icu.text.CharsetRecognizer#getName() */ abstract String getName(); static class NGramParser { // private static final int N_GRAM_SIZE = 3; private static final int N_GRAM_MASK = 0xFFFFFF; protected int byteIndex = 0; private int ngram; private final int[] ngramList; protected byte[] byteMap; private int ngramCount; private int hitCount; protected byte spaceChar; public NGramParser(int[] theNgramList, byte[] theByteMap) { ngramList = theNgramList; byteMap = theByteMap; ngram = 0; ngramCount = hitCount = 0; } /* * Binary search for value in table, which must have exactly 64 entries. */ private static int search(int[] table, int value) { int index = 0; if (table[index + 32] <= value) { index += 32; } if (table[index + 16] <= value) { index += 16; } if (table[index + 8] <= value) { index += 8; } if (table[index + 4] <= value) { index += 4; } if (table[index + 2] <= value) { index += 2; } if (table[index + 1] <= value) { index += 1; } if (table[index] > value) { index -= 1; } if (index < 0 || table[index] != value) { return -1; } return index; } private void lookup(int thisNgram) { ngramCount += 1; if (search(ngramList, thisNgram) >= 0) { hitCount += 1; } } protected void addByte(int b) { ngram = ((ngram << 8) + (b & 0xFF)) & N_GRAM_MASK; lookup(ngram); } private int nextByte(CharsetDetector det) { if (byteIndex >= det.fInputLen) { return -1; } return det.fInputBytes[byteIndex++] & 0xFF; } protected void parseCharacters(CharsetDetector det) { int b; boolean ignoreSpace = false; while ((b = nextByte(det)) >= 0) { byte mb = byteMap[b]; // TODO: 0x20 might not be a space in all character sets... if (mb != 0) { if (!(mb == spaceChar && ignoreSpace)) { addByte(mb); } ignoreSpace = (mb == spaceChar); } } } public int parse(CharsetDetector det) { return parse (det, (byte)0x20); } public int parse(CharsetDetector det, byte spaceCh) { this.spaceChar = spaceCh; parseCharacters(det); // TODO: Is this OK? The buffer could have ended in the middle of a word... addByte(spaceChar); double rawPercent = (double) hitCount / (double) ngramCount; // if (rawPercent <= 2.0) { // return 0; // } // TODO - This is a bit of a hack to take care of a case // were we were getting a confidence of 135... if (rawPercent > 0.33) { return 98; } return (int) (rawPercent * 300.0); } } static class NGramParser_IBM420 extends NGramParser { private byte alef = 0x00; protected static byte[] unshapeMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x42, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x47, (byte) 0x49, (byte) 0x4A, (byte) 0x4B, (byte) 0x4C, (byte) 0x4D, (byte) 0x4E, (byte) 0x4F, /* 5- */ (byte) 0x50, (byte) 0x49, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x56, (byte) 0x58, (byte) 0x58, (byte) 0x5A, (byte) 0x5B, (byte) 0x5C, (byte) 0x5D, (byte) 0x5E, (byte) 0x5F, /* 6- */ (byte) 0x60, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x63, (byte) 0x65, (byte) 0x65, (byte) 0x67, (byte) 0x67, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, /* 7- */ (byte) 0x69, (byte) 0x71, (byte) 0x71, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x77, (byte) 0x79, (byte) 0x7A, (byte) 0x7B, (byte) 0x7C, (byte) 0x7D, (byte) 0x7E, (byte) 0x7F, /* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x80, (byte) 0x8B, (byte) 0x8B, (byte) 0x8D, (byte) 0x8D, (byte) 0x8F, /* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9A, (byte) 0x9E, (byte) 0x9E, /* A- */ (byte) 0x9E, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x9E, (byte) 0xAB, (byte) 0xAB, (byte) 0xAD, (byte) 0xAD, (byte) 0xAF, /* B- */ (byte) 0xAF, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xB1, (byte) 0xBB, (byte) 0xBB, (byte) 0xBD, (byte) 0xBD, (byte) 0xBF, /* C- */ (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xBF, (byte) 0xCC, (byte) 0xBF, (byte) 0xCE, (byte) 0xCF, /* D- */ (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDA, (byte) 0xDC, (byte) 0xDC, (byte) 0xDC, (byte) 0xDF, /* E- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* F- */ (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; public NGramParser_IBM420(int[] theNgramList, byte[] theByteMap) { super(theNgramList, theByteMap); } private byte isLamAlef(byte b) { if(b == (byte)0xb2 || b == (byte)0xb3){ return (byte)0x47; }else if(b == (byte)0xb4 || b == (byte)0xb5){ return (byte)0x49; }else if(b == (byte)0xb8 || b == (byte)0xb9){ return (byte)0x56; }else return (byte)0x00; } /* * Arabic shaping needs to be done manually. Cannot call ArabicShaping class * because CharsetDetector is dealing with bytes not Unicode code points. We could * convert the bytes to Unicode code points but that would leave us dependent * on CharsetICU which we try to avoid. IBM420 converter amongst different versions * of JDK can produce different results and therefore is also avoided. */ private int nextByte(CharsetDetector det) { if (byteIndex >= det.fInputLen || det.fInputBytes[byteIndex] == 0) { return -1; } int next; alef = isLamAlef(det.fInputBytes[byteIndex]); if(alef != (byte)0x00) next = 0xB1 & 0xFF; else next = unshapeMap[det.fInputBytes[byteIndex]& 0xFF] & 0xFF; byteIndex++; return next; } protected void parseCharacters(CharsetDetector det) { int b; boolean ignoreSpace = false; while ((b = nextByte(det)) >= 0) { byte mb = byteMap[b]; // TODO: 0x20 might not be a space in all character sets... if (mb != 0) { if (!(mb == spaceChar && ignoreSpace)) { addByte(mb); } ignoreSpace = (mb == spaceChar); } if(alef != (byte)0x00){ mb = byteMap[alef & 0xFF]; // TODO: 0x20 might not be a space in all character sets... if (mb != 0) { if (!(mb == spaceChar && ignoreSpace)) { addByte(mb); } ignoreSpace = (mb == spaceChar); } } } } } int match(CharsetDetector det, int[] ngrams, byte[] byteMap) { return match(det, ngrams, byteMap, (byte)0x20); } int match(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar) { NGramParser parser = new NGramParser(ngrams, byteMap); return parser.parse(det, spaceChar); } int matchIBM420(CharsetDetector det, int[] ngrams, byte[] byteMap, byte spaceChar){ NGramParser_IBM420 parser = new NGramParser_IBM420(ngrams, byteMap); return parser.parse(det, spaceChar); } static class NGramsPlusLang { int[] fNGrams; String fLang; NGramsPlusLang(String la, int [] ng) { fLang = la; fNGrams = ng; } } static class CharsetRecog_8859_1 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; private static final NGramsPlusLang[] ngrams_8859_1 = new NGramsPlusLang[] { new NGramsPlusLang( "da", new int[] { // ' af' 0x206166, 0x206174, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207369, 0x207374, 0x207469, 0x207669, 0x616620, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646572, 0x646574, 0x652073, 0x656420, 0x656465, 0x656E20, 0x656E64, 0x657220, 0x657265, 0x657320, 0x657420, 0x666F72, 0x676520, 0x67656E, 0x676572, 0x696765, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6572, 0x6C6967, 0x6C6C65, 0x6D6564, 0x6E6465, 0x6E6520, 0x6E6720, 0x6E6765, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722064, 0x722065, 0x722073, 0x726520, 0x737465, 0x742073, 0x746520, 0x746572, 0x74696C, 0x766572, }), new NGramsPlusLang( "de", new int[] { // ' an' 0x20616E, 0x206175, 0x206265, 0x206461, 0x206465, 0x206469, 0x206569, 0x206765, 0x206861, 0x20696E, 0x206D69, 0x207363, 0x207365, 0x20756E, 0x207665, 0x20766F, 0x207765, 0x207A75, 0x626572, 0x636820, 0x636865, 0x636874, 0x646173, 0x64656E, 0x646572, 0x646965, 0x652064, 0x652073, 0x65696E, 0x656974, 0x656E20, 0x657220, 0x657320, 0x67656E, 0x68656E, 0x687420, 0x696368, 0x696520, 0x696E20, 0x696E65, 0x697420, 0x6C6963, 0x6C6C65, 0x6E2061, 0x6E2064, 0x6E2073, 0x6E6420, 0x6E6465, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E7465, 0x722064, 0x726465, 0x726569, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x756E64, 0x756E67, 0x766572, }), new NGramsPlusLang( "en", new int[] { 0x206120, 0x20616E, 0x206265, 0x20636F, 0x20666F, 0x206861, 0x206865, 0x20696E, 0x206D61, 0x206F66, 0x207072, 0x207265, 0x207361, 0x207374, 0x207468, 0x20746F, 0x207768, 0x616964, 0x616C20, 0x616E20, 0x616E64, 0x617320, 0x617420, 0x617465, 0x617469, 0x642061, 0x642074, 0x652061, 0x652073, 0x652074, 0x656420, 0x656E74, 0x657220, 0x657320, 0x666F72, 0x686174, 0x686520, 0x686572, 0x696420, 0x696E20, 0x696E67, 0x696F6E, 0x697320, 0x6E2061, 0x6E2074, 0x6E6420, 0x6E6720, 0x6E7420, 0x6F6620, 0x6F6E20, 0x6F7220, 0x726520, 0x727320, 0x732061, 0x732074, 0x736169, 0x737420, 0x742074, 0x746572, 0x746861, 0x746865, 0x74696F, 0x746F20, 0x747320, }), new NGramsPlusLang( "es", new int[] { 0x206120, 0x206361, 0x20636F, 0x206465, 0x20656C, 0x20656E, 0x206573, 0x20696E, 0x206C61, 0x206C6F, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, 0x20756E, 0x207920, 0x612063, 0x612064, 0x612065, 0x61206C, 0x612070, 0x616369, 0x61646F, 0x616C20, 0x617220, 0x617320, 0x6369F3, 0x636F6E, 0x646520, 0x64656C, 0x646F20, 0x652064, 0x652065, 0x65206C, 0x656C20, 0x656E20, 0x656E74, 0x657320, 0x657374, 0x69656E, 0x69F36E, 0x6C6120, 0x6C6F73, 0x6E2065, 0x6E7465, 0x6F2064, 0x6F2065, 0x6F6E20, 0x6F7220, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732064, 0x732065, 0x732070, 0x736520, 0x746520, 0x746F20, 0x756520, 0xF36E20, }), new NGramsPlusLang( "fr", new int[] { 0x206175, 0x20636F, 0x206461, 0x206465, 0x206475, 0x20656E, 0x206574, 0x206C61, 0x206C65, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207365, 0x20736F, 0x20756E, 0x20E020, 0x616E74, 0x617469, 0x636520, 0x636F6E, 0x646520, 0x646573, 0x647520, 0x652061, 0x652063, 0x652064, 0x652065, 0x65206C, 0x652070, 0x652073, 0x656E20, 0x656E74, 0x657220, 0x657320, 0x657420, 0x657572, 0x696F6E, 0x697320, 0x697420, 0x6C6120, 0x6C6520, 0x6C6573, 0x6D656E, 0x6E2064, 0x6E6520, 0x6E7320, 0x6E7420, 0x6F6E20, 0x6F6E74, 0x6F7572, 0x717565, 0x72206C, 0x726520, 0x732061, 0x732064, 0x732065, 0x73206C, 0x732070, 0x742064, 0x746520, 0x74696F, 0x756520, 0x757220, }), new NGramsPlusLang( "it", new int[] { 0x20616C, 0x206368, 0x20636F, 0x206465, 0x206469, 0x206520, 0x20696C, 0x20696E, 0x206C61, 0x207065, 0x207072, 0x20756E, 0x612063, 0x612064, 0x612070, 0x612073, 0x61746F, 0x636865, 0x636F6E, 0x64656C, 0x646920, 0x652061, 0x652063, 0x652064, 0x652069, 0x65206C, 0x652070, 0x652073, 0x656C20, 0x656C6C, 0x656E74, 0x657220, 0x686520, 0x692061, 0x692063, 0x692064, 0x692073, 0x696120, 0x696C20, 0x696E20, 0x696F6E, 0x6C6120, 0x6C6520, 0x6C6920, 0x6C6C61, 0x6E6520, 0x6E6920, 0x6E6F20, 0x6E7465, 0x6F2061, 0x6F2064, 0x6F2069, 0x6F2073, 0x6F6E20, 0x6F6E65, 0x706572, 0x726120, 0x726520, 0x736920, 0x746120, 0x746520, 0x746920, 0x746F20, 0x7A696F, }), new NGramsPlusLang( "nl", new int[] { 0x20616C, 0x206265, 0x206461, 0x206465, 0x206469, 0x206565, 0x20656E, 0x206765, 0x206865, 0x20696E, 0x206D61, 0x206D65, 0x206F70, 0x207465, 0x207661, 0x207665, 0x20766F, 0x207765, 0x207A69, 0x61616E, 0x616172, 0x616E20, 0x616E64, 0x617220, 0x617420, 0x636874, 0x646520, 0x64656E, 0x646572, 0x652062, 0x652076, 0x65656E, 0x656572, 0x656E20, 0x657220, 0x657273, 0x657420, 0x67656E, 0x686574, 0x696520, 0x696E20, 0x696E67, 0x697320, 0x6E2062, 0x6E2064, 0x6E2065, 0x6E2068, 0x6E206F, 0x6E2076, 0x6E6465, 0x6E6720, 0x6F6E64, 0x6F6F72, 0x6F7020, 0x6F7220, 0x736368, 0x737465, 0x742064, 0x746520, 0x74656E, 0x746572, 0x76616E, 0x766572, 0x766F6F, }), new NGramsPlusLang( "no", new int[] { 0x206174, 0x206176, 0x206465, 0x20656E, 0x206572, 0x20666F, 0x206861, 0x206920, 0x206D65, 0x206F67, 0x2070E5, 0x207365, 0x20736B, 0x20736F, 0x207374, 0x207469, 0x207669, 0x20E520, 0x616E64, 0x617220, 0x617420, 0x646520, 0x64656E, 0x646574, 0x652073, 0x656420, 0x656E20, 0x656E65, 0x657220, 0x657265, 0x657420, 0x657474, 0x666F72, 0x67656E, 0x696B6B, 0x696C20, 0x696E67, 0x6B6520, 0x6B6B65, 0x6C6520, 0x6C6C65, 0x6D6564, 0x6D656E, 0x6E2073, 0x6E6520, 0x6E6720, 0x6E6765, 0x6E6E65, 0x6F6720, 0x6F6D20, 0x6F7220, 0x70E520, 0x722073, 0x726520, 0x736F6D, 0x737465, 0x742073, 0x746520, 0x74656E, 0x746572, 0x74696C, 0x747420, 0x747465, 0x766572, }), new NGramsPlusLang( "pt", new int[] { 0x206120, 0x20636F, 0x206461, 0x206465, 0x20646F, 0x206520, 0x206573, 0x206D61, 0x206E6F, 0x206F20, 0x207061, 0x20706F, 0x207072, 0x207175, 0x207265, 0x207365, 0x20756D, 0x612061, 0x612063, 0x612064, 0x612070, 0x616465, 0x61646F, 0x616C20, 0x617220, 0x617261, 0x617320, 0x636F6D, 0x636F6E, 0x646120, 0x646520, 0x646F20, 0x646F73, 0x652061, 0x652064, 0x656D20, 0x656E74, 0x657320, 0x657374, 0x696120, 0x696361, 0x6D656E, 0x6E7465, 0x6E746F, 0x6F2061, 0x6F2063, 0x6F2064, 0x6F2065, 0x6F2070, 0x6F7320, 0x706172, 0x717565, 0x726120, 0x726573, 0x732061, 0x732064, 0x732065, 0x732070, 0x737461, 0x746520, 0x746F20, 0x756520, 0xE36F20, 0xE7E36F, }), new NGramsPlusLang( "sv", new int[] { 0x206174, 0x206176, 0x206465, 0x20656E, 0x2066F6, 0x206861, 0x206920, 0x20696E, 0x206B6F, 0x206D65, 0x206F63, 0x2070E5, 0x20736B, 0x20736F, 0x207374, 0x207469, 0x207661, 0x207669, 0x20E472, 0x616465, 0x616E20, 0x616E64, 0x617220, 0x617474, 0x636820, 0x646520, 0x64656E, 0x646572, 0x646574, 0x656420, 0x656E20, 0x657220, 0x657420, 0x66F672, 0x67656E, 0x696C6C, 0x696E67, 0x6B6120, 0x6C6C20, 0x6D6564, 0x6E2073, 0x6E6120, 0x6E6465, 0x6E6720, 0x6E6765, 0x6E696E, 0x6F6368, 0x6F6D20, 0x6F6E20, 0x70E520, 0x722061, 0x722073, 0x726120, 0x736B61, 0x736F6D, 0x742073, 0x746120, 0x746520, 0x746572, 0x74696C, 0x747420, 0x766172, 0xE47220, 0xF67220, }), }; public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1252" : "ISO-8859-1"; int bestConfidenceSoFar = -1; String lang = null; for (NGramsPlusLang ngl: ngrams_8859_1) { int confidence = match(det, ngl.fNGrams, byteMap); if (confidence > bestConfidenceSoFar) { bestConfidenceSoFar = confidence; lang = ngl.fLang; } } return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, bestConfidenceSoFar, name, lang); } public String getName() { return "ISO-8859-1"; } } static class CharsetRecog_8859_2 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0x20, (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, (byte) 0x20, (byte) 0xB1, (byte) 0x20, (byte) 0xB3, (byte) 0x20, (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0x20, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0x20, (byte) 0xBE, (byte) 0xBF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, }; private static final NGramsPlusLang[] ngrams_8859_2 = new NGramsPlusLang[] { new NGramsPlusLang( "cs", new int[] { 0x206120, 0x206279, 0x20646F, 0x206A65, 0x206E61, 0x206E65, 0x206F20, 0x206F64, 0x20706F, 0x207072, 0x2070F8, 0x20726F, 0x207365, 0x20736F, 0x207374, 0x20746F, 0x207620, 0x207679, 0x207A61, 0x612070, 0x636520, 0x636820, 0x652070, 0x652073, 0x652076, 0x656D20, 0x656EED, 0x686F20, 0x686F64, 0x697374, 0x6A6520, 0x6B7465, 0x6C6520, 0x6C6920, 0x6E6120, 0x6EE920, 0x6EEC20, 0x6EED20, 0x6F2070, 0x6F646E, 0x6F6A69, 0x6F7374, 0x6F7520, 0x6F7661, 0x706F64, 0x706F6A, 0x70726F, 0x70F865, 0x736520, 0x736F75, 0x737461, 0x737469, 0x73746E, 0x746572, 0x746EED, 0x746F20, 0x752070, 0xBE6520, 0xE16EED, 0xE9686F, 0xED2070, 0xED2073, 0xED6D20, 0xF86564, }), new NGramsPlusLang( "hu", new int[] { 0x206120, 0x20617A, 0x206265, 0x206567, 0x20656C, 0x206665, 0x206861, 0x20686F, 0x206973, 0x206B65, 0x206B69, 0x206BF6, 0x206C65, 0x206D61, 0x206D65, 0x206D69, 0x206E65, 0x20737A, 0x207465, 0x20E973, 0x612061, 0x61206B, 0x61206D, 0x612073, 0x616B20, 0x616E20, 0x617A20, 0x62616E, 0x62656E, 0x656779, 0x656B20, 0x656C20, 0x656C65, 0x656D20, 0x656E20, 0x657265, 0x657420, 0x657465, 0x657474, 0x677920, 0x686F67, 0x696E74, 0x697320, 0x6B2061, 0x6BF67A, 0x6D6567, 0x6D696E, 0x6E2061, 0x6E616B, 0x6E656B, 0x6E656D, 0x6E7420, 0x6F6779, 0x732061, 0x737A65, 0x737A74, 0x737AE1, 0x73E967, 0x742061, 0x747420, 0x74E173, 0x7A6572, 0xE16E20, 0xE97320, }), new NGramsPlusLang( "pl", new int[] { 0x20637A, 0x20646F, 0x206920, 0x206A65, 0x206B6F, 0x206D61, 0x206D69, 0x206E61, 0x206E69, 0x206F64, 0x20706F, 0x207072, 0x207369, 0x207720, 0x207769, 0x207779, 0x207A20, 0x207A61, 0x612070, 0x612077, 0x616E69, 0x636820, 0x637A65, 0x637A79, 0x646F20, 0x647A69, 0x652070, 0x652073, 0x652077, 0x65207A, 0x65676F, 0x656A20, 0x656D20, 0x656E69, 0x676F20, 0x696120, 0x696520, 0x69656A, 0x6B6120, 0x6B6920, 0x6B6965, 0x6D6965, 0x6E6120, 0x6E6961, 0x6E6965, 0x6F2070, 0x6F7761, 0x6F7769, 0x706F6C, 0x707261, 0x70726F, 0x70727A, 0x727A65, 0x727A79, 0x7369EA, 0x736B69, 0x737461, 0x776965, 0x796368, 0x796D20, 0x7A6520, 0x7A6965, 0x7A7920, 0xF37720, }), new NGramsPlusLang( "ro", new int[] { 0x206120, 0x206163, 0x206361, 0x206365, 0x20636F, 0x206375, 0x206465, 0x206469, 0x206C61, 0x206D61, 0x207065, 0x207072, 0x207365, 0x2073E3, 0x20756E, 0x20BA69, 0x20EE6E, 0x612063, 0x612064, 0x617265, 0x617420, 0x617465, 0x617520, 0x636172, 0x636F6E, 0x637520, 0x63E320, 0x646520, 0x652061, 0x652063, 0x652064, 0x652070, 0x652073, 0x656120, 0x656920, 0x656C65, 0x656E74, 0x657374, 0x692061, 0x692063, 0x692064, 0x692070, 0x696520, 0x696920, 0x696E20, 0x6C6120, 0x6C6520, 0x6C6F72, 0x6C7569, 0x6E6520, 0x6E7472, 0x6F7220, 0x70656E, 0x726520, 0x726561, 0x727520, 0x73E320, 0x746520, 0x747275, 0x74E320, 0x756920, 0x756C20, 0xBA6920, 0xEE6E20, }) }; public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1250" : "ISO-8859-2"; int bestConfidenceSoFar = -1; String lang = null; for (NGramsPlusLang ngl: ngrams_8859_2) { int confidence = match(det, ngl.fNGrams, byteMap); if (confidence > bestConfidenceSoFar) { bestConfidenceSoFar = confidence; lang = ngl.fLang; } } return bestConfidenceSoFar <= 0 ? null : new CharsetMatch(det, bestConfidenceSoFar, name, lang); } public String getName() { return "ISO-8859-2"; } } abstract static class CharsetRecog_8859_5 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0x20, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0xFE, (byte) 0xFF, }; public String getName() { return "ISO-8859-5"; } } static class CharsetRecog_8859_5_ru extends CharsetRecog_8859_5 { private static final int[] ngrams = { 0x20D220, 0x20D2DE, 0x20D4DE, 0x20D7D0, 0x20D820, 0x20DAD0, 0x20DADE, 0x20DDD0, 0x20DDD5, 0x20DED1, 0x20DFDE, 0x20DFE0, 0x20E0D0, 0x20E1DE, 0x20E1E2, 0x20E2DE, 0x20E7E2, 0x20EDE2, 0xD0DDD8, 0xD0E2EC, 0xD3DE20, 0xD5DBEC, 0xD5DDD8, 0xD5E1E2, 0xD5E220, 0xD820DF, 0xD8D520, 0xD8D820, 0xD8EF20, 0xDBD5DD, 0xDBD820, 0xDBECDD, 0xDDD020, 0xDDD520, 0xDDD8D5, 0xDDD8EF, 0xDDDE20, 0xDDDED2, 0xDE20D2, 0xDE20DF, 0xDE20E1, 0xDED220, 0xDED2D0, 0xDED3DE, 0xDED920, 0xDEDBEC, 0xDEDC20, 0xDEE1E2, 0xDFDEDB, 0xDFE0D5, 0xDFE0D8, 0xDFE0DE, 0xE0D0D2, 0xE0D5D4, 0xE1E2D0, 0xE1E2D2, 0xE1E2D8, 0xE1EF20, 0xE2D5DB, 0xE2DE20, 0xE2DEE0, 0xE2EC20, 0xE7E2DE, 0xEBE520, }; public String getLanguage() { return "ru"; } public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_8859_6 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, }; public String getName() { return "ISO-8859-6"; } } static class CharsetRecog_8859_6_ar extends CharsetRecog_8859_6 { private static final int[] ngrams = { 0x20C7E4, 0x20C7E6, 0x20C8C7, 0x20D9E4, 0x20E1EA, 0x20E4E4, 0x20E5E6, 0x20E8C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E420, 0xC7E4C3, 0xC7E4C7, 0xC7E4C8, 0xC7E4CA, 0xC7E4CC, 0xC7E4CD, 0xC7E4CF, 0xC7E4D3, 0xC7E4D9, 0xC7E4E2, 0xC7E4E5, 0xC7E4E8, 0xC7E4EA, 0xC7E520, 0xC7E620, 0xC7E6CA, 0xC820C7, 0xC920C7, 0xC920E1, 0xC920E4, 0xC920E5, 0xC920E8, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xD920C7, 0xD9E4E9, 0xE1EA20, 0xE420C7, 0xE4C920, 0xE4E920, 0xE4EA20, 0xE520C7, 0xE5C720, 0xE5C920, 0xE5E620, 0xE620C7, 0xE720C7, 0xE7C720, 0xE8C7E4, 0xE8E620, 0xE920C7, 0xEA20C7, 0xEA20E5, 0xEA20E8, 0xEAC920, 0xEAD120, 0xEAE620, }; public String getLanguage() { return "ar"; } public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_8859_7 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA1, (byte) 0xA2, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xDC, (byte) 0x20, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0x20, (byte) 0xFC, (byte) 0x20, (byte) 0xFD, (byte) 0xFE, (byte) 0xC0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0x20, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x20, }; public String getName() { return "ISO-8859-7"; } } static class CharsetRecog_8859_7_el extends CharsetRecog_8859_7 { private static final int[] ngrams = { 0x20E1ED, 0x20E1F0, 0x20E3E9, 0x20E4E9, 0x20E5F0, 0x20E720, 0x20EAE1, 0x20ECE5, 0x20EDE1, 0x20EF20, 0x20F0E1, 0x20F0EF, 0x20F0F1, 0x20F3F4, 0x20F3F5, 0x20F4E7, 0x20F4EF, 0xDFE120, 0xE120E1, 0xE120F4, 0xE1E920, 0xE1ED20, 0xE1F0FC, 0xE1F220, 0xE3E9E1, 0xE5E920, 0xE5F220, 0xE720F4, 0xE7ED20, 0xE7F220, 0xE920F4, 0xE9E120, 0xE9EADE, 0xE9F220, 0xEAE1E9, 0xEAE1F4, 0xECE520, 0xED20E1, 0xED20E5, 0xED20F0, 0xEDE120, 0xEFF220, 0xEFF520, 0xF0EFF5, 0xF0F1EF, 0xF0FC20, 0xF220E1, 0xF220E5, 0xF220EA, 0xF220F0, 0xF220F4, 0xF3E520, 0xF3E720, 0xF3F4EF, 0xF4E120, 0xF4E1E9, 0xF4E7ED, 0xF4E7F2, 0xF4E9EA, 0xF4EF20, 0xF4EFF5, 0xF4F9ED, 0xF9ED20, 0xFEED20, }; public String getLanguage() { return "el"; } public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1253" : "ISO-8859-7"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, confidence, name, "el"); } } abstract static class CharsetRecog_8859_8 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, }; public String getName() { return "ISO-8859-8"; } } static class CharsetRecog_8859_8_I_he extends CharsetRecog_8859_8 { private static final int[] ngrams = { 0x20E0E5, 0x20E0E7, 0x20E0E9, 0x20E0FA, 0x20E1E9, 0x20E1EE, 0x20E4E0, 0x20E4E5, 0x20E4E9, 0x20E4EE, 0x20E4F2, 0x20E4F9, 0x20E4FA, 0x20ECE0, 0x20ECE4, 0x20EEE0, 0x20F2EC, 0x20F9EC, 0xE0FA20, 0xE420E0, 0xE420E1, 0xE420E4, 0xE420EC, 0xE420EE, 0xE420F9, 0xE4E5E0, 0xE5E020, 0xE5ED20, 0xE5EF20, 0xE5F820, 0xE5FA20, 0xE920E4, 0xE9E420, 0xE9E5FA, 0xE9E9ED, 0xE9ED20, 0xE9EF20, 0xE9F820, 0xE9FA20, 0xEC20E0, 0xEC20E4, 0xECE020, 0xECE420, 0xED20E0, 0xED20E1, 0xED20E4, 0xED20EC, 0xED20EE, 0xED20F9, 0xEEE420, 0xEF20E4, 0xF0E420, 0xF0E920, 0xF0E9ED, 0xF2EC20, 0xF820E4, 0xF8E9ED, 0xF9EC20, 0xFA20E0, 0xFA20E1, 0xFA20E4, 0xFA20EC, 0xFA20EE, 0xFA20F9, }; public String getName() { return "ISO-8859-8-I"; } public String getLanguage() { return "he"; } public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8-I"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, confidence, name, "he"); } } static class CharsetRecog_8859_8_he extends CharsetRecog_8859_8 { private static final int[] ngrams = { 0x20E0E5, 0x20E0EC, 0x20E4E9, 0x20E4EC, 0x20E4EE, 0x20E4F0, 0x20E9F0, 0x20ECF2, 0x20ECF9, 0x20EDE5, 0x20EDE9, 0x20EFE5, 0x20EFE9, 0x20F8E5, 0x20F8E9, 0x20FAE0, 0x20FAE5, 0x20FAE9, 0xE020E4, 0xE020EC, 0xE020ED, 0xE020FA, 0xE0E420, 0xE0E5E4, 0xE0EC20, 0xE0EE20, 0xE120E4, 0xE120ED, 0xE120FA, 0xE420E4, 0xE420E9, 0xE420EC, 0xE420ED, 0xE420EF, 0xE420F8, 0xE420FA, 0xE4EC20, 0xE5E020, 0xE5E420, 0xE7E020, 0xE9E020, 0xE9E120, 0xE9E420, 0xEC20E4, 0xEC20ED, 0xEC20FA, 0xECF220, 0xECF920, 0xEDE9E9, 0xEDE9F0, 0xEDE9F8, 0xEE20E4, 0xEE20ED, 0xEE20FA, 0xEEE120, 0xEEE420, 0xF2E420, 0xF920E4, 0xF920ED, 0xF920FA, 0xF9E420, 0xFAE020, 0xFAE420, 0xFAE5E9, }; public String getLanguage() { return "he"; } public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1255" : "ISO-8859-8"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, confidence, name, "he"); } } abstract static class CharsetRecog_8859_9 extends CharsetRecog_sbcs { protected static byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0x69, (byte) 0xFE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0x20, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; public String getName() { return "ISO-8859-9"; } } static class CharsetRecog_8859_9_tr extends CharsetRecog_8859_9 { private static final int[] ngrams = { 0x206261, 0x206269, 0x206275, 0x206461, 0x206465, 0x206765, 0x206861, 0x20696C, 0x206B61, 0x206B6F, 0x206D61, 0x206F6C, 0x207361, 0x207461, 0x207665, 0x207961, 0x612062, 0x616B20, 0x616C61, 0x616D61, 0x616E20, 0x616EFD, 0x617220, 0x617261, 0x6172FD, 0x6173FD, 0x617961, 0x626972, 0x646120, 0x646520, 0x646920, 0x652062, 0x65206B, 0x656469, 0x656E20, 0x657220, 0x657269, 0x657369, 0x696C65, 0x696E20, 0x696E69, 0x697220, 0x6C616E, 0x6C6172, 0x6C6520, 0x6C6572, 0x6E2061, 0x6E2062, 0x6E206B, 0x6E6461, 0x6E6465, 0x6E6520, 0x6E6920, 0x6E696E, 0x6EFD20, 0x72696E, 0x72FD6E, 0x766520, 0x796120, 0x796F72, 0xFD6E20, 0xFD6E64, 0xFD6EFD, 0xFDF0FD, }; public String getLanguage() { return "tr"; } public CharsetMatch match(CharsetDetector det) { String name = det.fC1Bytes ? "windows-1254" : "ISO-8859-9"; int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, confidence, name, "tr"); } } static class CharsetRecog_windows_1251 extends CharsetRecog_sbcs { private static final int[] ngrams = { // ' в ' ' во' ' до' ' за' ' и ' ' ка' ' ко' ' на' ' не' ' об' ' по' ' пр' ' ра' ' со' ' ст' ' то' 0x20E220, 0x20E2EE, 0x20E4EE, 0x20E7E0, 0x20E820, 0x20EAE0, 0x20EAEE, 0x20EDE0, 0x20EDE5, 0x20EEE1, 0x20EFEE, 0x20EFF0, 0x20F0E0, 0x20F1EE, 0x20F1F2, 0x20F2EE, // ' чт' ' эт' 'ани' 'ать' 'го ' 'ель' 'ени' 'ест' 'ет ' 'и п' 'ие ' 'ии ' 'ия ' 'лен' 'ли ' 'льн' 0x20F7F2, 0x20FDF2, 0xE0EDE8, 0xE0F2FC, 0xE3EE20, 0xE5EBFC, 0xE5EDE8, 0xE5F1F2, 0xE5F220, 0xE820EF, 0xE8E520, 0xE8E820, 0xE8FF20, 0xEBE5ED, 0xEBE820, 0xEBFCED, // 'на ' 'не ' 'ние' 'ния' 'но ' 'нов' 'о в' 'о п' 'о с' 'ов ' 'ова' 'ого' 'ой ' 'оль' 'ом ' 'ост' 0xEDE020, 0xEDE520, 0xEDE8E5, 0xEDE8FF, 0xEDEE20, 0xEDEEE2, 0xEE20E2, 0xEE20EF, 0xEE20F1, 0xEEE220, 0xEEE2E0, 0xEEE3EE, 0xEEE920, 0xEEEBFC, 0xEEEC20, 0xEEF1F2, // 'пол' 'пре' 'при' 'про' 'рав' 'ред' 'ста' 'ств' 'сти' 'ся ' 'тел' 'то ' 'тор' 'ть ' 'что' 'ых ' 0xEFEEEB, 0xEFF0E5, 0xEFF0E8, 0xEFF0EE, 0xF0E0E2, 0xF0E5E4, 0xF1F2E0, 0xF1F2E2, 0xF1F2E8, 0xF1FF20, 0xF2E5EB, 0xF2EE20, 0xF2EEF0, 0xF2FC20, 0xF7F2EE, 0xFBF520, }; private static final byte[] byteMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 1- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 2- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 3- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 4- */ (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, /* 5- */ (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 6- */ (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, /* 7- */ (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 8- */ (byte) 0x90, (byte) 0x83, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, /* 9- */ (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA8, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, /* A- */ (byte) 0x20, (byte) 0xA2, (byte) 0xA2, (byte) 0xBC, (byte) 0x20, (byte) 0xB4, (byte) 0x20, (byte) 0x20, (byte) 0xA8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xBF, /* B- */ (byte) 0x20, (byte) 0x20, (byte) 0xB3, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0xB8, (byte) 0x20, (byte) 0xBA, (byte) 0x20, (byte) 0xBC, (byte) 0xBE, (byte) 0xBE, (byte) 0xBF, /* C- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* D- */ (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, /* E- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* F- */ (byte) 0xF0, (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, (byte) 0xF9, (byte) 0xFA, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0xFF, }; public String getName() { return "windows-1251"; } public String getLanguage() { return "ru"; } public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_windows_1256 extends CharsetRecog_sbcs { private static final int[] ngrams = { 0x20C7E1, 0x20C7E4, 0x20C8C7, 0x20DAE1, 0x20DDED, 0x20E1E1, 0x20E3E4, 0x20E6C7, 0xC720C7, 0xC7C120, 0xC7CA20, 0xC7D120, 0xC7E120, 0xC7E1C3, 0xC7E1C7, 0xC7E1C8, 0xC7E1CA, 0xC7E1CC, 0xC7E1CD, 0xC7E1CF, 0xC7E1D3, 0xC7E1DA, 0xC7E1DE, 0xC7E1E3, 0xC7E1E6, 0xC7E1ED, 0xC7E320, 0xC7E420, 0xC7E4CA, 0xC820C7, 0xC920C7, 0xC920DD, 0xC920E1, 0xC920E3, 0xC920E6, 0xCA20C7, 0xCF20C7, 0xCFC920, 0xD120C7, 0xD1C920, 0xD320C7, 0xDA20C7, 0xDAE1EC, 0xDDED20, 0xE120C7, 0xE1C920, 0xE1EC20, 0xE1ED20, 0xE320C7, 0xE3C720, 0xE3C920, 0xE3E420, 0xE420C7, 0xE520C7, 0xE5C720, 0xE6C7E1, 0xE6E420, 0xEC20C7, 0xED20C7, 0xED20E3, 0xED20E6, 0xEDC920, 0xEDD120, 0xEDE420, }; private static final byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x81, (byte) 0x20, (byte) 0x83, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x88, (byte) 0x20, (byte) 0x8A, (byte) 0x20, (byte) 0x9C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, (byte) 0x90, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x98, (byte) 0x20, (byte) 0x9A, (byte) 0x20, (byte) 0x9C, (byte) 0x20, (byte) 0x20, (byte) 0x9F, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xAA, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xB5, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0x20, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF4, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xF9, (byte) 0x20, (byte) 0xFB, (byte) 0xFC, (byte) 0x20, (byte) 0x20, (byte) 0xFF, }; public String getName() { return "windows-1256"; } public String getLanguage() { return "ar"; } public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_KOI8_R extends CharsetRecog_sbcs { private static final int[] ngrams = { 0x20C4CF, 0x20C920, 0x20CBC1, 0x20CBCF, 0x20CEC1, 0x20CEC5, 0x20CFC2, 0x20D0CF, 0x20D0D2, 0x20D2C1, 0x20D3CF, 0x20D3D4, 0x20D4CF, 0x20D720, 0x20D7CF, 0x20DAC1, 0x20DCD4, 0x20DED4, 0xC1CEC9, 0xC1D4D8, 0xC5CCD8, 0xC5CEC9, 0xC5D3D4, 0xC5D420, 0xC7CF20, 0xC920D0, 0xC9C520, 0xC9C920, 0xC9D120, 0xCCC5CE, 0xCCC920, 0xCCD8CE, 0xCEC120, 0xCEC520, 0xCEC9C5, 0xCEC9D1, 0xCECF20, 0xCECFD7, 0xCF20D0, 0xCF20D3, 0xCF20D7, 0xCFC7CF, 0xCFCA20, 0xCFCCD8, 0xCFCD20, 0xCFD3D4, 0xCFD720, 0xCFD7C1, 0xD0CFCC, 0xD0D2C5, 0xD0D2C9, 0xD0D2CF, 0xD2C1D7, 0xD2C5C4, 0xD3D120, 0xD3D4C1, 0xD3D4C9, 0xD3D4D7, 0xD4C5CC, 0xD4CF20, 0xD4CFD2, 0xD4D820, 0xD9C820, 0xDED4CF, }; private static final byte[] byteMap = { (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6A, (byte) 0x6B, (byte) 0x6C, (byte) 0x6D, (byte) 0x6E, (byte) 0x6F, (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x7A, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xA3, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, (byte) 0xC0, (byte) 0xC1, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xCB, (byte) 0xCC, (byte) 0xCD, (byte) 0xCE, (byte) 0xCF, (byte) 0xD0, (byte) 0xD1, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, }; public String getName() { return "KOI8-R"; } public String getLanguage() { return "ru"; } public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_IBM424_he extends CharsetRecog_sbcs { protected static byte[] byteMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 4- */ (byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 7- */ (byte) 0x40, (byte) 0x71, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x00, (byte) 0x40, (byte) 0x40, /* 8- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 9- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* B- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, }; public String getLanguage() { return "he"; } } static class CharsetRecog_IBM424_he_rtl extends CharsetRecog_IBM424_he { public String getName() { return "IBM424_rtl"; } private static final int[] ngrams = { 0x404146, 0x404148, 0x404151, 0x404171, 0x404251, 0x404256, 0x404541, 0x404546, 0x404551, 0x404556, 0x404562, 0x404569, 0x404571, 0x405441, 0x405445, 0x405641, 0x406254, 0x406954, 0x417140, 0x454041, 0x454042, 0x454045, 0x454054, 0x454056, 0x454069, 0x454641, 0x464140, 0x465540, 0x465740, 0x466840, 0x467140, 0x514045, 0x514540, 0x514671, 0x515155, 0x515540, 0x515740, 0x516840, 0x517140, 0x544041, 0x544045, 0x544140, 0x544540, 0x554041, 0x554042, 0x554045, 0x554054, 0x554056, 0x554069, 0x564540, 0x574045, 0x584540, 0x585140, 0x585155, 0x625440, 0x684045, 0x685155, 0x695440, 0x714041, 0x714042, 0x714045, 0x714054, 0x714056, 0x714069, }; public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap, (byte)0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_IBM424_he_ltr extends CharsetRecog_IBM424_he { public String getName() { return "IBM424_ltr"; } private static final int[] ngrams = { 0x404146, 0x404154, 0x404551, 0x404554, 0x404556, 0x404558, 0x405158, 0x405462, 0x405469, 0x405546, 0x405551, 0x405746, 0x405751, 0x406846, 0x406851, 0x407141, 0x407146, 0x407151, 0x414045, 0x414054, 0x414055, 0x414071, 0x414540, 0x414645, 0x415440, 0x415640, 0x424045, 0x424055, 0x424071, 0x454045, 0x454051, 0x454054, 0x454055, 0x454057, 0x454068, 0x454071, 0x455440, 0x464140, 0x464540, 0x484140, 0x514140, 0x514240, 0x514540, 0x544045, 0x544055, 0x544071, 0x546240, 0x546940, 0x555151, 0x555158, 0x555168, 0x564045, 0x564055, 0x564071, 0x564240, 0x564540, 0x624540, 0x694045, 0x694055, 0x694071, 0x694540, 0x714140, 0x714540, 0x714651 }; public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap, (byte)0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } abstract static class CharsetRecog_IBM420_ar extends CharsetRecog_sbcs { protected static byte[] byteMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 1- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 2- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 3- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 4- */ (byte) 0x40, (byte) 0x40, (byte) 0x42, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 5- */ (byte) 0x40, (byte) 0x51, (byte) 0x52, (byte) 0x40, (byte) 0x40, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 6- */ (byte) 0x40, (byte) 0x40, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 7- */ (byte) 0x70, (byte) 0x71, (byte) 0x72, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, /* 8- */ (byte) 0x80, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x8B, (byte) 0x8C, (byte) 0x8D, (byte) 0x8E, (byte) 0x8F, /* 9- */ (byte) 0x90, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0x9B, (byte) 0x9C, (byte) 0x9D, (byte) 0x9E, (byte) 0x9F, /* A- */ (byte) 0xA0, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF, /* B- */ (byte) 0xB0, (byte) 0xB1, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0x40, (byte) 0x40, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xBB, (byte) 0xBC, (byte) 0xBD, (byte) 0xBE, (byte) 0xBF, /* C- */ (byte) 0x40, (byte) 0x81, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, (byte) 0x40, (byte) 0xCB, (byte) 0x40, (byte) 0xCD, (byte) 0x40, (byte) 0xCF, /* D- */ (byte) 0x40, (byte) 0x91, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0xDA, (byte) 0xDB, (byte) 0xDC, (byte) 0xDD, (byte) 0xDE, (byte) 0xDF, /* E- */ (byte) 0x40, (byte) 0x40, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xEA, (byte) 0xEB, (byte) 0x40, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* F- */ (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0x40, (byte) 0xFB, (byte) 0xFC, (byte) 0xFD, (byte) 0xFE, (byte) 0x40, }; public String getLanguage() { return "ar"; } } static class CharsetRecog_IBM420_ar_rtl extends CharsetRecog_IBM420_ar { private static final int[] ngrams = { 0x4056B1, 0x4056BD, 0x405856, 0x409AB1, 0x40ABDC, 0x40B1B1, 0x40BBBD, 0x40CF56, 0x564056, 0x564640, 0x566340, 0x567540, 0x56B140, 0x56B149, 0x56B156, 0x56B158, 0x56B163, 0x56B167, 0x56B169, 0x56B173, 0x56B178, 0x56B19A, 0x56B1AD, 0x56B1BB, 0x56B1CF, 0x56B1DC, 0x56BB40, 0x56BD40, 0x56BD63, 0x584056, 0x624056, 0x6240AB, 0x6240B1, 0x6240BB, 0x6240CF, 0x634056, 0x734056, 0x736240, 0x754056, 0x756240, 0x784056, 0x9A4056, 0x9AB1DA, 0xABDC40, 0xB14056, 0xB16240, 0xB1DA40, 0xB1DC40, 0xBB4056, 0xBB5640, 0xBB6240, 0xBBBD40, 0xBD4056, 0xBF4056, 0xBF5640, 0xCF56B1, 0xCFBD40, 0xDA4056, 0xDC4056, 0xDC40BB, 0xDC40CF, 0xDC6240, 0xDC7540, 0xDCBD40, }; public String getName() { return "IBM420_rtl"; } public CharsetMatch match(CharsetDetector det) { int confidence = matchIBM420(det, ngrams, byteMap, (byte)0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_IBM420_ar_ltr extends CharsetRecog_IBM420_ar { private static final int[] ngrams = { 0x404656, 0x4056BB, 0x4056BF, 0x406273, 0x406275, 0x4062B1, 0x4062BB, 0x4062DC, 0x406356, 0x407556, 0x4075DC, 0x40B156, 0x40BB56, 0x40BD56, 0x40BDBB, 0x40BDCF, 0x40BDDC, 0x40DAB1, 0x40DCAB, 0x40DCB1, 0x49B156, 0x564056, 0x564058, 0x564062, 0x564063, 0x564073, 0x564075, 0x564078, 0x56409A, 0x5640B1, 0x5640BB, 0x5640BD, 0x5640BF, 0x5640DA, 0x5640DC, 0x565840, 0x56B156, 0x56CF40, 0x58B156, 0x63B156, 0x63BD56, 0x67B156, 0x69B156, 0x73B156, 0x78B156, 0x9AB156, 0xAB4062, 0xADB156, 0xB14062, 0xB15640, 0xB156CF, 0xB19A40, 0xB1B140, 0xBB4062, 0xBB40DC, 0xBBB156, 0xBD5640, 0xBDBB40, 0xCF4062, 0xCF40DC, 0xCFB156, 0xDAB19A, 0xDCAB40, 0xDCB156 }; public String getName() { return "IBM420_ltr"; } public CharsetMatch match(CharsetDetector det) { int confidence = matchIBM420(det, ngrams, byteMap, (byte)0x40); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } static class CharsetRecog_cp866 extends CharsetRecog_sbcs { private static final int[] ngrams = { // ' в ' ' во' ' до' ' за' ' и ' ' ка' ' ко' ' на' ' не' ' об' ' по' ' пр' ' ра' ' со' ' ст' ' то' 0x20A220, 0x20A2AE, 0x20A4AE, 0x20A7A0, 0x20A820, 0x20AAA0, 0x20AAAE, 0x20ADA0, 0x20ADA5, 0x20AEA1, 0x20AFAE, 0x20AFE0, 0x20E0A0, 0x20E1AE, 0x20E1E2, 0x20E2AE, // ' чт' ' эт' 'ани' 'ать' 'го ' 'ель' 'ени' 'ест' 'ет ' 'и п' 'ие ' 'ии ' 'ия ' 'лен' 'ли ' 'льн' 0x20E7E2, 0x20EDE2, 0xA0ADA8, 0xA0E2EC, 0xA3AE20, 0xA5ABEC, 0xA5ADA8, 0xA5E1E2, 0xA5E220, 0xA820AF, 0xA8A520, 0xA8A820, 0xA8EF20, 0xABA5AD, 0xABA820, 0xABECAD, // 'на ' 'не ' 'ние' 'ния' 'но ' 'нов' 'о в' 'о п' 'о с' 'ов ' 'ова' 'ого' 'ой ' 'оль' 'ом ' 'ост' 0xADA020, 0xADA520, 0xADA8A5, 0xADA8EF, 0xADAE20, 0xADAEA2, 0xAE20A2, 0xAE20AF, 0xAE20E1, 0xAEA220, 0xAEA2A0, 0xAEA3AE, 0xAEA920, 0xAEABEC, 0xAEAC20, 0xAEE1E2, // 'пол' 'пре' 'при' 'про' 'рав' 'ред' 'ста' 'ств' 'сти' 'ся ' 'тел' 'то ' 'тор' 'ть ' 'что' 'ых ' 0xAFAEAB, 0xAFE0A5, 0xAFE0A8, 0xAFE0AE, 0xE0A0A2, 0xE0A5A4, 0xE1E2A0, 0xE1E2A2, 0xE1E2A8, 0xE1EF20, 0xE2A5AB, 0xE2AE20, 0xE2AEE0, 0xE2EC20, 0xE7E2AE, 0xEBE520 }; private static final byte[] byteMap = { /* -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F */ /* 0- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 1- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 2- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x00, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 3- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 4- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 5- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 6- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 7- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* 8- */ (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF, /* 9- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* A- */ (byte) 0xA0, (byte) 0xA1, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xAB, (byte) 0xAC, (byte) 0xAD, (byte) 0xAE, (byte) 0xAF, /* B- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* C- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* D- */ (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, /* E- */ (byte) 0xE0, (byte) 0xE1, (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, (byte) 0xEB, (byte) 0xEC, (byte) 0xED, (byte) 0xEE, (byte) 0xEF, /* F- */ (byte) 0xF0, (byte) 0xF0, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, (byte) 0x20, }; public String getName() { return "cp866"; } public String getLanguage() { return "ru"; } public CharsetMatch match(CharsetDetector det) { int confidence = match(det, ngrams, byteMap); return confidence == 0 ? null : new CharsetMatch(det, this, confidence); } } } ================================================ FILE: src/main/java/com/ibm/icu/text/CharsetRecognizer.java ================================================ /* ******************************************************************************* * Copyright (C) 2005-2012, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.text; /** * Abstract class for recognizing a single charset. * Part of the implementation of ICU's CharsetDetector. * Each specific charset that can be recognized will have an instance of some subclass of this class. * All interaction between the overall CharsetDetector and the stuff specific to an individual charset happens * via the interface provided here. * Instances of CharsetDetector DO NOT have or maintain state pertaining to a specific match or detect operation. * The WILL be shared by multiple instances of CharsetDetector. They encapsulate const charset-specific information. */ abstract class CharsetRecognizer { /** * Get the IANA name of this charset. * @return the charset name. */ abstract String getName(); /** * Get the ISO language code for this charset. * @return the language code, or null if the language cannot be determined. */ public String getLanguage() { return null; } /** * Test the match of this charset with the input text data * which is obtained via the CharsetDetector object. * * @param det The CharsetDetector, which contains the input text * to be checked for being in this charset. * @return A CharsetMatch object containing details of match * with this charset, or null if there was no match. */ abstract CharsetMatch match(CharsetDetector det); } ================================================ FILE: src/main/java/com/mucommander/PlatformManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander; import java.io.File; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.runtime.OsFamily; /** * This class takes care of platform-specific issues, such as getting screen dimensions and issuing commands. * * @author Maxence Bernard, Nicolas Rinaudo */ public class PlatformManager { private static final Logger LOGGER = LoggerFactory.getLogger(PlatformManager.class); /** Folder in which to store the preferences. */ private static AbstractFile prefFolder; /** * Returns the path to the default muCommander preferences folder. *

* This folder is: *

    *
  • ~/Library/Preferences/trolCommander/ under MAC OS X.
  • *
  • ~/.trolcommander/ under all other OSes.
  • *
*

* If the default preferences folder doesn't exist, this method will create it. * * @return the path to the default trolCommander preferences folder. */ public static AbstractFile getDefaultPreferencesFolder() { return FileFactory.getFile(getDefaultPreferencesFolderPath()); } /** * Returns the path to the default muCommander preferences folder. *

* This folder is: *

    *
  • ~/Library/Preferences/trolCommander/ under MAC OS X.
  • *
  • ~/.trolcommander/ under all other OSes.
  • *
*

* If the default preferences folder doesn't exist, this method will create it. * * @return the path to the default trolCommander preferences folder. */ public static String getDefaultPreferencesFolderPath() { File folder; // Mac OS X specific folder (~/Library/Preferences/trolCommander) if (OsFamily.MAC_OS_X.isCurrent()) { folder = new File(System.getProperty("user.home") + "/Library/Preferences/trolCommander"); // For all other platforms, use generic folder (~/.trolcommander) } else { folder = new File(System.getProperty("user.home"), "/.trolcommander"); } // Makes sure the folder exists. if (!folder.exists()) { if (!folder.mkdir()) { LOGGER.warn("Could not create preference folder: {}", folder.getAbsolutePath()); } } return folder.getAbsolutePath(); } /** * Returns the path to the folder that contains all the user's data. *

* All modules that save user data to a file should do so in a file located in * the folder returned by this method. *

* The value returned by this method can be set through {@link #setPreferencesFolder(File)}. * Otherwise, the {@link #getDefaultPreferencesFolder() default preference folder} will be * used. * * @return the path to the user's preference folder. * @see #setPreferencesFolder(AbstractFile) */ public static AbstractFile getPreferencesFolder() { // If the preferences folder has been set, use it. if (prefFolder != null) { return prefFolder; } return getDefaultPreferencesFolder(); } /** * Sets the path to the folder in which trolCommander will look for its preferences. *

* If folder is a file, its parent folder will be used instead. If it doesn't exist, * this method will create it. * * @param folder path to the folder in which trolCommander will look for its preferences. * @throws IOException if an IO error occurs. * @see #getPreferencesFolder() * @see #setPreferencesFolder(String) * @see #setPreferencesFolder(AbstractFile) */ public static void setPreferencesFolder(File folder) throws IOException { AbstractFile file = FileFactory.getFile(folder.getAbsolutePath()); if (file != null) { setPreferencesFolder(file); } } /** * Sets the path to the folder in which trolCommander will look for its preferences. *

* If folder is a file, its parent folder will be used instead. If it doesn't exist, * this method will create it. * * @param path path to the folder in which trolCommander will look for its preferences. * @throws IOException if an IO error occurs. * @see #getPreferencesFolder() * @see #setPreferencesFolder(File) * @see #setPreferencesFolder(AbstractFile) */ public static void setPreferencesFolder(String path) throws IOException { AbstractFile folder = FileFactory.getFile(path); if (folder == null) { setPreferencesFolder(new File(path)); } else { setPreferencesFolder(folder); } } /** * Sets the path to the folder in which trolCommander will look for its preferences. *

* If folder is a file, its parent folder will be used instead. If it doesn't exist, * this method will create it. * * @param folder path to the folder in which trolCommander will look for its preferences. * @throws IOException if an IO error occurs. * @see #getPreferencesFolder() * @see #setPreferencesFolder(String) * @see #setPreferencesFolder(File) */ public static void setPreferencesFolder(AbstractFile folder) throws IOException { if (!folder.exists()) { folder.mkdir(); } else if (!folder.isBrowsable()) { folder = folder.getParent(); } prefFolder = folder; } } ================================================ FILE: src/main/java/com/mucommander/RuntimeConstants.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander; import java.awt.*; import java.io.IOException; import java.io.InputStream; import java.util.Calendar; import java.util.jar.Attributes; import java.util.jar.Manifest; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.util.ResourceLoader; /** * Defines various generic trolCommander constants. * @author Nicolas Rinaudo */ public class RuntimeConstants { private static final Logger LOGGER = LoggerFactory.getLogger(RuntimeConstants.class); /** Path to the themes directory. */ public static final String THEMES_PATH = "/themes"; /** Path to the viewer/editor themes directory. */ public static final String TEXT_SYNTAX_THEMES_PATH = "/themes/editor"; /** Path to the trolCommander license file. */ public static final String LICENSE = "/license.txt"; /** Default trolCommander theme. */ public static final String DEFAULT_THEME = "Native"; public static final boolean DISPLAY_4K = is4KDisplay(); /** Homepage URL. */ public static final String HOMEPAGE_URL = "http://trolsoft.ru/en/soft/trolcommander"; public static final String DEFAULT_VERSION_URL = "http://trolsoft.ru/content/soft/trolcommander/version.xml"; /** URL at which to download the latest version description. */ public static final String VERSION_URL; /** URL of the trolCommander forums. */ public static final String FORUMS_URL = HOMEPAGE_URL + "/forums/"; /** URL at which to see the donation information. */ public static final String DONATION_URL = HOMEPAGE_URL + "/#donate"; /** Bug tracker URL. */ public static final String BUG_REPOSITORY_URL = "https://github.com/trol73/mucommander/issues"; //HOMEPAGE_URL + "/bugs/"; /** Documentation URL. */ public static final String DOCUMENTATION_URL = HOMEPAGE_URL;// + "/documentation/"; /** * Release date to use in case the JAR file doesn't contain the information. * This is guaranteed to trigger a software update - the JAR file is corrupt, so we might as well get the latest * version. */ private static final String DEFAULT_RELEASE_DATE = "20020101"; /** Current trolCommander version (MAJOR.MINOR.DEV). */ public static final String VERSION; /** Date at which the build was generated (YYYYMMDD). */ public static final String BUILD_DATE; /** Time at which the build was generated (HHMM). */ public static final String BUILD_TIME; /** Copyright information (YYYY-YYYY). */ public static final String COPYRIGHT; /** String describing the software (trolCommander vMAJOR.MINOR.DEV). */ public static final String APP_STRING; /** String describing the trolCommander build number. */ public static final String BUILD_NUMBER; /** YYYYMMDDHHmm */ public static final String BUILD_CODE; static { Attributes attributes = getManifestAttributes(); if (attributes == null) { // No MANIFEST.MF found, use default values. VERSION = "?"; COPYRIGHT = "2013-" + Calendar.getInstance().get(Calendar.YEAR); // We use a date that we are sure is later than the latest version to trigger the version checker. // After all, the JAR appears to be corrupt and should be upgraded. BUILD_DATE = DEFAULT_RELEASE_DATE; BUILD_TIME = null; VERSION_URL = DEFAULT_VERSION_URL; BUILD_NUMBER = "?"; BUILD_CODE = null; } else { // A MANIFEST.MF file was found, extract data from it. VERSION = getAttribute(attributes, "Specification-Version"); BUILD_DATE = getAttribute(attributes, "Build-Date"); BUILD_TIME = getAttribute(attributes, "Build-Time"); VERSION_URL = getAttribute(attributes, "Build-URL"); BUILD_NUMBER = getAttribute(attributes, "Implementation-Version"); // Protection against corrupt manifest files. COPYRIGHT = BUILD_DATE.length() > 4 ? BUILD_DATE.substring(0, 4) : DEFAULT_RELEASE_DATE; BUILD_CODE = BUILD_DATE + BUILD_TIME; } APP_STRING = "trolCommander v" + VERSION; } @Nullable private static Attributes getManifestAttributes() { try (InputStream in = ResourceLoader.getResourceAsStream("META-INF/MANIFEST.MF", ResourceLoader.getDefaultClassLoader(), ResourceLoader.getRootPackageAsFile(RuntimeConstants.class))) { if (in != null) { Manifest manifest = new Manifest(); manifest.read(in); return manifest.getMainAttributes(); } } catch (IOException e) { LOGGER.warn("Failed to read MANIFEST.MF, default values will be used", e); } return null; } /** * Extract the requested attribute value. * @param attributes attributes from which to extract the requested value. * @param name name of the attribute to retrieve. * @return the requested attribute value. */ private static String getAttribute(Attributes attributes, String name) { String buffer = attributes.getValue(name); return buffer == null ? "?" : buffer; } private static boolean is4KDisplay() { Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); return screenSize.width*screenSize.height > 3500*3500; } } ================================================ FILE: src/main/java/com/mucommander/StressTester.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Random; import javax.swing.JButton; import javax.swing.JDialog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.main.table.FileTable; /** * Used to start muCommander in stress-test mode. * @author Maxence Bernard */ public class StressTester implements Runnable, ActionListener { private static final Logger LOGGER = LoggerFactory.getLogger(StressTester.class); private boolean run; public StressTester() { run = true; } /** * Stops the current stress test. */ public void stop() { run = false; } public void run() { Random random = new Random(); MainFrame mainFrame = WindowManager.getCurrentMainFrame(); while (run) { if(random.nextInt(2)==0) { ActionManager.performAction(com.mucommander.ui.action.impl.SwitchActiveTableAction.Descriptor.ACTION_ID, mainFrame); } FolderPanel folderPanel = mainFrame.getActivePanel(); FileTable fileTable = mainFrame.getActiveTable(); AbstractFile currentFolder = folderPanel.getCurrentFolder(); try { AbstractFile parentFolder = currentFolder.getParent(); AbstractFile[] children = currentFolder.ls(); // 1 in 3 chance to go up if folder has children if (children.length==0 || (random.nextInt(3)==0 && parentFolder!=null)) { fileTable.selectFile(0); ActionManager.performAction(com.mucommander.ui.action.impl.OpenAction.Descriptor.ACTION_ID, mainFrame); } else { AbstractFile randomChild = children[random.nextInt(children.length)]; if(!randomChild.isBrowsable()) continue; // Try to ls() in RandomChild to trigger an IOException if folder is not readable // so that no error dialog pops up when calling tryChangeCurrentFolder() randomChild.ls(); fileTable.selectFile(randomChild); ActionManager.performAction(com.mucommander.ui.action.impl.OpenAction.Descriptor.ACTION_ID, mainFrame); // folderPanel.tryChangeCurrentFolder(randomChild, true); } } catch(Exception e) { LOGGER.debug("Caught Exception", e); } LOGGER.trace("Sleeping for a bit..."); try { Thread.sleep(100+random.nextInt(200)); } catch(InterruptedException e) { LOGGER.debug("Caught InterruptedException", e); } } } public void actionPerformed(ActionEvent e) { stop(); } /** * Method used to start the stress tester. * @param args command line arguments. */ public static void main(String[] args) { TrolCommander.main(args); StressTester instance = new StressTester(); JDialog stopDialog = new JDialog(); JButton stopButton = new JButton("Stop"); new Thread(instance).start(); stopButton.addActionListener(instance); stopDialog.getContentPane().add(stopButton); stopDialog.setSize(new Dimension(80, 60)); stopDialog.setVisible(true); } } ================================================ FILE: src/main/java/com/mucommander/TrolCommander.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.launcher.LauncherCmdHelper; import com.mucommander.launcher.LauncherExecutor; import com.mucommander.launcher.LauncherTask; import com.mucommander.profiler.Profiler; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.startup.CheckVersionDialog; import com.mucommander.ui.main.WindowManager; import com.mucommander.updates.VersionChecker; import com.mucommander.utils.text.Translator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.SwingUtilities; import java.util.*; import java.util.List; import static com.mucommander.launcher.TasksKt.prepareLauncherTasks; /** * trolCommander launcher. *

* This class is used to start muCommander. It will analyse command line * arguments, initialize the whole software and start the main window. * * @author Maxence Bernard, Nicolas Rinaudo, Oleg Trifonov */ public class TrolCommander { private static Logger logger; /** true while the application is launching, false after it has finished launching */ private static boolean isLaunching = true; /** Launch lock. */ private static final Object LAUNCH_LOCK = new Object(); /** * Prevents initialization of the Launcher. */ private TrolCommander() {} /** * This method can be called to wait until the application has been launched. The caller thread will be blocked * until the application has been launched. * This method will return immediately if the application has already been launched when it is called. */ public static void waitUntilLaunched() { getLogger().debug("called, thread {}", Thread.currentThread()); synchronized(LAUNCH_LOCK) { while (isLaunching) { try { getLogger().debug("waiting"); LAUNCH_LOCK.wait(); } catch (InterruptedException e) { // will loop } } } } // - Boot code -------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Method used to migrate commands that used to be defined in the configuration but were moved to commands.xml. * @param useName name of the use custom command configuration variable. * @param commandName name of the custom command configuration variable. */ public static void migrateCommand(String useName, String commandName, String alias) { String command; if (TcConfigurations.getPreferences().getBooleanVariable(useName) && (command = TcConfigurations.getPreferences().getVariable(commandName)) != null) { CommandManager.registerCommand(new Command(alias, command, CommandType.SYSTEM_COMMAND)); TcConfigurations.getPreferences().removeVariable(useName); TcConfigurations.getPreferences().removeVariable(commandName); } } /** * Main method used to startup muCommander. * @param args command line arguments. */ public static void main(String[] args) { if (OsFamily.MAC_OS_X.isCurrent()) { System.setProperty("com.apple.mrj.application.apple.menu.about.name", "trolCommander"); // disable openGL in javaFX (used for HtmlViewer) as it cashes JVM under vmWare System.setProperty("prism.order", "sw"); } Profiler.start("init"); Profiler.start("loading"); int processors = Runtime.getRuntime().availableProcessors(); System.out.println("Current OS family: " + OsFamily.getCurrent()); System.out.println("Processors: " + processors); try (LauncherExecutor executor = new LauncherExecutor(processors <= 0 ? 1 : processors)) { LauncherCmdHelper helper = new LauncherCmdHelper(args, true, false); // Whether, or not to ignore warnings when booting. helper.parseArgs(); List tasks = prepareLauncherTasks(helper); if (processors <= 1) { for (LauncherTask t : tasks) { t.call(); } } else { while (!executor.isFull()) { executor.executeFirst(tasks); } while (!tasks.isEmpty()) { executor.executeFirst(tasks); if (executor.isFull()) { try { Thread.sleep(1); } catch (Exception ignore) {} } else { if (executor.executeFirst(tasks)) { continue; } LauncherTask t = tasks.getFirst(); executor.execute(t, true); tasks.remove(t); } } } // executor.shutdown(); System.out.println("finished"); } catch(Throwable t) { // Startup failed, dispose the splash screen // if (splashScreen != null) { // splashScreen.dispose(); // } getLogger().error("Startup failed", t); // Display an error dialog with a proper message and error details InformationDialog.showErrorDialog(null, null, Translator.get("startup_error"), null, t); // Quit the application WindowManager.quit(); } /* try { executor.awaitTermination(100, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } */ // Done launching, wake up threads waiting for the application being launched. // Important: this must be done before disposing the splash screen, as this would otherwise create a deadlock // if the AWT event thread were waiting in #waitUntilLaunched . synchronized(LAUNCH_LOCK) { isLaunching = false; LAUNCH_LOCK.notifyAll(); } // Check for newer version unless it was disabled if (TcConfigurations.getPreferences().getVariable(TcPreference.CHECK_FOR_UPDATE, TcPreferences.DEFAULT_CHECK_FOR_UPDATE)) { SwingUtilities.invokeLater(() -> { try { VersionChecker versionChecker = VersionChecker.getInstance(); if (versionChecker != null && versionChecker.isNewVersionAvailable()) { new CheckVersionDialog(WindowManager.getCurrentMainFrame(), versionChecker, false); } } catch (Exception e) { getLogger().error("Check version error", e); } }); } Profiler.stop("init"); //Profiler.print(); //Profiler.hide("launcher."); //Profiler.printThreads(); //Profiler.initThreads(); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(TrolCommander.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/adb/AdbUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.adb; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.process.ExecutionFinishListener; import com.mucommander.process.ExecutorUtils; import com.mucommander.ui.tools.ToolsEnvironment; import se.vidstige.jadb.JadbConnection; import se.vidstige.jadb.JadbDevice; import se.vidstige.jadb.JadbException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Oleg Trifonov * Created on 25/12/15. */ public class AdbUtils { private static Map lastDeviceNames; /** * Get list of connected ADB devices * @return null if adb doesn't found */ public static List getDevices() { try { JadbConnection connection = new JadbConnection(); List devices = connection.getDevices(); List names = new ArrayList<>(); for (JadbDevice device : devices) { names.add(device.getSerial()); } return names; } catch (JadbException | IOException e) { e.printStackTrace(); } return null; } /** * * @return true if adb found */ public static boolean checkAdb() { AbstractFile adbPath = getAdbPath(); if (OsFamily.getCurrent().isUnixBased() && adbPath != null) { try { int result = ExecutorUtils.execute("./adb devices -l", adbPath); return result == 0; } catch (IOException | InterruptedException ignore) {} } try { int result = ExecutorUtils.execute("adb devices -l", adbPath); return result == 0; } catch (IOException | InterruptedException ignore) {} return false; } /** * Tried to found adb utility location * * @return path to adb utility or null */ private static AbstractFile getAdbPath() { String path; try { path = ToolsEnvironment.getEnv("ANDROID_HOME"); } catch (SecurityException ignore) { path = null; } if (path != null) { String adb = path + (OsFamily.WINDOWS .isCurrent() ? "\\platform-tools\\adb.exe" : "/platform-tools/adb"); AbstractFile result = FileFactory.getFile(adb); if (result != null && result.exists() && !result.isDirectory()) { return result.getParent(); } } try { path = ToolsEnvironment.getEnv("ADB_HOME"); } catch (SecurityException ignore) { path = null; } if (path != null) { String adb = path + (OsFamily.getCurrent() == OsFamily.WINDOWS ? "\\adb.exe" : "/adb"); AbstractFile result = FileFactory.getFile(adb); if (result != null && result.exists() && !result.isDirectory()) { return result.getParent(); } } if (OsFamily.MAC_OS_X.isCurrent()) { String defaultPath = System.getProperty("user.home") + "/Library/Android/sdk/platform-tools/adb"; AbstractFile result = FileFactory.getFile(defaultPath); if (result != null && result.exists() && !result.isDirectory()) { return result.getParent(); } } return null; } /** * * @param serial the device serial number * * @return device name (or null if unknown) */ public static String getDeviceName(String serial) { if (lastDeviceNames == null || !lastDeviceNames.containsKey(serial)) { lastDeviceNames = getDeviceNames(); } return lastDeviceNames.get(serial); } /** * * @return serial to name */ public static Map getDeviceNames() { final Map result = new HashMap<>(); ExecutionFinishListener listener = (exitCode, output) -> { parseDevicesList(result, output); }; AbstractFile adbPath = getAdbPath(); if (OsFamily.getCurrent().isUnixBased() && adbPath != null) { try { ExecutorUtils.executeAndGetOutput("./adb devices -l", adbPath, listener); } catch (IOException | InterruptedException e) { try { ExecutorUtils.executeAndGetOutput("adb devices -l", adbPath, listener); } catch (IOException | InterruptedException e2) { e2.printStackTrace(); } } } return result; } private static void parseDevicesList(Map result, String output) { String[] lines = output.split("\\r?\\n"); for (String s : lines) { String[] columns = s.split("\\s+"); for (String val : columns) { if (val.startsWith("model:")) { String serial = columns[0]; String name = val.substring(6); // "model:" name = name.replace('_', ' '); result.put(serial, name); } } } } } ================================================ FILE: src/main/java/com/mucommander/adb/AndroidMenu.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.adb; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.icon.IconManager; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import java.util.List; /** * An abstract JMenu that contains an item for each Android ADB devices available * *

Note: the items list is refreshed each time the menu is selected. In other words, a new instance of AdbMenu * does not have to be created in order to see new devices. * * Created on 28/12/15. * @author Oleg Trifonov */ public abstract class AndroidMenu extends JMenu implements MenuListener { /** * Creates a new instance of AndroidMenu. */ public AndroidMenu() { super(Translator.get("adb.android_devices")); setIcon(IconManager.getIcon(IconManager.IconSet.FILE, "android.png")); // Menu items will be added when menu gets selected addMenuListener(this); } /** * Returns the action to perform for the given item. * * @param deviceSerial the serial number of the device * @return the action to perform for the given Android device */ public abstract TcAction getMenuItemAction(String deviceSerial); @Override public void menuSelected(MenuEvent e) { // Remove previous menu items (if any) removeAll(); List androidDevices = AdbUtils.getDevices(); if (androidDevices == null) { return; } if (androidDevices.isEmpty()) { add(new JMenuItem(Translator.get("adb.no_devices"))).setEnabled(false); return; } MnemonicHelper mnemonicHelper = new MnemonicHelper(); for (String serial : androidDevices) { JMenuItem menuItem = new JMenuItem(getMenuItemAction(serial)); menuItem.setMnemonic(mnemonicHelper.getMnemonic(menuItem.getText())); String name = AdbUtils.getDeviceName(serial); menuItem.setText(name == null ? serial : name); menuItem.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, "android.png")); add(menuItem); } } @Override public void menuDeselected(MenuEvent e) { } @Override public void menuCanceled(MenuEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/auth/CredentialsConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.auth; /** * Contains XML elements and attributes used to parse and write the credentials file. * * @author Maxence Bernard */ interface CredentialsConstants { /** Root element */ String ELEMENT_ROOT = "credentials_list"; /** Element for each credential item, containing a URL, login and password */ String ELEMENT_CREDENTIALS = "credentials"; /** Element containing the credentials' URL */ String ELEMENT_URL = "url"; /** Element containing the credentials' login */ String ELEMENT_LOGIN = "login"; /** Element containing the credentials' (encrypted) password*/ String ELEMENT_PASSWORD = "password"; /** Element that defines a property (name/value pair) */ String ELEMENT_PROPERTY = "property"; /** Name attribute of the property element */ String ATTRIBUTE_NAME = "name"; /** Value attribute of the property element */ String ATTRIBUTE_VALUE = "value"; /** Name of the root element's attribute containing the muCommander version that was used to create the credentials file */ String ATTRIBUTE_VERSION = "version"; /** Root element's attribute containing the encryption method used for passwords */ String ATTRIBUTE_ENCRYPTION = "encryption"; /** Weak password encryption method */ String WEAK_ENCRYPTION_METHOD = "weak"; } ================================================ FILE: src/main/java/com/mucommander/auth/CredentialsManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.auth; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.PlatformManager; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.commons.collections.VectorChangeListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.Authenticator; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.util.Chmod; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.io.backup.BackupOutputStream; /** * This class manages {@link CredentialsMapping} instances (login/password pairs associated with a server realm) used to * connect to authenticated file systems. It provides methods to find credentials matching a particular location and to * read and write credentials to an XML file. * *

* Two types of {@link CredentialsMapping} are used: *

    *
  • persistent credentials: stored in an XML file when the application terminates, and loaded the next time the * application is started.
  • *
  • volatile credentials: lost when the application terminates.
  • *
* * @author Maxence Bernard */ public class CredentialsManager { private static Logger logger; /** Contains volatile CredentialsMapping instances, lost when the application terminates */ private static final List volatileCredentialMappings = new Vector<>(); /** Contains persistent CredentialsMapping instances, stored to an XML file when the application * terminates, and loaded the next time the application is started */ private static final AlteredVector persistentCredentialMappings = new AlteredVector<>(); /** Singleton CredentialsManagerAuthenticator instance */ private final static Authenticator AUTHENTICATOR = new CredentialsManagerAuthenticator(); /** Credentials file location */ private static AbstractFile credentialsFile; /** Default credentials file name */ private static final String DEFAULT_CREDENTIALS_FILE_NAME = "credentials.xml"; /** Tracks changes made to the persistent credentials vector. * We keep a reference to the listener so it doesn't get garbage collected. */ private static final VectorChangeListener PERSISTENT_CREDENTIALS_VECTOR_CHANGE_LISTENER; // Don't remove me! /** True when changes were made after the credentials file was last saved */ private static boolean saveNeeded; /** create a singleton instance, needs to be referenced so that it's not garbage collected (AlteredVector * stores VectorChangeListener as weak references) */ private static final CredentialsManager singleton = new CredentialsManager(); static { // Listen to changes made to the persistent entries vector. // Note: we must keep a reference to the listener, as it would otherwise be garbage collected. persistentCredentialMappings.addVectorChangeListener(PERSISTENT_CREDENTIALS_VECTOR_CHANGE_LISTENER = new VectorChangeListener() { public void elementsAdded(int startIndex, int nbAdded) { saveNeeded = true; } public void elementsRemoved(int startIndex, int nbRemoved) { saveNeeded = true; } public void elementChanged(int index) { saveNeeded = true; } }); } /** * Returns the path to the credentials file. * * @return the path to the credentials file. * @throws IOException if there was some problem locating the default credentials file. */ private static AbstractFile getCredentialsFile() throws IOException { if(credentialsFile == null) return PlatformManager.getPreferencesFolder().getChild(DEFAULT_CREDENTIALS_FILE_NAME); return credentialsFile; } /** * Sets the path to the credentials file. * * @param path path to the credentials file * @throws FileNotFoundException if path is not available. */ public static void setCredentialsFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setCredentialsFile(new File(path)); } else { setCredentialsFile(file); } } /** * Sets the path to the credentials file. * * @param file path to the credentials file * @throws FileNotFoundException if path is not available. */ public static void setCredentialsFile(File file) throws FileNotFoundException { setCredentialsFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the credentials file. * * @param file path to the credentials file * @throws FileNotFoundException if path is not available. */ public static void setCredentialsFile(AbstractFile file) throws FileNotFoundException { if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file); } credentialsFile = file; } /** * Tries to load credentials from the credentials file if it exists. Does nothing if it doesn't. * * @throws Exception if an error occurs while loading the credentials file. */ public static void loadCredentials() throws Exception { AbstractFile credentialsFile = getCredentialsFile(); if (credentialsFile.exists()) { getLogger().debug("Found credentials file: "+credentialsFile.getAbsolutePath()); // Parse the credentials file new CredentialsParser().parse(credentialsFile); getLogger().debug("Credentials file loaded."); } else { getLogger().debug("No credentials file found at " + credentialsFile.getAbsolutePath()); } } /** * Tries to write the credentials file. Unless the 'forceWrite' is set to true, the credentials file will be written * only if changes were made to persistent entries since last write. * * @param forceWrite if false, the credentials file will be written only if changes were made to persistent entries * since last write, if true the file will always be written. * @throws IOException if an I/O error occurs. */ public static void writeCredentials(boolean forceWrite) throws IOException { // Write credentials file only if changes were made to persistent entries since last write, or if write is forced if (!(forceWrite || saveNeeded)) { return; } BackupOutputStream out = null; try { credentialsFile = getCredentialsFile(); CredentialsWriter.write(out = new BackupOutputStream(credentialsFile)); saveNeeded = false; } finally { if (out != null) { // TODO autoclosable try { out.close(); } catch(Exception ignore) {} } } // Under UNIX-based systems, change the credentials file's permissions so that the file can't be read by // 'group' and 'other'. boolean fileSecured = !OsFamily.getCurrent().isUnixBased() || Chmod.chmod(credentialsFile, 0600); // rw------- if (fileSecured) { getLogger().debug("Credentials file saved successfully."); } else { getLogger().warn("Credentials file could not be chmod!"); } } /** * Returns an array of {@link CredentialsMapping} that match the location designated by the given {@link FileURL} * and which can be used to authenticate. The location is compared against all known credentials, both volatile and * persistent. * *

The returned credentials will match the given URL's scheme and host, but the path may differ so there is * no guarantee that the credentials will successfully authenticate the location. * *

The best match (credentials with the 'closest' path to the provided location's path) is returned at the first * position ([0]), if there is at least one matching credentials instance. The returned array can be empty * (zero length) but never null. * * @param location the location to be compared against known credentials instances, both volatile and persistent * @return an array of CredentialsMapping matching the given URL's scheme and host, best match at the first position */ public static CredentialsMapping[] getMatchingCredentials(FileURL location) { // Retrieve matches List matchesV = getMatchingCredentialsV(location); // Transform vector into an array CredentialsMapping[] matches = new CredentialsMapping[matchesV.size()]; matchesV.toArray(matches); return matches; } /** * Returns an {@link Authenticator} that authenticates {@link FileURL} instances using the credentials and realm * properties stored by {@link CredentialsManager}. * * @return an {@link Authenticator} that authenticates {@link FileURL} instances using the credentials and realm * properties stored by {@link CredentialsManager}. */ public static Authenticator getAuthenticator() { return AUTHENTICATOR; } /** * Returns a Vector of CredentialsMapping matching the given URL's scheme and host, best match at the first position. * The returned Vector may be empty but never null. * * @param location the location to be compared against known credentials instances, both volatile and persistent * @return a Vector of CredentialsMapping matching the given URL's scheme and host, best match at the first position */ private static List getMatchingCredentialsV(FileURL location) { List matchesV = new ArrayList<>(); findMatches(location, volatileCredentialMappings, matchesV); findMatches(location, persistentCredentialMappings, matchesV); // Find the best match and move it at the first position in the vector int bestMatchIndex = getBestMatchIndex(location, matchesV); if (bestMatchIndex >= 0) { matchesV.addFirst(matchesV.remove(bestMatchIndex)); } return matchesV; } /** * Adds the given credentials to the list of known credentials. * *

Depending on value returned by {@link CredentialsMapping#isPersistent()}, the credentials will either be stored * in the volatile credentials list or the persistent one. Any existing credentials mapped to the same realm * will be replaced by the provided ones. * *

This method should be called when new credentials have been entered by the user, after they have been validated * by the application (i.e. access was granted to the location). * * @param credentialsMapping credentials to be added to the list of known credentials */ public static void addCredentials(CredentialsMapping credentialsMapping) { // Do not add if the credentials are empty if (credentialsMapping.getCredentials().isEmpty()) { return; } boolean persist = credentialsMapping.isPersistent(); getLogger().trace("called, realm="+ credentialsMapping.getRealm()+" isPersistent="+ credentialsMapping.isPersistent()); getLogger().trace("before, persistentCredentials="+ persistentCredentialMappings); getLogger().trace("before, volatileCredentials="+ volatileCredentialMappings); if (persist) { replaceListElement(persistentCredentialMappings, credentialsMapping); volatileCredentialMappings.remove(credentialsMapping); } else { replaceListElement(volatileCredentialMappings, credentialsMapping); persistentCredentialMappings.removeElement(credentialsMapping); } getLogger().trace("after, persistentCredentials="+ persistentCredentialMappings); getLogger().trace("after, volatileCredentials="+ volatileCredentialMappings); } /** * Use the credentials and realm properties of the specified CredentialsMapping to authenticate the * given {@link FileURL}. *

Any credentials contained by the FileURL will be lost and replaced with the new ones. * If properties with the same key are defined both in the realm and the given FileURL, the ones from the FileURL * will be preserved. * * @param location the FileURL to authenticate * @param credentialsMapping the credentials to use to authenticate the given FileURL */ public static void authenticate(FileURL location, CredentialsMapping credentialsMapping) { location.setCredentials(credentialsMapping.getCredentials()); FileURL realm = credentialsMapping.getRealm(); Set propertyKeys = realm.getPropertyNames(); for (String key : propertyKeys) { if (location.getProperty(key) == null) { location.setProperty(key, realm.getProperty(key)); } } } /** * Looks for the best set of credentials matching the given location (if any) and use it to authenticate the * URL by calling {@link #authenticate(com.mucommander.commons.file.FileURL, CredentialsMapping)}. * Returns true if a set of credentials was found and used to authenticate the URL, false * otherwise. * *

Credentials are first looked for using {@link #getMatchingCredentials(com.mucommander.commons.file.FileURL)}. * If there is no match, guest credentials are retrieved from the URL and used (if any). * * @param location the FileURL to authenticate */ private static void authenticateImplicit(FileURL location) { getLogger().trace("called, fileURL="+ location +" containsCredentials="+ location.containsCredentials()); CredentialsMapping[] creds = getMatchingCredentials(location); if (creds.length > 0) { authenticate(location, creds[0]); } else { Credentials guestCredentials = location.getGuestCredentials(); if(guestCredentials!=null) { authenticate(location, new CredentialsMapping(guestCredentials, location.getRealm(), false)); } } } /** * Looks for credentials matching the specified location in the given credentials Vector and adds them to the given * matches Vector. * * @param location the location to find matching credentials for * @param credentials the Vector containing the CredentialsMapping instances to compare to the given location * @param matches the Vector where matching CredentialsMapping instances will be added */ private static void findMatches(FileURL location, List credentials, List matches) { for (CredentialsMapping tempCredentialsMapping: credentials) { FileURL tempRealm = tempCredentialsMapping.getRealm(); if (location.schemeEquals(tempRealm) && location.portEquals(tempRealm) && location.hostEquals(tempRealm)) { matches.add(tempCredentialsMapping); } } getLogger().trace("returning matches="+matches); } /** * Finds are returns the index of the CredentialsMapping instance that best matches the given location * amongst the provided matching CredentialsMapping Vector, or -1 if the matches Vector is empty. * *

* The path of each matching CredentialsMapping' location is compared to the provided location's path: the more * folder parts match, the better. If both paths are equal, then the CredentialsMapping index is returned (perfect match). * * @param location the location to be compared against CredentialsMapping matches * @param matches CredentialsMapping instances matching the given location * @return the CredentialsMapping instance that best matches the given location, -1 if the given matches Vector is empty. */ private static int getBestMatchIndex(FileURL location, List matches) { if (matches.isEmpty()) { return -1; } // Splits the provided location's path into an array of folder tokens (e.g. "/home/maxence" -> ["home","maxence"]) String path = location.getPath(); List pathTokensV = new ArrayList<>(); StringTokenizer st = new StringTokenizer(path, "/\\"); while(st.hasMoreTokens()) { pathTokensV.add(st.nextToken()); } int nbTokens = pathTokensV.size(); String[] pathTokens = new String[nbTokens]; pathTokensV.toArray(pathTokens); CredentialsMapping tempCredentialsMapping; FileURL tempURL; String tempPath; int nbMatchingToken; int maxTokens = 0; int bestMatchIndex = 0; // Compares the location's path against all the one of all CredentialsMapping instances int nbMatches = matches.size(); for (int i=0; i nbMatchingToken = 1 // /var/log and /usr -> nbMatchingToken = 0 st = new StringTokenizer(tempPath, "/\\"); nbMatchingToken = 0; for (int j=0; jmaxTokens) { // We just found a better match maxTokens = nbMatchingToken; bestMatchIndex = i; } } getLogger().trace("returning bestMatchIndex="+bestMatchIndex); return bestMatchIndex; } /** * Replaces any object that's equal to the given one in the Vector, preserving its position. If the * vector contains no such object, it is added to the end of the vector. * * @param list the List to replace/add the object to * @param o the object to replace/add */ private static void replaceListElement(List list, CredentialsMapping o) { int index = list.indexOf(o); if (index < 0) { list.add(o); } else { list.set(index, o); } } /** * Returns the list of known volatile {@link CredentialsMapping}, stored in a Vector. *

* The returned Vector instance is the one actually used by CredentialsManager, so use it with caution. * * @return the list of known volatile {@link CredentialsMapping}. */ public static List getVolatileCredentialMappings() { return volatileCredentialMappings; } /** * Returns the list of known persistent {@link CredentialsMapping}, stored in an {@link AlteredVector}. *

* Any changes made to the Vector will be detected and will yield to writing the credentials file when * {@link #writeCredentials(boolean)} is called with false. * * @return the list of known persistent {@link CredentialsMapping}. */ public static AlteredVector getPersistentCredentialMappings() { return persistentCredentialMappings; } /////////////////// // Inner classes // /////////////////// /** * An {@link Authenticator} implementation that uses {@link CredentialsManager#authenticateImplicit(FileURL)} to * authenticate the specified {@link FileURL} instances. * * @author Maxence Bernard */ private static class CredentialsManagerAuthenticator implements Authenticator { public void authenticate(FileURL fileURL) { CredentialsManager.authenticateImplicit(fileURL); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(CredentialsManager.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/auth/CredentialsMapping.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.auth; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; /** * CredentialsMapping associates a {@link Credentials} instance with a 'realm' , that is the location to a server. * It also adds the notion of persistence, allowing to specify whether the credentials should be saved to disk when the * application quits and restored next time the application starts. * * @see CredentialsManager * @author Maxence Bernard */ public final class CredentialsMapping { /** User credentials */ private final Credentials credentials; /** The location credentials are associated with */ private final FileURL realm; /** Should these credentials be saved to disk ? */ private final boolean isPersistent; /** * Creates a new CredentialsMapping instance that associates the specified credentials with the given location. * * @param credentials user credentials * @param realm the location to associate the credentials with * @param isPersistent if true, indicates to CredentialsManager that the credentials should be saved when the * application terminates. */ public CredentialsMapping(Credentials credentials, FileURL realm, boolean isPersistent) { this.credentials = credentials; this.isPersistent = isPersistent; this.realm = realm.getRealm(); } /** * Returns the credentials. * * @return the credentials */ public Credentials getCredentials() { return credentials; } /** * Returns the location associated with the credentials. *

* Note: the returned {@link FileURL} does not contain any credentials. * * @return the location associated with the credentials. */ public FileURL getRealm() { return realm; } /** * Returns true if these credentials should be saved when the application terminates. * * @return true if these credentials should be saved when the application terminates, false otherwise. */ public boolean isPersistent() { return isPersistent; } /** * Returns true if the given Object is a {@link com.mucommander.auth.CredentialsMapping} instance * whose credentials and realm are equals to those of this instance. * * @param o the Object to test for equality * @return true if both CredentialsMapping instances are equal */ @Override public boolean equals(Object o) { if (!(o instanceof CredentialsMapping cm)) { // Note: CredentialsMapping is final, no need to test classes return false; } return cm.credentials.equals(this.credentials, false) && cm.realm.equals(this.realm, false, true); } @Override public String toString() { return credentials.toString()+" "+realm.toString(false); } } ================================================ FILE: src/main/java/com/mucommander/auth/CredentialsParser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.auth; import com.mucommander.bookmark.XORCipher; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import com.mucommander.io.backup.BackupInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.util.Hashtable; import java.util.Map; import javax.xml.parsers.SAXParserFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; /** * This class takes care of parsing the credentials XML file and adding parsed {@link CredentialsMapping} instances * to {@link CredentialsManager}. * * @author Maxence Bernard * @see CredentialsWriter */ class CredentialsParser extends DefaultHandler implements CredentialsConstants { private static Logger logger; // Variables used for XML parsing private FileURL url; private Map urlProperties; private String login; private String password; private StringBuilder characters; /** muCommander version that was used to write the credentials file */ private String version; /** Contains the encryption method used to encrypt/decrypt passwords */ private String encryptionMethod; /** * Creates a new CredentialsParser. */ public CredentialsParser() { } /** * Parses the given XML credentials file. Should only be called by CredentialsManager. */ void parse(AbstractFile file) throws Exception { characters = new StringBuilder(); try (InputStream in = new BackupInputStream(file)) { SAXParserFactory.newInstance().newSAXParser().parse(in, this); } } /** * Returns the muCommander version that was used to write the credentials file, null if it is unknown. *

* Note: the version attribute was introduced in muCommander 0.8.4. * * @return the muCommander version that was used to write the credentials file, null if it is unknown. */ public String getVersion() { return version; } /////////////////////////////////// // ContentHandler implementation // /////////////////////////////////// @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { characters.setLength(0); switch (qName) { case ELEMENT_CREDENTIALS: // Reset parsing variables url = null; urlProperties = null; login = null; password = null; break; // Property element (properties will be set when credentials element ends case ELEMENT_PROPERTY: if (urlProperties == null) urlProperties = new Hashtable<>(); urlProperties.put(attributes.getValue(ATTRIBUTE_NAME), attributes.getValue(ATTRIBUTE_VALUE)); break; // Root element, the 'encryption' attribute specifies which encoding was used to encrypt passwords case ELEMENT_ROOT: encryptionMethod = attributes.getValue("encryption"); version = attributes.getValue(ATTRIBUTE_VERSION); break; } } @Override public void endElement(String uri, String localName, String qName) { switch (qName) { case ELEMENT_CREDENTIALS: if (url == null || login == null || password == null) { getLogger().info("Missing value, credentials ignored: url=" + url + " login=" + login); return; } // Copy properties into FileURL instance (if any) if (urlProperties != null) { for (String key : urlProperties.keySet()) url.setProperty(key, urlProperties.get(key)); } // Decrypt password try { password = XORCipher.decryptXORBase64(password); } catch (IOException e) { getLogger().info("Password could not be decrypted: " + password + ", credentials will be ignored"); return; } // Add credentials to persistent credentials list CredentialsManager.getPersistentCredentialMappings().add(new CredentialsMapping(new Credentials(login, password), url, true)); break; case ELEMENT_URL: try { url = FileURL.getFileURL(characters.toString().trim()); } catch (MalformedURLException e) { getLogger().info("Malformed URL: " + characters + ", location will be ignored"); } break; case ELEMENT_LOGIN: login = characters.toString().trim(); break; case ELEMENT_PASSWORD: password = characters.toString().trim(); break; } } @Override public void characters(char[] ch, int offset, int length) { characters.append(ch, offset, length); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(CredentialsParser.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/auth/CredentialsWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.auth; import com.mucommander.RuntimeConstants; import com.mucommander.bookmark.XORCipher; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.Set; /** * This class provides a method to write persistent credentials contained by {@link CredentialsManager} to an XML file. * * @author Maxence Bernard * @see CredentialsParser */ public class CredentialsWriter implements CredentialsConstants { /** * Writes the credentials XML file in the user's preferences folder. * This method should only be called by {@link CredentialsManager}. */ static void write(OutputStream stream) throws IOException { XmlWriter out = new XmlWriter(stream); // Root element, add the encryption method used XmlAttributes attributes = new XmlAttributes(); attributes.add(ATTRIBUTE_ENCRYPTION, WEAK_ENCRYPTION_METHOD); // Version the file attributes.add(ATTRIBUTE_VERSION, RuntimeConstants.VERSION); out.startElement(ELEMENT_ROOT, attributes); out.println(); Iterator iterator = CredentialsManager.getPersistentCredentialMappings().iterator(); CredentialsMapping credentialsMapping; FileURL realm; Set propertyKeys; while(iterator.hasNext()) { credentialsMapping = iterator.next(); realm = credentialsMapping.getRealm(); // Start credentials element out.startElement(ELEMENT_CREDENTIALS); out.println(); // Write URL out.startElement(ELEMENT_URL); out.writeCData(realm.toString(false)); out.endElement(ELEMENT_URL); Credentials credentials = credentialsMapping.getCredentials(); // Write login out.startElement(ELEMENT_LOGIN); out.writeCData(credentials.getLogin()); out.endElement(ELEMENT_LOGIN); // Write password (XOR encrypted) out.startElement(ELEMENT_PASSWORD); out.writeCData(XORCipher.encryptXORBase64(credentials.getPassword())); out.endElement(ELEMENT_PASSWORD); // Write properties, each property is stored in a separate 'property' element propertyKeys = realm.getPropertyNames(); for (String name : propertyKeys) { attributes = new XmlAttributes(); attributes.add(ATTRIBUTE_NAME, name); attributes.add(ATTRIBUTE_VALUE, realm.getProperty(name)); out.startElement(ELEMENT_PROPERTY, attributes); out.endElement(ELEMENT_PROPERTY); } // End credentials element out.endElement(ELEMENT_CREDENTIALS); } // End root element out.endElement(ELEMENT_ROOT); } } ================================================ FILE: src/main/java/com/mucommander/auth/package.html ================================================ API for dealing with authentication and user credentials. ================================================ FILE: src/main/java/com/mucommander/bonjour/BonjourDirectory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bonjour; import java.io.IOException; import java.net.Inet6Address; import java.net.MalformedURLException; import java.util.List; import java.util.Vector; import javax.jmdns.JmDNS; import javax.jmdns.ServiceEvent; import javax.jmdns.ServiceInfo; import javax.jmdns.ServiceListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; /** * Collects and maintains a list of available Bonjour/Zeroconf services using the JmDNS library. * Newly discovered services are added to the list and removed as they become unavailable. * *

Use {@link #getServices()} to get a list of currently available Bonjour services. * * @author Maxence Bernard * @see BonjourMenu */ public class BonjourDirectory implements ServiceListener { private static Logger logger; /** Singleton instance held to prevent garbage collection and also used for synchronization */ private final static BonjourDirectory instance = new BonjourDirectory(); /** Does all the hard work */ private static JmDNS jmDNS; /** List of discovered and currently active Bonjour services */ private static final List services = new Vector<>(); /** Known Bonjour/Zeroconf service types and their corresponding protocol */ private final static String[][] KNOWN_SERVICE_TYPES = { {"_http._tcp.local.", FileProtocols.HTTP}, {"_ftp._tcp.local.", FileProtocols.FTP}, {"_ssh._tcp.local.", FileProtocols.SFTP}, {"_smb._tcp.local.", FileProtocols.SMB} }; /** Number of milliseconds to wait for service info resolution before giving up */ private final static int SERVICE_RESOLUTION_TIMEOUT = 10000; /** * No-arg contructor made private so that only one instance can exist. */ private BonjourDirectory() { } /** * Enables/disables Bonjour services discovery. If currently active and false is specified, current services * will be lost and {@link #getServices()} will return an empty array. If currently inactive and true is specified, * services discovery will be immediately started but it may take a while (a few seconds at least) to * collect services. * @param enabled whether Bonjour services discovery should be enabled. */ public static void setActive(boolean enabled) { if (enabled && jmDNS==null) { // Start JmDNS try { jmDNS = JmDNS.create(); // Listens to service events for known service types for (String[] KNOWN_SERVICE_TYPE : KNOWN_SERVICE_TYPES) jmDNS.addServiceListener(KNOWN_SERVICE_TYPE[0], instance); } catch(IOException e) { getLogger().warn("Could not instantiate jmDNS, Bonjour not enabled", e); } } else if(!enabled && jmDNS != null) { // Shutdown JmDNS try { jmDNS.close(); if (1==0) throw new IOException(); // FIXME looks like that different versions of Bonjour library have different Bonjour.close() method - with declared IOException and without it } catch (IOException e) { throw new RuntimeException(e); } services.clear(); jmDNS = null; } } /** * Returns true if Bonjour services discovery is currently running. * @return true if Bonjour services discovery is currently running, false otherwise. */ public static boolean isActive() { return jmDNS != null; } /** * Returns all currently available Bonjour services. The returned array may be empty but never null. * If BonjourDirectory is not currently active ({@link #isActive()}, an empty array will be returned. * @return all currently available Bonjour services */ public static BonjourService[] getServices() { BonjourService[] servicesArray = new BonjourService[services.size()]; services.toArray(servicesArray); return servicesArray; } /** * Wraps a Bonjour service into a {@link BonjourService} object and returns it. Returns null if * the service type doesn't correspond to any of the supported protocols, or if the service URL is malformed. * * @param serviceInfo the ServiceInfo to wrap into a BonjourService * @return a BonjourService instance corresponding to the given ServiceInfo */ private static BonjourService createBonjourService(ServiceInfo serviceInfo) { try { String type = serviceInfo.getType(); // Looks for the file protocol corresponding to the service type for (String[] KNOWN_SERVICE_TYPE : KNOWN_SERVICE_TYPES) { if (KNOWN_SERVICE_TYPE[0].equals(type)) { return new BonjourService(serviceInfo.getName(), FileURL.getFileURL(serviceInfo.getURL(KNOWN_SERVICE_TYPE[1])), serviceInfo.getQualifiedName()); } } } catch (MalformedURLException e) { // Null will be returned } return null; } //////////////////////////////////// // ServiceListener implementation // //////////////////////////////////// public void serviceAdded(final ServiceEvent serviceEvent) { getLogger().trace("name="+serviceEvent.getName()+" type="+serviceEvent.getType()); // Ignore if Bonjour has been disabled if (!isActive()) { return; } // Resolve service info in a separate thread, serviceResolved() will be called once service info has been resolved. // Not spawning a thread often leads to service info loss (serviceResolved() not called). new Thread(() -> jmDNS.requestServiceInfo(serviceEvent.getType(), serviceEvent.getName(), SERVICE_RESOLUTION_TIMEOUT)).start(); } public void serviceResolved(ServiceEvent serviceEvent) { getLogger().trace("name="+serviceEvent.getName()+" type="+serviceEvent.getType()+" info="+serviceEvent.getInfo()); // Ignore if Bonjour has been disabled if (!isActive()) return; // Creates a new BonjourService corresponding to the new service and add it to the list of current Bonjour services ServiceInfo serviceInfo = serviceEvent.getInfo(); if(serviceInfo!=null) { if(serviceInfo.getInetAddress() instanceof Inet6Address) { // IPv6 addresses not supported at this time + they seem not to be correctly handled by ServiceInfo getLogger().debug("ignoring IPv6 service"); return; } BonjourService bs = createBonjourService(serviceInfo); // Synchronized to properly handle duplicate calls synchronized(instance) { if(bs!=null && !services.contains(bs)) { getLogger().debug("BonjourService "+bs+" added"); services.add(bs); } } } } public void serviceRemoved(ServiceEvent serviceEvent) { getLogger().trace("name="+serviceEvent.getName()+" type="+serviceEvent.getType()); // Ignore if Bonjour has been disabled if(!isActive()) return; // Looks for an existing BonjourService instance corresponding to the service being removed and removes it from // the list of current Bonjour services. // ServiceInfo should be available in JmDNS's cache. ServiceInfo serviceInfo = jmDNS.getServiceInfo(serviceEvent.getType(), serviceEvent.getName()); if (serviceInfo != null) { if(serviceInfo.getInetAddress() instanceof Inet6Address) { // IPv6 addresses not supported at this time + they seem not to be correctly handled by ServiceInfo getLogger().debug("ignoring IPv6 service"); return; } BonjourService bs = createBonjourService(serviceInfo); // Synchronized to properly handle duplicate calls synchronized(instance) { // Note: BonjourService#equals() uses the service's fully qualified name as the discriminator. if (bs != null && services.contains(bs)) { getLogger().debug("BonjourService "+bs+" removed"); services.remove(bs); } } } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(BonjourDirectory.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/bonjour/BonjourMenu.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bonjour; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.icon.IconManager; import javax.swing.*; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; /** * An abstract JMenu that contains an item for each Bonjour service available * (as returned {@link BonjourDirectory#getServices()} displaying the Bonjour service's name). When an item is clicked, * the action returned by {@link #getMenuItemAction(BonjourService)} is returned. * *

Note: the items list is refreshed each time the menu is selected. In other words, a new instance of BonjourMenu * does not have to be created in order to see new Bonjour services. * * @author Maxence Bernard */ public abstract class BonjourMenu extends JMenu implements MenuListener { /** * Creates a new instance of BonjourMenu. */ public BonjourMenu() { super(Translator.get("bonjour.bonjour_services")); setIcon(IconManager.getIcon(IconManager.IconSet.FILE, "bonjour.png")); // Menu items will be added when menu gets selected addMenuListener(this); } /** * Returns the action to perform for the given {@link BonjourService}. This method is called for every * BonjourService available when this menu is selected. * * @param bs the BonjourService * @return the action to perform for the given BonjourService */ public abstract TcAction getMenuItemAction(BonjourService bs); ///////////////////////////////// // MenuListener implementation // ///////////////////////////////// @Override public void menuSelected(MenuEvent menuEvent) { // Remove previous menu items (if any) removeAll(); if (!BonjourDirectory.isActive()) { // Inform that Bonjour support has been disabled add(new JMenuItem(Translator.get("bonjour.bonjour_disabled"))).setEnabled(false); return; } BonjourService[] services = BonjourDirectory.getServices(); if (services.length > 0) { // Add a menu item for each Bonjour service. // When clicked, the corresponding URL will be opened in the active table. MnemonicHelper mnemonicHelper = new MnemonicHelper(); for (BonjourService service : services) { JMenuItem menuItem = new JMenuItem(getMenuItemAction(service)); menuItem.setMnemonic(mnemonicHelper.getMnemonic(menuItem.getText())); add(menuItem); } } else { // Inform that no service have been discovered add(new JMenuItem(Translator.get("bonjour.no_service_discovered"))).setEnabled(false); } } @Override public void menuDeselected(MenuEvent menuEvent) { } @Override public void menuCanceled(MenuEvent menuEvent) { } } ================================================ FILE: src/main/java/com/mucommander/bonjour/BonjourService.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bonjour; import com.mucommander.commons.file.FileURL; import lombok.Getter; /** * A simple container for a Bonjour service described by a name and URL. * * @author Maxence Bernard */ public class BonjourService { /** the unqualified name of the service, e.g. 'foobar' * -- GETTER -- * Returns the unqualified name of this service, e.g. 'foobar'. */ @Getter private final String name; /** the url pointing to the service's location */ private final FileURL url; /** the fully qualified name of the service, e.g. 'foobar._http._tcp.local' * -- GETTER -- * Returns the fully qualified name of this service, e.g. 'foobar._http._tcp.local' */ @Getter private final String fullyQualifiedName; /** * Creates a new BonjourService instance using the given name and URL. * * @param name the unqualified name of the service, e.g. 'foobar' * @param url the url pointing to the service's location * @param fullyQualifiedName the fully qualified name of the service, e.g. 'foobar._http._tcp.local' */ public BonjourService(String name, FileURL url, String fullyQualifiedName) { this.name = name; this.url = url; this.fullyQualifiedName = fullyQualifiedName; } /** * Returns the name appended with the URL's scheme. * * @return the name appended with the URL's scheme. */ public String getNameWithProtocol() { return name+" ["+url.getScheme().toUpperCase()+"]"; } /** * Returns the location of this service. * * @return the location of this service. */ public FileURL getURL() { return url; } /** * Returns true if the given Object is a BonjourService instance with the same fully qualified name. */ @Override public boolean equals(Object o) { return o instanceof BonjourService && fullyQualifiedName.equals(((BonjourService) o).fullyQualifiedName); } /** * Returns a String representation of this BonjourService in the form name / url. */ @Override public String toString() { return name + " / " + url.toString(false); } } ================================================ FILE: src/main/java/com/mucommander/bonjour/package.html ================================================ Wrapper for the JmDNS library, provides Bonjour/Zeroconf support. ================================================ FILE: src/main/java/com/mucommander/bookmark/Bookmark.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; /** * Represents a bookmark. *

Bookmarks are simple name/location pairs: *

    *
  • The name is a String describing the bookmark.
  • *
  • * The location should designate a path or file URL. The designated location may not exist or may not even be * a valid path or URL, so it is up to classes that call {@link #getLocation()} to deal with it appropriately. *
  • *
* * @author Maxence Bernard */ public class Bookmark implements Cloneable { private String name; private String location; private String parent; /** * Creates a new Bookmark using the given name and location. * * @param name name given to this bookmark * @param location location (path or URL) this bookmark points to */ public Bookmark(String name, String location, String parent) { // Use setters to checks for null values setName(name); setLocation(location); setParent(parent); } /** * Returns this bookmark's name. * @return this bookmark's name. * @see #setName(String) */ public String getName() { return name; } /** * Changes this bookmark's name to the given one and fires an event to registered {@link BookmarkListener} * instances. * @param newName bookmark's new name. * @see #getName() */ public void setName(String newName) { if (newName == null) { newName = ""; } if (!newName.equals(this.name)) { this.name = newName; // Notify registered listeners of the change BookmarkManager.fireBookmarksChanged(); } } /** * Returns this bookmark's location which should normally designate a path or file URL, but which isn't * necessarily valid nor exists. * @return this bookmark's location. * @see #setLocation(String) */ public String getLocation() { return location; } /** * Changes this bookmark's location to the given one and fires an event to registered {@link BookmarkListener} * instances. * @param newLocation bookmark's new location. * @see #getLocation() */ public void setLocation(String newLocation) { // Replace null values by empty strings if (newLocation == null) { newLocation = ""; } if (!newLocation.equals(this.location)) { this.location = newLocation; // Notify registered listeners of the change BookmarkManager.fireBookmarksChanged(); } } public String getParent() { return parent; } public void setParent(String parent) { if (parent != null && parent.trim().isEmpty()) { parent = null; } this.parent = parent; } /** * Returns a clone of this bookmark. */ @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } /** * Returns the bookmark's name. */ public String toString() { if (parent != null) { return parent + " -> " + name; } return name; } public boolean equals(Object object) { if (!(object instanceof Bookmark bookmark)) { return false; } return bookmark.getName().equals(name); } } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkBuilder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; /** * Implementations of this interface are used to build bookmark sets. * @author Nicolas Rinaudo */ public interface BookmarkBuilder { /** * Notifies the builder that the bookmark list is starting. * @throws BookmarkException if an error occurs. */ void startBookmarks() throws BookmarkException; /** * Notifies the builder of a new bookmark in the list. * @param name bookmark's name. * @param location bookmark's location. * @param parent bookmark's parent name. * @throws BookmarkException if an error occurs. */ void addBookmark(String name, String location, String parent) throws BookmarkException; /** * Notifies the builder that the bookmark list is finished. * @throws BookmarkException if an error occurs. */ void endBookmarks() throws BookmarkException; } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; /** * Defines bookmarks XML file structure. * @author Nicolas Rinaudo */ interface BookmarkConstants { /** Root element */ String ELEMENT_ROOT = "bookmarks"; /** Name of the root element's attribute containing the muCommander version that was used to create the bookmarks file */ String ATTRIBUTE_VERSION = "version"; /** Element describing one of the bookmarks in the list */ String ELEMENT_BOOKMARK = "bookmark"; /** Bookmark name */ String ELEMENT_NAME = "name"; /** Bookmark location */ String ELEMENT_LOCATION = "location"; /** Bookmark parent name */ String ELEMENT_PARENT = "parent"; // /** Bookmark URL: was used up until 0.8 beta3 nightly builds and replaced by 'location' element. Kept // * for upward compatibility */ // String ELEMENT_URL = "url"; } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; /** * Exception thrown when bookmark related errors occur. * @author Nicolas Rinaudo */ public class BookmarkException extends Exception { /** * Creates a new exception with the specified message. * @param message exception's message. */ public BookmarkException(String message) {super(message);} /** * Creates a new exception wrapping the specified error. * @param cause root cause of the new exception. */ public BookmarkException(Throwable cause) {super(cause);} /** * Creates a new exception with the specified message and cause. * @param message exception's message. * @param cause root cause of the new exception. */ public BookmarkException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; /** * Interface to be implemented by classes that wish to be notified when changes are made to the bookmarks list. * Those classes need to be registered to receive those events, this can be done by calling * {@link BookmarkManager#addBookmarkListener(BookmarkListener)}. * * @author Maxence Bernard */ public interface BookmarkListener { /** * This method is invoked when a bookmark has been added, removed or modified. */ void bookmarksChanged(); } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; import com.mucommander.PlatformManager; import com.mucommander.bookmark.file.BookmarkProtocolProvider; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.commons.collections.VectorChangeListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.io.backup.BackupOutputStream; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.WeakHashMap; /** * This class manages the bookmark list and its parsing and storage as an XML file. *

* It monitors any changes made to the bookmarks and when changes are made, fires change events to registered * listeners. * * @author Maxence Bernard, Nicolas Rinaudo */ public class BookmarkManager implements VectorChangeListener { /** Whether we're currently loading the bookmarks or not. */ private static boolean isLoading = false; /** Bookmarks file location */ private static AbstractFile bookmarksFile; /** Default bookmarks file name */ private static final String DEFAULT_BOOKMARKS_FILE_NAME = "bookmarks.xml"; /** Bookmark instances */ private static final AlteredVector bookmarks = new AlteredVector<>(); /** Contains all registered bookmark listeners, stored as weak references */ private static final WeakHashMap listeners = new WeakHashMap<>(); /** Specifies whether bookmark events should be fired when a change to the bookmarks is detected */ private static boolean fireEvents = true; /** True when changes were made after the bookmarks file was last saved */ private static boolean saveNeeded; /** Last bookmark change timestamp */ private static long lastBookmarkChangeTime; /** Last event pause timestamp */ private static long lastEventPauseTime; /** create a singleton instance, needs to be referenced so that it's not garbage collected (AlteredVector * stores VectorChangeListener as weak references) */ private static final BookmarkManager singleton = new BookmarkManager(); /** Value of bookmark's name that make the bookmark treated as a separator */ public static final String BOOKMARKS_SEPARATOR = "-"; static { // Listen to changes made to the bookmarks vector bookmarks.addVectorChangeListener(singleton); } /** * Prevents instanciation of BookmarkManager. */ private BookmarkManager() {} // - Bookmark building ----------------------------------------------------- // ------------------------------------------------------------------------- /** * Passes messages about all known bookmarks to the specified builder. * @param builder where to send bookmark building messages. * @throws BookmarkException if an error occurs. */ private static synchronized void buildBookmarks(BookmarkBuilder builder) throws BookmarkException { builder.startBookmarks(); for (Bookmark bookmark : bookmarks) { builder.addBookmark(bookmark.getName(), bookmark.getLocation(), bookmark.getParent()); } builder.endBookmarks(); } // - Bookmark file access -------------------------------------------------- // ------------------------------------------------------------------------- /** * Returns the path to the bookmark file. *

* If it hasn't been changed through a call to {@link #setBookmarksFile(String)}, * this method will return the default, system dependant bookmarks file. * * @return the path to the bookmark file. * @see #setBookmarksFile(String) * @throws IOException if there was a problem locating the default bookmarks file. */ private static synchronized AbstractFile getBookmarksFile() throws IOException { if (bookmarksFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_BOOKMARKS_FILE_NAME); } return bookmarksFile; } /** * Sets the path to the bookmarks file. *

* This is a convenience method and is strictly equivalent to calling setBookmarksFile(FileFactory.getFile(file)). * * @param path path to the bookmarks file * @exception FileNotFoundException if path is not accessible. * @see #getBookmarksFile() */ public static void setBookmarksFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setBookmarksFile(new File(path)); } else { setBookmarksFile(file); } } /** * Sets the path to the bookmarks file. *

* This is a convenience method and is strictly equivalent to calling setBookmarksFile(FileFactory.getFile(file.getAbsolutePath())). * * @param file path to the bookmarks file * @exception FileNotFoundException if path is not accessible. * @see #getBookmarksFile() */ private static void setBookmarksFile(File file) throws FileNotFoundException { setBookmarksFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the bookmarks file. * @param file path to the bookmarks file * @exception FileNotFoundException if path is not accessible. * @see #getBookmarksFile() */ private static synchronized void setBookmarksFile(AbstractFile file) throws FileNotFoundException { if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } bookmarksFile = file; } // - Bookmarks loading ----------------------------------------------------- // ------------------------------------------------------------------------- /** * Loads all available bookmarks. * @throws Exception if an error occurs. */ public static synchronized void loadBookmarks() throws Exception { // Parse the bookmarks file isLoading = true; try (InputStream in = new BackupInputStream(getBookmarksFile())) { readBookmarks(in, new Loader()); isLoading = false; } catch (IOException e) { isLoading = false; throw e; } } /** * Reads bookmarks from the specified InputStream. * @param in where to read bookmarks from. * @throws Exception if an error occurs. */ public static void readBookmarks(InputStream in) throws Exception { readBookmarks(in, new Loader()); } /** * Reads bookmarks from the specified InputStream and passes messages to the specified {@link BookmarkBuilder}. * @param in where to read bookmarks from. * @param builder where to send builing messages to. * @throws Exception if an error occurs. */ public static synchronized void readBookmarks(InputStream in, BookmarkBuilder builder) throws Exception { new BookmarkParser().parse(in, builder); } // - Bookmarks writing ----------------------------------------------------- // ------------------------------------------------------------------------- /** * Returns a {@link BookmarkBuilder} that will write all building messages as XML to the specified output stream. * @param out where to write the bookmarks' XML content. * @return a {@link BookmarkBuilder} that will write all building messages as XML to the specified output stream. * @throws IOException if an IO related error occurs. */ public static BookmarkBuilder getBookmarkWriter(OutputStream out) throws IOException { return new BookmarkWriter(out); } /** * Writes all known bookmarks to the bookmark {@link #getBookmarksFile() file}. * @param forceWrite if false, the bookmarks file will be written only if changes were made to bookmarks since * last write, if true the file will always be written * @throws IOException if an I/O error occurs. * @throws BookmarkException if an error occurs. */ public static synchronized void writeBookmarks(boolean forceWrite) throws IOException, BookmarkException { // Write bookmarks file only if changes were made to the bookmarks since last write, or if write is forced. if (!forceWrite && !saveNeeded) { return; } try (OutputStream out = new BackupOutputStream(getBookmarksFile())) { buildBookmarks(getBookmarkWriter(out)); saveNeeded = false; } } // - Bookmarks access ------------------------------------------------------ // ------------------------------------------------------------------------- /** * Returns an {@link AlteredVector} that contains all bookmarks. * *

Important: the returned Vector should not directly be used to * add or remove bookmarks, doing so won't trigger any event to registered bookmark listeners. * However, it is safe to modify bookmarks individually, events will be properly fired. * @return an {@link AlteredVector} that contains all bookmarks. */ public static synchronized AlteredVector getBookmarks() { return bookmarks; } /** * Deletes the specified bookmark. * @param bookmark bookmark to delete from the list. */ public static synchronized void removeBookmark(Bookmark bookmark) { bookmarks.remove(bookmark); } /** * Convenience method that looks for a Bookmark with the given name (case ignored) and returns it, * or null if none was found. If several bookmarks have the given name, the first one is returned. * * @param name the bookmark's name * @return a Bookmark instance with the given name, null if none was found */ public static synchronized Bookmark getBookmark(String name) { for (Bookmark b : bookmarks) { if (b.getName().equalsIgnoreCase(name)) { return b; } } return null; } public static List getParentBookmarks() { List result = new ArrayList<>(); for (Bookmark b : bookmarks) { if (b.getLocation().isEmpty()) { result.add(b); } } return result; } /** * Convenience method that adds a bookmark to the bookmark list. * * @param b the Bookmark instance to add to the bookmark list. */ public static synchronized void addBookmark(Bookmark b) { bookmarks.add(b); } /** * Check if a given URL represents a bookmark. * * @param fileURL the URL to examine * @return true if the given URL represents a bookmark, false otherwise */ public static boolean isBookmark(FileURL fileURL) { return fileURL != null && BookmarkProtocolProvider.BOOKMARK.equals(fileURL.getScheme()); } /** * Check if a given file represents a bookmark. * * @param file the URL to examine * @return true if the given file represents a bookmark, false otherwise */ public static boolean isBookmark(AbstractFile file) { return file != null && BookmarkProtocolProvider.BOOKMARK.equals(file.getURL().getScheme()); } // - Listeners ------------------------------------------------------------- // ------------------------------------------------------------------------- /** * Adds the specified BookmarkListener to the list of registered listeners. * *

Listeners are stored as weak references so {@link #removeBookmarkListener(BookmarkListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the BookmarkListener to add to the list of registered listeners. * @see #removeBookmarkListener(BookmarkListener) */ public static void addBookmarkListener(BookmarkListener listener) { synchronized(listeners) { listeners.put(listener, null); } } /** * Removes the specified BookmarkListener from the list of registered listeners. * * @param listener the BookmarkListener to remove from the list of registered listeners. * @see #addBookmarkListener(BookmarkListener) */ private static void removeBookmarkListener(BookmarkListener listener) { synchronized(listeners) { listeners.remove(listener); } } /** * Notifies all the registered bookmark listeners of a bookmark change. This can be : *

    *
  • A new bookmark which has just been added *
  • An existing bookmark which has been modified *
  • An existing bookmark which has been removed *
*/ static void fireBookmarksChanged() { // Bookmarks file will need to be saved if (!isLoading) { saveNeeded = true; } lastBookmarkChangeTime = System.currentTimeMillis(); // Do not fire event if events are currently disabled if (!fireEvents) { return; } synchronized(listeners) { // Iterate on all listeners for (BookmarkListener listener : listeners.keySet()) { listener.bookmarksChanged(); } } } /** * Specifies whether bookmark events should be fired when a change in the bookmarks is detected. This allows * to temporarily suspend events firing when a lot of them are made, for example when editing the bookmarks list. * *

If true is specified, any subsequent calls to fireBookmarksChanged will be ignored, until this method is * called again with false. * @param b whether to fire events. */ public static synchronized void setFireEvents(boolean b) { if (b) { // Fire a bookmarks changed event if bookmarks were modified during event pause if (!fireEvents && lastBookmarkChangeTime >= lastEventPauseTime) { fireEvents = true; fireBookmarksChanged(); } } else { // Remember pause start time if (fireEvents) { fireEvents = false; lastEventPauseTime = System.currentTimeMillis(); } } } ///////////////////////////////////////// // VectorChangeListener implementation // ///////////////////////////////////////// public void elementsAdded(int startIndex, int nbAdded) { fireBookmarksChanged(); } public void elementsRemoved(int startIndex, int nbRemoved) { fireBookmarksChanged(); } public void elementChanged(int index) { fireBookmarksChanged(); } // - Bookmark loading ------------------------------------------------------ // ------------------------------------------------------------------------- private static class Loader implements BookmarkBuilder { public void startBookmarks() { } public void endBookmarks() { } public void addBookmark(String name, String location, String parent) { BookmarkManager.addBookmark(new Bookmark(name, location, parent)); } } } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkParser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.SAXParserFactory; import java.io.InputStream; /** * This class takes care of parsing the bookmarks XML file and adding parsed {@link Bookmark} instances to {@link BookmarkManager}. * * @author Maxence Bernard */ class BookmarkParser extends DefaultHandler implements BookmarkConstants { private static Logger logger; /** Variable used for XML parsing */ private String bookmarkName; /** Variable used for XML parsing */ private String bookmarkLocation; /** Variable used for XML parsing */ private String bookmarkParent; /** Variable used for XML parsing */ private StringBuilder characters; /** Receives bookmarks events. */ private BookmarkBuilder builder; /** muCommander version that was used to write the bookmarks file */ private String version; /** * Creates a new BookmarkParser instance. */ BookmarkParser() {} /** * Parses the given XML bookmarks file. Should only be called by BookmarkManager. */ void parse(InputStream in, BookmarkBuilder builder) throws Exception { this.builder = builder; characters = new StringBuilder(); SAXParserFactory.newInstance().newSAXParser().parse(in, this); } /** * Returns the muCommander version that was used to write the bookmarks file, null if it is unknown. *

* Note: the version attribute was introduced in muCommander 0.8.4. * * @return the muCommander version that was used to write the bookmarks file, null if it is unknown. */ public String getVersion() { return version; } /* ------------------------ */ /* ContentHandler methods */ /* ------------------------ */ @Override public void startDocument() throws SAXException { try { builder.startBookmarks(); } catch (BookmarkException e) { throw new SAXException(e); } } @Override public void endDocument() throws SAXException { try { builder.endBookmarks(); } catch (BookmarkException e) { throw new SAXException(e); } } /** * Method called when some PCDATA has been found in an XML node. */ @Override public void characters(char[] ch, int start, int length) { characters.append(ch, start, length); } /** * Notifies the parser that a new XML node has been found. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { characters.setLength(0); if (qName.equals(ELEMENT_ROOT)) { version = attributes.getValue(ATTRIBUTE_VERSION); } else if (qName.equals(ELEMENT_BOOKMARK)) { // Reset parsing variables bookmarkName = null; bookmarkLocation = null; bookmarkParent = null; } } /** * Notifies the parser that an XML node has been closed. */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { switch (qName) { case ELEMENT_BOOKMARK: addBookmark(); break; case ELEMENT_NAME: bookmarkName = characters.toString().trim(); break; case ELEMENT_LOCATION: bookmarkLocation = characters.toString().trim(); break; case ELEMENT_PARENT: bookmarkParent = characters.toString().trim(); break; // Note: url element has been deprecated in 0.8 beta3 but is still checked against for upward compatibility. // case ELEMENT_URL: // // Until early 0.8 beta3 nightly builds, credentials were stored directly in the bookmark's url. // // Now bookmark locations are free of credentials, these are stored in a dedicated credentials file where // // the password is encrypted. // try { // FileURL url = FileURL.getFileURL(characters.toString().trim()); // Credentials credentials = url.getCredentials(); // // // If the URL contains credentials, import them into CredentialsManager and remove credentials // // from the bookmark's location // if (credentials != null) { // CredentialsManager.addCredentials(new CredentialsMapping(credentials, url, true)); // bookmarkLocation = url.toString(false); // } else { // bookmarkLocation = characters.toString().trim(); // } // } catch (MalformedURLException e) { // bookmarkLocation = characters.toString().trim(); // } // break; } } private void addBookmark() throws SAXException { if (bookmarkName == null || bookmarkLocation == null) { getLogger().info("Missing value, bookmark ignored: name=" + bookmarkName + " location=" + bookmarkLocation); return; } try { builder.addBookmark(bookmarkName, bookmarkLocation, bookmarkParent); } catch (BookmarkException e) { throw new SAXException(e); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(BookmarkParser.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/bookmark/BookmarkWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; import com.mucommander.RuntimeConstants; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; import java.io.IOException; import java.io.OutputStream; /** * This class provides a method to write bookmarks to an XML file. * * @author Maxence Bernard, Nicolas Rinaudo */ class BookmarkWriter implements BookmarkConstants, BookmarkBuilder { private final XmlWriter out; BookmarkWriter(OutputStream stream) throws IOException { out = new XmlWriter(stream); } public void startBookmarks() throws BookmarkException { // Root element try { // Version the file. // Note: the version attribute was introduced in muCommander 0.8.4. XmlAttributes attributes = new XmlAttributes(); attributes.add("version", RuntimeConstants.VERSION); out.startElement(ELEMENT_ROOT, attributes); out.println(); } catch(IOException e) { throw new BookmarkException(e); } } public void endBookmarks() throws BookmarkException { try { out.endElement(ELEMENT_ROOT); } catch(IOException e) { throw new BookmarkException(e); } } public void addBookmark(String name, String location, String parent) throws BookmarkException { try { out.startElement(ELEMENT_BOOKMARK); out.println(); writeElement(ELEMENT_NAME, name); writeElement(ELEMENT_LOCATION, location); writeElement(ELEMENT_PARENT, parent); out.endElement(ELEMENT_BOOKMARK); } catch(IOException e) { throw new BookmarkException(e); } } private void writeElement(String name, String value) throws IOException { if (value != null) { out.startElement(name); out.writeCData(value); out.endElement(name); } } } ================================================ FILE: src/main/java/com/mucommander/bookmark/XORCipher.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark; import com.mucommander.commons.io.base64.Base64Decoder; import com.mucommander.commons.io.base64.Base64Encoder; import java.io.IOException; /** * This class provides simple XOR symmetrical encryption using a static hard-coded key, coupled with Base64 * encoding so that encrypted strings only use alphanumeric characters and thus can be embedded in text formats such * as XML. * *

Disclaimer: this obviously is weak encryption at most, the key used being static and public, and XOR * encryption being easy to crack. This doesn't aim or pretend to be anything more than a way to scramble text * without requiring a master password in the application. * * @author Maxence Bernard */ public class XORCipher { /** Long enough key (256 bytes) to avoid having too much redundancy in small text strings. */ private final static int[] NOT_SO_PRIVATE_KEY = { 161, 220, 156, 76, 177, 174, 56, 37, 98, 93, 224, 19, 160, 95, 69, 140, 91, 138, 33, 114, 248, 57, 179, 17, 54, 172, 249, 58, 26, 181, 167, 231, 241, 185, 218, 174, 37, 102, 100, 26, 16, 214, 119, 29, 118, 151, 135, 175, 245, 247, 160, 188, 77, 173, 109, 255, 73, 44, 186, 211, 117, 236, 204, 58, 246, 210, 128, 33, 234, 218, 82, 188, 78, 229, 180, 108, 247, 200, 3, 142, 206, 45, 165, 111, 96, 72, 76, 81, 238, 186, 240, 167, 185, 152, 68, 228, 87, 142, 145, 7, 74, 12, 106, 94, 15, 218, 155, 71, 87, 136, 58, 40, 246, 94, 7, 89, 29, 0, 78, 204, 70, 220, 240, 127, 59, 184, 109, 106 }; /** * Cyphers the given byte array using XOR symmetrical encryption with a static hard-coded key. * * @param b the byte array to encrypt/decrypt * @return the encrypted/decrypted byte array */ private static byte[] xor(byte[] b) { int len = b.length; int keyLen = NOT_SO_PRIVATE_KEY.length; byte[] result = new byte[len]; for (int i = 0; i < len; i++) { result[i] = (byte) (b[i] ^ NOT_SO_PRIVATE_KEY[i % keyLen]); } return result; } /** * Encrypts the given String using XOR cipher followed by Base64 encoding. The returned String will only contain * alphanumeric characters. * * @param s the String to encrypt * @return an XOR-Base64 encrypted String */ public static String encryptXORBase64(String s) { // TODO: // Important: String.getBytes() returns bytes in the platform's default encoding, which might vary across // platforms. This may potentially cause problems when decrypting a string on a different platform from the one // which served to encrypt it. // It is however too late to change as it could prevent existing encrypted strings (credentials file) from being // loaded after the application is updated. return Base64Encoder.encode(xor(s.getBytes())); } /** * Decrypts the given XOR-Base64 encrypted String and throws an IOException if the given String is not properly * Base64-encoded. * * @param s a XOR-Base64 encrypted String * @return the decrypted String * @throws IOException if the given String is not properly Base64-encoded */ public static String decryptXORBase64(String s) throws IOException { // TODO: // Important: new String() creates a string using the platform's default encoding, which might vary across // platforms. This may potentially cause problems when decrypting a string on a different platform from the one // which served to encrypt it. // It is however too late to change as it could prevent existing encrypted strings (credentials file) from being // loaded after the application is updated. return new String(xor(Base64Decoder.decodeAsBytes(s))); } } ================================================ FILE: src/main/java/com/mucommander/bookmark/file/BookmarkFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark.file; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkBuilder; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.*; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.*; import java.nio.charset.StandardCharsets; /** * Represents a file in the bookmark:// file system. * @author Nicolas Rinaudo */ public class BookmarkFile extends ProtocolFile { // - Instance fields ------------------------------------------------------- // ------------------------------------------------------------------------- /** Bookmark wrapped by this abstract file. */ private final Bookmark bookmark; /** Underlying abstract file. */ private AbstractFile file; /** Permissions for all bookmark files: rw- (600 octal). Only the 'user' permissions bits are supported. */ final static FilePermissions PERMISSIONS = new SimpleFilePermissions(384, 448); /** * Creates a new bookmark file wrapping the specified bookmark. * @param bookmark bookmark to wrap. * @throws IOException if the specified bookmark's URL cannot be resolved. */ BookmarkFile(Bookmark bookmark) throws IOException { super(FileURL.getFileURL(BookmarkProtocolProvider.BOOKMARK + "://" + java.net.URLEncoder.encode(bookmark.getName(), StandardCharsets.UTF_8))); this.bookmark = bookmark; } /** * Returns the AbstractFile this instance wraps. *

* Some methods need to have access to the underlying file. This, however, requires * resolving the path which can be time consuming. Using this method ensures that the * path is only resolved if necessary, and at most once. * * @return the AbstractFile this instance wraps. */ private synchronized AbstractFile getUnderlyingFile() { // Resolves the file if necessary. if (file == null) { file = FileFactory.getFile(bookmark.getLocation()); } return file; } /** * Returns the underlying bookmark. * @return the underlying bookmark. */ public Bookmark getBookmark() { return bookmark; } // - AbstractFile methods -------------------------------------------------- // ------------------------------------------------------------------------- /** * Returns the underlying bookmark's name. * @return the underlying bookmark's name. */ @Override public String getName() { return bookmark.getName(); } /** * Returns the wrapped file's descendants. * @return the wrapped file's descendants. * @throws IOException if an I/O error occurs. * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ @Override public AbstractFile[] ls() throws IOException, UnsupportedFileOperationException { return getUnderlyingFile().ls(); } /** * Returns the wrapped file's parent. * @return the wrapped file's parent. * @see #setParent(AbstractFile) */ @Override public AbstractFile getParent() { try { return new BookmarkRoot(); } catch(IOException e) { return null; } } /** * Returns the result of the wrapped file's getFreeSpace() methods. * @return the result of the wrapped file's getFreeSpace() methods. * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ @Override public long getFreeSpace() throws IOException, UnsupportedFileOperationException { return getUnderlyingFile().getFreeSpace(); } /** * Returns the result of the wrapped file's getTotalSpace() methods. * @return the result of the wrapped file's getTotalSpace() methods. * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ @Override public long getTotalSpace() throws IOException, UnsupportedFileOperationException { return getUnderlyingFile().getTotalSpace(); } /** * Returns false. * @return false. */ @Override public boolean isDirectory() { return true; } /** * Sets the wrapped file's parent. * @param parent object to use as the wrapped file's parent. * @see AbstractFile#getParent() */ @Override public void setParent(AbstractFile parent) { getUnderlyingFile().setParent(parent);} /** * Returns true if the specified bookmark exists. *

* A bookmark is said to exist if and only if it is known to the {@link com.mucommander.bookmark.BookmarkManager}. * * @return true if the specified bookmark exists, false otherwise. */ @Override public boolean exists() { return BookmarkManager.getBookmark(bookmark.getName()) != null; } @Override public void mkfile() { BookmarkManager.addBookmark(bookmark); } public boolean equals(Object o) { // Makes sure we're working with an abstract file. if (!(o instanceof AbstractFile)) { return false; } // Retrieves the actual file instance. // We might have received a Proxied or Cached file, so we need to make sure // we 'unwrap' that before comparing. AbstractFile file = ((AbstractFile)o).getAncestor(); // We only know how to compare one bookmark file to the other. if (file instanceof BookmarkFile) { return bookmark.equals(((BookmarkFile)file).getBookmark()); } return false; } @Override public String getCanonicalPath() {return bookmark.getLocation();} // - Bookmark renaming ----------------------------------------------------- // ------------------------------------------------------------------------- /** * Attempts to rename the bookmark to the specified destination. * The operation will only be carried out if the specified destination is a BookmarkFile or has an * ancestor that is. * * @param destination where to move the bookmark to. * @throws IOException if the operation could not be carried out. */ @Override public void renameTo(AbstractFile destination) throws IOException { checkRenamePrerequisites(destination, true, true); destination = destination.getTopAncestor(); // Makes sure we're working with a bookmark. if (!(destination instanceof BookmarkFile)) { throw new IOException(); } // Creates the new bookmark and checks for conflicts. Bookmark newBookmark = new Bookmark(destination.getName(), bookmark.getLocation(), bookmark.getParent()); Bookmark oldBookmark = BookmarkManager.getBookmark(newBookmark.getName()); if (oldBookmark != null) { BookmarkManager.removeBookmark(oldBookmark); } // Adds the new bookmark and deletes its 'old' version. BookmarkManager.addBookmark(newBookmark); BookmarkManager.removeBookmark(bookmark); } // TODO: bookmark deleting is currently disabled as a quick fix for #329 // /** // * Deletes the bookmark. // *

// * Deleting a bookmark means unregistering it from the {@link com.mucommander.bookmark.BookmarkManager}. // * // */ // @Override // public void delete() { // BookmarkManager.removeBookmark(bookmark); // } @Override @UnsupportedFileOperation public void delete() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.DELETE); } // - Bookmark duplication -------------------------------------------------- // ------------------------------------------------------------------------- /** * Tries to copy the bookmark to the specified destination. *

* If the specified destination is an instance of BookmarkFile, * this will duplicate the bookmark. Otherwise, this method will fail. * * @param destination where to copy the bookmark to. * @throws FileTransferException if the specified destination is not an instance of BookmarkFile. */ @Override public void copyRemotelyTo(AbstractFile destination) throws IOException { // Makes sure we're working with a bookmark. destination = destination.getTopAncestor(); if (!(destination instanceof BookmarkFile)) { throw new IOException(); } // Copies this bookmark to the specified destination. BookmarkManager.addBookmark(new Bookmark(destination.getName(), bookmark.getLocation(), bookmark.getParent())); } // - Permissions ----------------------------------------------------------- // ------------------------------------------------------------------------- /** * Returns the same permissions for all boookmark files: rw- (600 octal). * Only the 'user' permissions bits are supported. * @return this file's permissions. * @see #changePermission(int,int,boolean) */ @Override public FilePermissions getPermissions() { return PERMISSIONS; } /** * Always throws an {@link UnsupportedFileOperationException} when called: bookmarks always have all permissions, * this is not changeable. * * @param access ignored. * @param permission ignored. * @param enabled ignored. * @see #getPermissions() */ @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } // - Import / export ------------------------------------------------------- // ------------------------------------------------------------------------- @Override public InputStream getInputStream() throws IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(); BookmarkBuilder builder = BookmarkManager.getBookmarkWriter(stream); try { builder.startBookmarks(); builder.addBookmark(bookmark.getName(), bookmark.getLocation(), bookmark.getParent()); builder.endBookmarks(); } catch (Throwable e) { // If an exception occurred, we have to look for its root cause. Throwable e2; // Looks for the cause. while ((e2 = e.getCause()) != null) { e = e2; } // If the cause is an IOException, thow it. if (e instanceof IOException) { throw (IOException)e; } // Otherwise, throw the exception as an IOException with a the underlying cause's message. throw new IOException(e.getMessage()); } return new ByteArrayInputStream(stream.toByteArray()); } @Override public OutputStream getOutputStream() throws IOException {return new BookmarkOutputStream();} // - Unused methods -------------------------------------------------------- // ------------------------------------------------------------------------- // The following methods are not used by BookmarkFile. They will throw an exception or // return an 'operation non supported' / default value. @Override @UnsupportedFileOperation public void mkdir() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CREATE_DIRECTORY);} @Override public long getLastModifiedDate() {return 0;} @Override public PermissionBits getChangeablePermissions() {return PermissionBits.EMPTY_PERMISSION_BITS;} @Override @UnsupportedFileOperation public void setLastModifiedDate(long lastModified) throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE);} @Override public long getSize() {return -1;} @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE);} @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE);} @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE);} @Override public Object getUnderlyingFileObject() {return null;} @Override public boolean isSymlink() {return false;} @Override public boolean isSystem() {return false;} @Override public String getOwner() {return null;} @Override public boolean canGetOwner() {return false;} @Override public String getGroup() {return null;} @Override public boolean canGetGroup() {return false;} @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } } ================================================ FILE: src/main/java/com/mucommander/bookmark/file/BookmarkOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark.file; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkBuilder; import com.mucommander.bookmark.BookmarkManager; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; /** * Class used to provide an output stream on a bookmark. * @author Nicolas Rinaudo */ class BookmarkOutputStream extends ByteArrayOutputStream implements BookmarkBuilder { // - Stream methods -------------------------------------------------------- // ------------------------------------------------------------------------- /** * Parses the content that has been written. * @throws IOException if an error occurs. */ @Override public void close() throws IOException { super.close(); try { BookmarkManager.readBookmarks(new ByteArrayInputStream(toByteArray()), this); } catch(IOException e) { throw e; } catch(Exception e) { throw new IOException(e.getMessage()); } } // - Bookmark builder methods ---------------------------------------------- // ------------------------------------------------------------------------- /** * Ignored. */ public void startBookmarks() {} /** * Ignored. */ public void endBookmarks() {} /** * Adds the specified bookmark to the bookmark manager *

* Note that this method will remove any previous bookmark of the same name. * * @param name name of the new bookmark. * @param location location of the new bookmark. */ public void addBookmark(String name, String location, String parent) { // Creates the new bookmark and checks for conflicts. Bookmark newBookmark = new Bookmark(name, location, parent); // Old bookmark of the same name, if any. Bookmark oldBookmark = BookmarkManager.getBookmark(name); if (oldBookmark != null) { BookmarkManager.removeBookmark(oldBookmark); } // Adds the new bookmark. BookmarkManager.addBookmark(newBookmark); } } ================================================ FILE: src/main/java/com/mucommander/bookmark/file/BookmarkProtocolProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark.file; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import java.io.IOException; /** * This class is the provider for the bookmark filesystem implemented by {@link com.mucommander.bookmark.file.BookmarkFile}. * * @author Nicolas Rinaudo * @see com.mucommander.bookmark.file.BookmarkFile */ public class BookmarkProtocolProvider implements ProtocolProvider { /** Protocol for the virtual bookmarks file system. */ public static final String BOOKMARK = "bookmark"; public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { // If the URL contains a path but no host, it's illegal. // If it contains neither host nor path, we're browsing bookmarks:// if (url.getHost() == null) { if (url.getPath().equals("/")) { return new BookmarkRoot(url); } throw new IOException(); } // If the URL contains a host, look it up in the bookmark list and use that // as the root of the returned path. else { Bookmark bookmark = BookmarkManager.getBookmark(url.getHost()); // If the bookmark doesn't exist, but a path is specified, throws an exception. // Otherwise, returns the requested bookmark. if (bookmark == null) { if (!url.getPath().equals("/")) { throw new IOException(); } return new BookmarkFile(new Bookmark(url.getHost(), url.getPath(), null)); } // If the bookmark exists, and a path is specified, creates a new path // from the bookmark's location and the specified path. if (!url.getPath().equals("/")) { return FileFactory.getFile(bookmark.getLocation() + url.getPath()); } // Otherwise, creates a new bookmark file. return new BookmarkFile(bookmark); } } } ================================================ FILE: src/main/java/com/mucommander/bookmark/file/BookmarkRoot.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.bookmark.file; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkListener; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.*; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Represents the root of the bookmarks:// file system. * @author Nicolas Rinaudo */ class BookmarkRoot extends ProtocolFile implements BookmarkListener { /** Time at which the bookmarks were last modified. */ private long lastModified; BookmarkRoot() throws IOException {this(FileURL.getFileURL(BookmarkProtocolProvider.BOOKMARK + "://"));} BookmarkRoot(FileURL url) { super(url); lastModified = System.currentTimeMillis(); BookmarkManager.addBookmarkListener(this); } @Override public AbstractFile[] ls() throws IOException { // Retrieves all available bookmarks. Object[] buffer = BookmarkManager.getBookmarks().toArray(); AbstractFile[] files = new AbstractFile[buffer.length]; // Creates the associated instances of BookmarkFile for (int i = 0; i < files.length; i++) { files[i] = new BookmarkFile((Bookmark)buffer[i]); } return files; } @Override public String getName() { return ""; } @Override public boolean isDirectory() { return true; } /** * Stores the current date as the date of last modification. */ public void bookmarksChanged() { lastModified = System.currentTimeMillis(); } /** * Returns the date at which the bookmark list was last modified. * @return the date at which the bookmark list was last modified. */ @Override public long getLastModifiedDate() { return lastModified; } // The following methods are not used by BookmarkFile. They will throw an exception, // return an 'operation non supported' value or return a default value. @Override public AbstractFile getParent() {return null;} @Override @UnsupportedFileOperation public void delete() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.DELETE);} @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY);} @Override @UnsupportedFileOperation public void renameTo(AbstractFile destFile) throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RENAME);} @Override @UnsupportedFileOperation public void setLastModifiedDate(long lastModified) throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE);} @Override public long getSize() {return -1;} @Override public void setParent(AbstractFile parent) {} @Override public boolean exists() {return true;} @Override public FilePermissions getPermissions() {return BookmarkFile.PERMISSIONS;} @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION);} @Override public PermissionBits getChangeablePermissions() {return PermissionBits.EMPTY_PERMISSION_BITS;} @Override public boolean isSymlink() {return false;} @Override public boolean isSystem() {return false;} @Override @UnsupportedFileOperation public void mkdir() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.CREATE_DIRECTORY);} @Override @UnsupportedFileOperation public InputStream getInputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.READ_FILE);} @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE);} @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE);} @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE);} @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE);} @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE);} @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException {throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE);} @Override public Object getUnderlyingFileObject() {return null;} @Override public String getOwner() {return null;} @Override public boolean canGetOwner() {return false;} @Override public String getGroup() {return null;} @Override public boolean canGetGroup() {return false;} @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } } ================================================ FILE: src/main/java/com/mucommander/bookmark/file/package.html ================================================ Provides an implementation of the bookmarks:// virtual file system. ================================================ FILE: src/main/java/com/mucommander/bookmark/package.html ================================================ API for bookmark management. ================================================ FILE: src/main/java/com/mucommander/cache/FastLRUCache.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.cache; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; /** * LRU cache implementation which uses LinkedHashMap which provides fast retrieval and insertion * operations. * *

The only area this implementation is slow at, is checking for and removing expired elements which * requires traversing all values and LinkedHashMap is slow at that. * To minimize the impact this could have on performance, this operation is not systematically performed * for each call to get() and set() methods, unless the cache is full. * That means this implementation is not as aggressive as it could be in terms of releasing expired items' memory * but favors performance instead, which is what caches are for. * * @author Maxence Bernard */ public class FastLRUCache extends LRUCache { /** Cache key->value/expirationDate map */ private final LinkedHashMap> cacheMap; /** Timestamp of last expired items purge */ private long lastExpiredPurge; /** Number of millisecond to wait between 2 expired items purges, if cache is not full */ private final static int PURGE_EXPIRED_DELAY = 1000; private static final class Value { private final V val; private final Long expiration; public Value(V value, Long expirationDate) { this.val = value; this.expiration = expirationDate; } } public FastLRUCache(int capacity) { super(capacity); this.cacheMap = new LinkedHashMap>(16, 0.75f, true) { // Override this method to automatically remove eldest entry before insertion when cache is full @Override protected boolean removeEldestEntry(Map.Entry> eldest) { return cacheMap.size() > FastLRUCache.this.capacity; } }; } /** * Returns a String representation of this cache. */ public String toString() { StringBuilder sb = new StringBuilder(super.toString()). append(" size=").append(cacheMap.size()). append(" capacity=").append(capacity). append(" eldestExpirationDate=").append(eldestExpirationDate).append('\n'); int i = 0; for (Map.Entry> mapEntry : cacheMap.entrySet()) { Object key = mapEntry.getKey(); Value value = mapEntry.getValue(); sb.append(i++).append("-key=").append(key).append(" value=").append(value.val). append(" expirationDate=").append(value.expiration).append('\n'); } if (UPDATE_CACHE_COUNTERS) { sb.append("nbCacheHits=").append(nbHits).append(" nbCacheMisses=").append(nbMisses).append('\n'); } return sb.toString(); } /** * Looks for cached items that have a passed expiration date and purge them. */ private void purgeExpiredItems() { long now = System.currentTimeMillis(); // No need to go any further if eldestExpirationDate is in the future. // Also, since iterating on the values is an expensive operation (especially for LinkedHashMap), // wait PURGE_EXPIRED_DELAY between two purges, unless cache is full if (this.eldestExpirationDate > now || (cacheMap.size()> iterator = cacheMap.values().iterator(); // Iterate on all cached values while (iterator.hasNext()) { Long expirationDateL = iterator.next().expiration; // No expiration date for this value if (expirationDateL == null) { continue; } long expirationDate = expirationDateL; // Test if the item has an expiration date and check if has passed if (expirationDate < now) { // Remove expired item iterator.remove(); } else if(expirationDate < this.eldestExpirationDate) { // update eldestExpirationDate this.eldestExpirationDate = expirationDate; } } // Set last purge timestamp to now lastExpiredPurge = now; } @Override public synchronized V get(K key) { // Look for expired items and purge them (if any) purgeExpiredItems(); // Look for a value corresponding to the specified key in the cache map Value value = cacheMap.get(key); if (value == null) { // No value matching key, better luck next time! if (UPDATE_CACHE_COUNTERS) { nbMisses++; // Increase cache miss counter } return null; } // Since expired items purge is not performed on every call to this method for // performance reason, we can end with an expired cached value so we need // to check this if (value.expiration != null && System.currentTimeMillis() > value.expiration) { // Value has expired, let's remove it if (UPDATE_CACHE_COUNTERS) { nbMisses++; // Increase cache miss counter } cacheMap.remove(key); return null; } if (UPDATE_CACHE_COUNTERS) { nbHits++; // Increase cache hit counter } return value.val; } @Override public synchronized void add(K key, V value, long timeToLive) { // Look for expired items and purge them (if any) purgeExpiredItems(); Long expirationDateL; if (timeToLive == -1) { expirationDateL = null; } else { long expirationDate = System.currentTimeMillis()+timeToLive; // Update eledestExpirationDate if new element's expiration date is older if (expirationDate(value, expirationDateL)); } @Override public synchronized int size() { return cacheMap.size(); } @Override public synchronized void clearAll() { cacheMap.clear(); eldestExpirationDate = Long.MAX_VALUE; } ////////////////// // Test methods // ////////////////// /** * Tests this LRUCache for corruption and throws a RuntimeException if something is wrong. */ @Override protected void testCorruption() throws RuntimeException { for (K key : cacheMap.keySet()) { Value value = cacheMap.get(key); if (value == null) { throw new RuntimeException("cache corrupted: value could not be found for key="+key); } if (value.expiration == null) { continue; } Long expirationDate = value.expiration; if (expirationDate < eldestExpirationDate) { throw new RuntimeException("cache corrupted: expiration date for key="+key+" older than eldestExpirationDate"); } } } } ================================================ FILE: src/main/java/com/mucommander/cache/LRUCache.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.cache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Random; /** * An abstract LRU cache. * *

An LRU (Least Recently Used) cache can contain a fixed number of items (the capacity). When capacity is reached, * the least recently used is removed. Each object retrieved with the {@link #get(Object) get()} method * makes the requested item the most recently used one. Similarly, each object inserted using the * {@link #add(Object, Object) add()} method makes the added item the most recently used one. * *

This LRUCache provides an optional feature : the ability to assign a time-to-live for each or part of the * items added. When the time-to-live of an item expires, the item is automatically removed from the cache and won't * be returned by the {@link #get(Object) get()} method anymore. * *

Implementation note: checking for expired items can be an expensive operation so it doesn't have * to be done as soon as the item has expired, the expired items can live a bit longer in the cache if necessary. *
The LRUCache implementation must however guarantee two things : *

    *
  • as soon as an item has expired, it cannot be returned by {@link #get(Object) get()}. *
  • when cache capacity is reached (cache is full) and a new item needs to be added, any expired item must be * immediately removed. This prevents least recently used items from being removed unnecessarily. *
* * @author Maxence Bernard */ public abstract class LRUCache { private static Logger logger; /** Cache capacity: maximum number of items this cache can contain */ protected int capacity; /** Current eldest expiration date amongst all items */ protected long eldestExpirationDate = Long.MAX_VALUE; /** Specifies whether cache hit/miss counters should be updated (should be enabled for Debug purposes only) */ protected final static boolean UPDATE_CACHE_COUNTERS = false; /** Number of cache hits since this LRUCache was created */ protected int nbHits; /** Number of cache misses since this LRUCache was created */ protected int nbMisses; /** * Creates an initially empty LRUCache with the specified maximum capacity. * @param capacity initial capacity (he maximum number of items this cache can contain) */ public LRUCache(int capacity) { this.capacity = capacity; } /** * Returns the maximum number of items this cache can contain. * @return the maximum number of items this cache can contain */ public int getCapacity() { return capacity; } /** * Returns the number of cache hits since this LRUCache was created. * * @return the number of cache hits since this LRUCache was created */ public int getHitCount() { return nbHits; } /** * Returns the number of cache misses since this LRUCache was created. * * @return the number of cache misses since this LRUCache was created */ public int getMissCount() { return nbMisses; } /** * Returns the cached object value corresponding to the given key and marks the cached item as the most * recently used one. * *

This method will return null if: *

    *
  • the given key doesn't exist *
  • the cached value corresponding to the key has expired *
* * @param key the cached item's key * @return the cached value corresponding to the specified key, or null if a value could not * found or has expired */ public abstract V get(K key); /** * Adds a new key/value pair to the cache and marks it as the most recently used. * *

If the cache's capacity has been reached (cache is full): *

    *
  • any object with a past expiration date will be removed
  • *
  • if no expired item could be removed, the least recently used item will be removed
  • *
* * @param key the key for the object to store * @param value the value to cache * @param timeToLive the time-to-live of the object in the cache in milliseconds, or -1 for no time-to-live, * the object will just be removed when it becomes the least recently used one. */ public abstract void add(K key, V value, long timeToLive); /** * Convenience method, equivalent to add(key, value, -1). * @param key key * @param value value */ public synchronized void add(K key, V value) { add(key, value, -1); } /** * Removes all items from this cache, leaving the cache in the same state as when it was just created. */ public abstract void clearAll(); /** * Returns the current size of this cache, i.e. the number of cached elements it contains. *
Note: Some items that have expired and have not yet been removed might be accounted for * in the returned size. * * @return the current size of this cache */ public abstract int size(); /** * Tests this LRUCache for corruption and throws a RuntimeException if something is wrong. */ protected abstract void testCorruption() throws RuntimeException; /** * Test method : simple test case + stress/sanity test * * @param args command line arguments */ public static void main(String[] args) { LRUCache cache; /* // Simple test case cache = new FastLRUCache(3); cache.add("orange", "ORANGE"); System.out.println(cache.toString()); cache.add("apple", "APPLE"); System.out.println(cache.toString()); System.out.println("get(orange)= "+cache.get("orange")); System.out.println(cache.toString()); cache.add("apricot", "APRICOT"); System.out.println(cache.toString()); cache.add("banana", "BANANA", 1000); System.out.println(cache.toString()); System.out.println("waiting for banana expiration"); try { Thread.sleep(1050); } catch(InterruptedException e) {} System.out.println(cache.toString()); System.out.println("get(banana)= "+cache.get("banana")); System.out.println(cache.toString()); */ long timeStamp = System.currentTimeMillis(); // Stress test to see if everything looks OK after a few thousand iterations int capacity = 1000; cache = new FastLRUCache<>(capacity); Random random = new Random(); for (int i=0; i<100000; i++) { // 50% chance to add a new element with a random value and expiration date (50% chance for no expiration date) if (cache.isEmpty() || random.nextBoolean()) { // System.out.println("cache.add()"); cache.add(random.nextInt(capacity), random.nextInt(), random.nextBoolean()?-1:random.nextInt(10)); } // 50% chance to retrieve a random existing element else { // System.out.println("cache.get()"); cache.get(random.nextInt(capacity)); } try { // Test the cache for corruption cache.testCorruption(); } catch(RuntimeException e) { getLogger().debug("Cache corrupted after "+i+" iterations, cache state="+cache); return; } // // Print the cache's state // System.out.println(cache.toString()); } getLogger().debug("Stress test took "+(System.currentTimeMillis()-timeStamp)+" ms.\n"); // Print the cache's state System.out.println(cache); } public boolean isEmpty() { return size() != 0; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(LRUCache.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/cache/TextHistory.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.cache; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import java.io.*; import java.lang.ref.WeakReference; import java.nio.charset.StandardCharsets; import java.util.*; /** * @author Oleg Trifonov * * Stores history of search requests for text search, file search etc. */ public class TextHistory { private static final int MAX_RECORDS = 500; public enum Type { TEXT_SEARCH("search-text.history"), HEX_DATA_SEARCH("search-hex.history"), FILE_NAME("search-files.history"), CALCULATOR("calculator.history"), EDITOR_BOOKMARKS("editor.bookmarks"); private final String fileName; Type(String fileName) { this.fileName = fileName; } } private final Map> history = new HashMap<>(); private static WeakReference instance; public static TextHistory getInstance() { TextHistory textHistory = instance != null ? instance.get() : null; if (textHistory == null) { textHistory = new TextHistory(); instance = new WeakReference<>(textHistory); } return textHistory; } public LinkedList getList(Type type) { LinkedList result = history.get(type); if (result == null) { try { result = load(getHistoryFile(type)); } catch (IOException e) { e.printStackTrace(); result = new LinkedList<>(); } history.put(type, result); } return result; } public void add(Type type, String s, boolean save) { LinkedList list = getList(type); int index = list.indexOf(s); if (index >= 0) { list.remove(index); // remove other elements if they exists while (list.remove(s)) { // } } if (s.trim().isEmpty()) { return; } list.addFirst(s); while (list.size() > MAX_RECORDS) { list.removeLast(); } // save only if new record was added if (index != 0 && save) { save(type); } } public void save(Type type) { try { save(getHistoryFile(type), getList(type)); } catch (IOException e) { e.printStackTrace(); } } private LinkedList load(AbstractFile file) { LinkedList result = new LinkedList<>(); if (!file.exists()) { return result; } try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) { String line; while ( (line = reader.readLine() ) != null) { String trim = line.trim(); if (trim.isEmpty() || trim.startsWith("#")) { continue; } result.add(line); } } catch (Exception e) { e.printStackTrace(); } return result; } private void save(AbstractFile file, List list) throws IOException { try (Writer writer = new BufferedWriter(new OutputStreamWriter(file.getOutputStream(), StandardCharsets.UTF_8))) { for (String s : list) { writer.write(s); writer.write('\n'); } } } /** * Returns the path to the history file. *

* Will return the default, system dependant bookmarks file. * * @return the path to the bookmark file. * @throws java.io.IOException if there was a problem locating the default history file. */ public static synchronized AbstractFile getHistoryFile(Type type) throws IOException { return PlatformManager.getPreferencesFolder().getChild(type.fileName); } public void clear() { history.clear(); instance = null; } } ================================================ FILE: src/main/java/com/mucommander/cache/WindowsStorage.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.cache; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import java.awt.Window; import java.io.*; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; /** * Stores windows sizes and positions */ public class WindowsStorage { private static final String STORAGE_FILE_NAME = "windows.list"; private static WeakReference instance; private Map records; public static class Record { public final int left, top, width, height; Record(String s) { String[] val = s.split(","); this.left = Integer.parseInt(val[0].trim()); this.top = Integer.parseInt(val[1].trim()); this.width = Integer.parseInt(val[2].trim()); this.height = Integer.parseInt(val[3].trim()); } public Record(int left, int top, int width, int height) { this.left = left; this.top = top; this.width = width; this.height = height; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof Record)) { return false; } Record rec = (Record)obj; return left == rec.left && top == rec.top && width == rec.width && height == rec.height; } @Override public String toString() { return String.valueOf(left) + ',' + top + ',' + width + ',' + height; } public void apply(Window window) { window.setLocation(left, top); window.setSize(width, height); } void applyPos(Window window) { window.setLocation(left, top); } } public static WindowsStorage getInstance() { WindowsStorage windowsStorage = instance != null ? instance.get() : null; if (windowsStorage == null) { windowsStorage = new WindowsStorage(); instance = new WeakReference<>(windowsStorage); } return windowsStorage; } public Record get(String key) { return getRecords().get(key); } public Record get(Window window, String suffix) { String key = getKey(window, suffix); return getRecords().get(key); } public Record get(Window frame) { return get(frame, null); } public void put(String key, Record rec) { Record prev = getRecords().put(key, rec); if (prev == null || !prev.equals(rec)) { try { save(getHistoryFile()); } catch (IOException e) { e.printStackTrace(); } } } public void put(Window window) { put(window, null); } public void put(Window window, String suffix) { Record rec = new Record(window.getLocation().x, window.getLocation().y, window.getWidth(), window.getHeight()); String key = getKey(window, suffix); put(key, rec); } public boolean init(Window window, String suffix, boolean storeSizes) { String key = getKey(window, suffix); Record rec = getRecords().get(key); if (rec != null && rec.width > 0 && rec.height > 40) { if (storeSizes) { rec.apply(window); } else { rec.applyPos(window); } return true; } return false; } private String getKey(Window window, String suffix) { Class c = window.getClass(); String key = c.getCanonicalName(); if (key == null) { key = c.getPackage().getName() + '.' + c.getName(); } if (suffix != null) { key += '#' + suffix; } return key; } public boolean init(Window window) { return init(window, null, true); } private Map getRecords() { if (records == null) { records = new HashMap<>(); try { load(getHistoryFile()); } catch (FileNotFoundException ignore) { } catch (IOException e) { e.printStackTrace(); } } return records; } private void load(AbstractFile file) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { loadRecord(line.trim()); } } // BufferedReader reader = null; // try { // reader = new BufferedReader(new InputStreamReader(file.getInputStream())); // String line; // while ( (line = reader.readLine() ) != null) { // line = line.trim(); // if (line.isEmpty() || line.startsWith("#")) { // continue; // } // int index = line.indexOf('='); // if (index < 0) { // continue; // } // String key = line.substring(0, index); // String val = line.substring(index + 1); // try { // records.put(key, new Record(val)); // } catch (Exception e) { // e.printStackTrace(); // } // } // } catch (Exception e) { // e.printStackTrace(); // } finally { // if (reader != null) { // reader.close(); // } // } } private void loadRecord(String line) { if (line.isEmpty() || line.startsWith("#")) { return; } int index = line.indexOf('='); if (index < 0) { return; } String key = line.substring(0, index); String val = line.substring(index + 1); try { records.put(key, new Record(val)); } catch (Exception e) { e.printStackTrace(); } } private void save(AbstractFile file) throws IOException { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(file.getOutputStream()))) { for (String key : getRecords().keySet()) { if (key != null) { saveRecord(writer, key, records.get(key)); } } } } private void saveRecord(Writer writer, String key, Record record) throws IOException { writer.write(key); writer.write('='); writer.write(record.toString()); writer.write('\n'); } /** * Returns the path to the history file. *

* Will return the default, system dependant bookmarks file. * * @return the path to the bookmark file. * @throws java.io.IOException if there was a problem locating the default history file. */ private static synchronized AbstractFile getHistoryFile() throws IOException { return PlatformManager.getPreferencesFolder().getChild(STORAGE_FILE_NAME); } public void clear() { if (records != null) { records.clear(); } records = null; instance = null; } } ================================================ FILE: src/main/java/com/mucommander/cache/package.html ================================================ Provides various cache implementations. ================================================ FILE: src/main/java/com/mucommander/command/AssociationBuilder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; /** * Receive notification of the logical structure of a custom association list. * @author Nicolas Rinaudo */ public interface AssociationBuilder { /** * Notifies the builder that association building is about to start. * @throws CommandException if an error occurs. */ void startBuilding() throws CommandException; /** * Notifies the builder that association building is finished. * @throws CommandException if an error occurs. */ void endBuilding() throws CommandException; /** * Notifies the builder that a new association declaration is starting. * @param command command to call when the association is matched. * @throws CommandException if an error occurs. */ void startAssociation(String command) throws CommandException; /** * Notifies the builder that the current association declaration is finished. * @throws CommandException if an error occurs. */ void endAssociation() throws CommandException; /** * Adds a mask to the current association. * @param mask regular expression that a file name must match in order to match the association. * @param isCaseSensitive whether the regular expression is case-sensitive. * @throws CommandException if an error occurs. */ void setMask(String mask, boolean isCaseSensitive) throws CommandException; /** * Adds a symlink IMAGE_FILTER on the current association. * @param isSymlink whether symbolic links must be refused or accepted by the association. * @throws CommandException if an error occurs. */ void setIsSymlink(boolean isSymlink) throws CommandException; /** * Adds a hidden IMAGE_FILTER on the current association. * @param isHidden whether hidden files must be refused or accepted by the association. * @throws CommandException if an error occurs. */ void setIsHidden(boolean isHidden) throws CommandException; /** * Adds a readable IMAGE_FILTER on the current association. * @param isReadable whether readable files must be refused or accepted by the association. * @throws CommandException if an error occurs. */ void setIsReadable(boolean isReadable) throws CommandException; /** * Adds a writable IMAGE_FILTER on the current association. * @param isWritable whether writable files must be refused or accepted by the association. * @throws CommandException if an error occurs. */ void setIsWritable(boolean isWritable) throws CommandException; /** * Adds an executable IMAGE_FILTER on the current association. * @param isExecutable whether executable files must be refused or accepted by the association. * @throws CommandException if an error occurs. */ void setIsExecutable(boolean isExecutable) throws CommandException; } ================================================ FILE: src/main/java/com/mucommander/command/AssociationFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import com.mucommander.commons.file.PermissionTypes; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.RegexpFilenameFilter; /** * @author Nicolas Rinaudo */ class AssociationFactory implements AssociationBuilder { private AndFileFilter filter; private String command; public void startBuilding() {} public void endBuilding() {} public void startAssociation(String command) { filter = new AndFileFilter(); this.command = command; } public void endAssociation() throws CommandException { // Skip empty file filters as they will break the whole // association mechanism. if(!filter.isEmpty()) CommandManager.registerAssociation(command, filter); } public void setMask(String mask, boolean isCaseSensitive) { filter.addFileFilter(new RegexpFilenameFilter(mask, isCaseSensitive)); } public void setIsDir(boolean isDir) { filter.addFileFilter(new AttributeFileFilter(FileAttribute.DIRECTORY, isDir)); } public void setIsSymlink(boolean isSymlink) { filter.addFileFilter(new AttributeFileFilter(FileAttribute.SYMLINK, isSymlink)); } public void setIsHidden(boolean isHidden) { filter.addFileFilter(new AttributeFileFilter(FileAttribute.HIDDEN, isHidden)); } public void setIsReadable(boolean isReadable) { filter.addFileFilter(new PermissionsFileFilter(PermissionTypes.READ_PERMISSION, isReadable)); } public void setIsWritable(boolean isWritable) { filter.addFileFilter(new PermissionsFileFilter(PermissionTypes.WRITE_PERMISSION, isWritable)); } public void setIsExecutable(boolean isExecutable) { filter.addFileFilter(new PermissionsFileFilter(PermissionTypes.EXECUTE_PERMISSION, isExecutable)); } } ================================================ FILE: src/main/java/com/mucommander/command/AssociationReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.InputStream; /** * Class used to parse custom associations XML files. *

* Association file parsing is done through the {@link #read(InputStream,AssociationBuilder) read} method, which is * the only way to interact with this class. *

* Note that while this class knows how to read the content of an association XML file, its role is not to interpret it. This * is done by instances of {@link AssociationBuilder}. * * @see AssociationsXmlConstants * @see AssociationBuilder * @see AssociationWriter * @author Nicolas Rinaudo */ public class AssociationReader extends DefaultHandler implements AssociationsXmlConstants { /** Where to send building messages. */ private final AssociationBuilder builder; private boolean isInAssociation; /** * Creates a new command reader. * @param b where to send custom command events. */ private AssociationReader(AssociationBuilder b) { builder = b; } /** * Parses the content of the specified input stream. *

* This method will go through the specified input stream and notify the builder of any new association declaration it * encounters. Note that parsing is done in a very lenient fashion, and perfectly invalid XML files might not raise * an exception. This is not a flaw in the parser, and both allows muCommander to be error resilient and the associations * file format to be extended without having to rewrite most of this code. *

* Note that even if an error occurs, both of the builder's {@link AssociationBuilder#startBuilding()} and * {@link AssociationBuilder#endBuilding()} methods will still be called. Parsing will stop at the first error * however, so while the builder is guaranteed to receive correct messages, it might not receive all declared * associations. * * @param in where to read association data from. * @param b where to send building events to. * @throws IOException if any IO error occurs. * @throws CommandException if any parse error occurs * @see #read(InputStream,AssociationBuilder) */ public static void read(InputStream in, AssociationBuilder b) throws IOException, CommandException { b.startBuilding(); try { SAXParserFactory.newInstance().newSAXParser().parse(in, new AssociationReader(b)); } catch(ParserConfigurationException | SAXException e) { throw new CommandException(e); } finally { b.endBuilding(); } } /** * This method is public as an implementation side effect and should not be called directly. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { String buffer; try { if (!isInAssociation) { if (qName.equals(ELEMENT_ASSOCIATION)) { // Makes sure the required attributes are present. if ((buffer = attributes.getValue(ATTRIBUTE_COMMAND)) == null) { return; } isInAssociation = true; builder.startAssociation(buffer); } } else { switch (qName) { case ELEMENT_MASK: String caseSensitive; if ((buffer = attributes.getValue(ATTRIBUTE_VALUE)) == null) { return; } if ((caseSensitive = attributes.getValue(ATTRIBUTE_CASE_SENSITIVE)) != null) { builder.setMask(buffer, caseSensitive.equals(VALUE_TRUE)); } else { builder.setMask(buffer, true); } break; case ELEMENT_IS_HIDDEN: if ((buffer = attributes.getValue(ATTRIBUTE_VALUE)) == null) { return; } builder.setIsHidden(buffer.equals(VALUE_TRUE)); break; case ELEMENT_IS_SYMLINK: if ((buffer = attributes.getValue(ATTRIBUTE_VALUE)) == null) { return; } builder.setIsSymlink(buffer.equals(VALUE_TRUE)); break; case ELEMENT_IS_READABLE: if ((buffer = attributes.getValue(ATTRIBUTE_VALUE)) == null) { return; } builder.setIsReadable(buffer.equals(VALUE_TRUE)); break; case ELEMENT_IS_WRITABLE: if ((buffer = attributes.getValue(ATTRIBUTE_VALUE)) == null) { return; } builder.setIsWritable(buffer.equals(VALUE_TRUE)); break; case ELEMENT_IS_EXECUTABLE: if ((buffer = attributes.getValue(ATTRIBUTE_VALUE)) == null) { return; } builder.setIsExecutable(buffer.equals(VALUE_TRUE)); break; } } } catch(CommandException e) { throw new SAXException(e); } } /** * This method is public as an implementation side effect, but should not be called directly. */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals(ELEMENT_ASSOCIATION) && isInAssociation) { try { builder.endAssociation(); } catch(CommandException e) { throw new SAXException(e); } isInAssociation = false; } } } ================================================ FILE: src/main/java/com/mucommander/command/AssociationWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; import java.io.IOException; import java.io.OutputStream; /** * Class used to write custom associations XML files. *

* AssociationWriter is an {@link AssociationBuilder} that will send * all build messages it receives into an XML stream (as defined in {@link AssociationsXmlConstants}). * * @author Nicolas Rinaudo */ public class AssociationWriter implements AssociationsXmlConstants, AssociationBuilder { /** Where to write the custom command associations to. */ private final XmlWriter out; /** * Builds a new writer that will send data to the specified output stream. * @param stream where to write the XML data. * @throws IOException if an I/O error occurs. */ AssociationWriter(OutputStream stream) throws IOException {out = new XmlWriter(stream);} /** * Opens the root XML element. */ @Override public void startBuilding() throws CommandException { try { out.startElement(ELEMENT_ROOT); out.println(); } catch(IOException e) { throw new CommandException(e); } } /** * Closes the root XML element. */ @Override public void endBuilding() throws CommandException { try { out.endElement(ELEMENT_ROOT); } catch(IOException e) { throw new CommandException(e); } } @Override public void startAssociation(String command) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_COMMAND, command); try { out.startElement(ELEMENT_ASSOCIATION, attr); out.println(); } catch(IOException e) { throw new CommandException(e); } } @Override public void endAssociation() throws CommandException { try { out.endElement(ELEMENT_ASSOCIATION); } catch(IOException e) { throw new CommandException(e); } } @Override public void setMask(String mask, boolean isCaseSensitive) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_VALUE, mask); if (!isCaseSensitive) { attr.add(ATTRIBUTE_CASE_SENSITIVE, VALUE_FALSE); } writeStandaloneElement(attr, ELEMENT_MASK); } @Override public void setIsSymlink(boolean isSymlink) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_VALUE, isSymlink ? VALUE_TRUE : VALUE_FALSE); writeStandaloneElement(attr, ELEMENT_IS_SYMLINK); } @Override public void setIsHidden(boolean isHidden) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_VALUE, isHidden ? VALUE_TRUE : VALUE_FALSE); writeStandaloneElement(attr, ELEMENT_IS_HIDDEN); } @Override public void setIsReadable(boolean isReadable) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_VALUE, isReadable ? VALUE_TRUE : VALUE_FALSE); writeStandaloneElement(attr, ELEMENT_IS_READABLE); } @Override public void setIsWritable(boolean isWritable) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_VALUE, isWritable ? VALUE_TRUE : VALUE_FALSE); writeStandaloneElement(attr, ELEMENT_IS_WRITABLE); } @Override public void setIsExecutable(boolean isExecutable) throws CommandException { XmlAttributes attr = new XmlAttributes(); attr.add(ATTRIBUTE_VALUE, isExecutable ? VALUE_TRUE : VALUE_FALSE); writeStandaloneElement(attr, ELEMENT_IS_EXECUTABLE); } private void writeStandaloneElement(XmlAttributes attr, String name) throws CommandException { try { out.writeStandAloneElement(name, attr); } catch (IOException e) { throw new CommandException(e); } } } ================================================ FILE: src/main/java/com/mucommander/command/AssociationsXmlConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; /** * Defines the structure of a custom associations XML file. *

* This interface is only meant as a convenient way of sharing the XML * file format between the {@link AssociationWriter} and {@link AssociationReader}. It will be removed * at bytecode optimisation time. *

* Associations XML files must match the following DTD: *

 * <!ELEMENT associations (association*)>
 * 
 * <!ELEMENT association (filename?,symlink?,hidden?,readable?,writable?,executable?)>
 * <!ATTLIST association command CDATA #REQUIRED>
 *
 * <!ELEMENT filename EMPTY>
 * <!ATTLIST filename value CDATA                 #REQUIRED>
 * <!ATTLIST filename case_sensitive (true|false) #IMPLIED>
 *
 * <!ELEMENT symlink EMPTY>
 * <!ATTLIST symlink value (true|false) #REQUIRED>
 *
 * <!ELEMENT hidden EMPTY>
 * <!ATTLIST hidden value (true|false) #REQUIRED>
 *
 * <!ELEMENT readable EMPTY>
 * <!ATTLIST readable value (true|false) #REQUIRED>
 *
 * <!ELEMENT writable EMPTY>
 * <!ATTLIST writable value (true|false) #REQUIRED>
 *
 * <!ELEMENT executable EMPTY>
 * <!ATTLIST executable value (true|false) #REQUIRED>
 * 
* * @see AssociationReader * @see AssociationWriter * @author Nicolas Rinaudo */ interface AssociationsXmlConstants { /** Root element. */ String ELEMENT_ROOT = "associations"; /** Custom association definition element. */ String ELEMENT_ASSOCIATION = "association"; String ELEMENT_MASK = "filename"; String ELEMENT_IS_SYMLINK = "symlink"; String ELEMENT_IS_HIDDEN = "hidden"; String ELEMENT_IS_READABLE = "readable"; String ELEMENT_IS_WRITABLE = "writable"; String ELEMENT_IS_EXECUTABLE = "executable"; /** Name of the attribute containing the alias of the command to execute in this association. */ String ATTRIBUTE_COMMAND = "command"; String ATTRIBUTE_VALUE = "value"; String ATTRIBUTE_CASE_SENSITIVE = "case_sensitive"; String VALUE_TRUE = "true"; String VALUE_FALSE = "false"; } ================================================ FILE: src/main/java/com/mucommander/command/Command.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import org.jetbrains.annotations.NotNull; import java.io.File; import java.util.ArrayList; import java.util.List; /** * Compiled shell commands. *

* A command is composed of three elements: *

    *
  • An {@link #getAlias() alias}, used to identify the command through the application.
  • *
  • A {@link #getCommand() command}, which is what will be executed by the instance of Command.
  • *
  • * A {@link #getType() type}, which can be any of {@link CommandType#SYSTEM_COMMAND system} (invisible and immutable), * {@link CommandType#INVISIBLE_COMMAND invisible} (invisible and mutable) or {@link CommandType#NORMAL_COMMAND} (visible and mutable). *
  • *
*

* The basic command syntax is fairly simple: *

    *
  • Any non-escaped \ character will escape the following character and be removed from the tokens.
  • *
  • Any non-escaped " character will escape all characters until the next occurrence of ", except for \.
  • *
  • Non-escaped space characters are used as token separators.
  • *
* It is important to remember that " characters are not removed from the resulting tokens. *

* It's also possible to include keywords in a command: *

    *
  • $f is replaced by a file's full path.
  • *
  • $n is replaced by a file's name.
  • *
  • $e is replaced by a file's extension.
  • *
  • $N is replaced by a file's name without its extension.
  • *
  • $p is replaced by a file's parent's path.
  • *
  • $j is replaced by the path of the folder in which the JVM was started.
  • *
*

* Once a Command instance has been retrieved, execution tokens can be retrieved through the * {@link #getTokens(AbstractFile)} method. This will return a tokenized version of the command and replace any * keyword by the corresponding file value . It's also possible to skip keyword replacement through the {@link #getTokens()} method. *

* A command's executable tokens are typically meant to be used with {@link com.mucommander.process.ProcessRunner#execute(String[], AbstractFile)} * in order to generate instances of {@link com.mucommander.process.AbstractProcess}. * * @author Nicolas Rinaudo * @see CommandManager * @see com.mucommander.process.ProcessRunner * @see com.mucommander.process.AbstractProcess */ public class Command implements Comparable { /** * Header of replacement keywords. */ private static final char KEYWORD_HEADER = '$'; /** * Instances of this keyword will be replaced by the file's full path. */ private static final char KEYWORD_PATH = 'f'; /** * Instances of this keyword will be replaced by the file's name. */ private static final char KEYWORD_NAME = 'n'; /** * Instances of this keyword will be replaced by the file's parent directory. */ private static final char KEYWORD_PARENT = 'p'; /** * Instances of this keyword will be replaced by the JVM's current directory. */ private static final char KEYWORD_VM_PATH = 'j'; /** * Instances of this keyword will be replaced by the file's extension. */ private static final char KEYWORD_EXTENSION = 'e'; /** * Instances of this keyword will be replaced by the file's name without its extension. */ private static final char KEYWORD_NAME_WITHOUT_EXTENSION = 'b'; /** * Command's alias. */ private final String alias; /** * Original command. */ private final String command; /** * Name used to display the command to users. */ private final String displayName; /** * Command type. */ private final CommandType type; /** * Filemask */ private final String fileMask; /** * Creates a new command. * * @param alias alias of the command. * @param command command that will be executed. * @param type type of the command. * @param displayName name of the command as seen by users (if null, defaults to alias). * @param fileMask mask for files specification */ public Command(String alias, String command, CommandType type, String displayName, String fileMask) { this.alias = alias; this.type = type; this.displayName = displayName; this.command = command; this.fileMask = fileMask; } /** * Creates a new command. *

* This is a convenience constructor and is strictly equivalent to calling * * {@link #Command(String, String, CommandType, String, String) * Command(}alias, command, {@link CommandType#NORMAL_COMMAND}, null, null) * . * * @param alias alias of the command. * @param command command that will be executed. */ public Command(String alias, String command) { this(alias, command, CommandType.NORMAL_COMMAND, null, null); } /** * Creates a new command. *

* This is a convenience constructor and is strictly equivalent to calling * {@link #Command(String, String, CommandType, String, String) Command(}alias, command, type, null, null). * * @param alias alias of the command. * @param command command that will be executed. * @param type type of the command. */ public Command(String alias, String command, CommandType type) { this(alias, command, type, null, null); } public Command(Command cmd) { this.alias = cmd.getAlias(); this.type = cmd.getType(); this.displayName = cmd.getDisplayName(); this.command = cmd.getCommand(); this.fileMask = cmd.getFileMask(); } /** * Returns this command's tokens without performing keyword substitution. * * @return this command's tokens without performing keyword substitution. */ public synchronized String[] getTokens() { return getTokens(command, (AbstractFile[]) null); } /** * Returns this command's tokens, replacing keywords by the corresponding values from the specified file. * * @param file file from which to retrieve keyword substitution values. * @return this command's tokens, replacing keywords by the corresponding values from the specified file. */ public synchronized String[] getTokens(AbstractFile file) { return getTokens(command, file); } /** * Returns this command's tokens, replacing keywords by the corresponding values from the specified fileset. * * @param files files from which to retrieve keyword substitution values. * @return this command's tokens, replacing keywords by the corresponding values from the specified fileset. */ public synchronized String[] getTokens(FileSet files) { return getTokens(command, files); } /** * Returns this command's tokens, replacing keywords by the corresponding values from the specified files. * * @param files files from which to retrieve keyword substitution values. * @return this command's tokens, replacing keywords by the corresponding values from the specified files. */ public synchronized String[] getTokens(AbstractFile[] files) { return getTokens(command, files); } /** * Returns the specified command's tokens without performing keyword substitution. * * @param command command to tokenize. * @return the specified command's tokens without performing keyword substitution. */ public static String[] getTokens(String command) { return getTokens(command, (AbstractFile[]) null); } /** * Returns the specified command's tokens after replacing keywords by the corresponding values from the specified file. * * @param command command to tokenize. * @param file file from which to retrieve keyword substitution values. * @return the specified command's tokens after replacing keywords by the corresponding values from the specified file. */ public static String[] getTokens(String command, AbstractFile file) { return getTokens(command, new AbstractFile[]{file}); } /** * Returns the specified command's tokens after replacing keywords by the corresponding values from the specified fileset. * * @param command command to tokenize. * @param files file from which to retrieve keyword substitution values. * @return the specified command's tokens after replacing keywords by the corresponding values from the specified fileset. */ public static String[] getTokens(String command, FileSet files) { return getTokens(command, files.toArray(new AbstractFile[0])); } /** * Returns the specified command's tokens after replacing keywords by the corresponding values from the specified files. * * @param command command to tokenize. * @param files file from which to retrieve keyword substitution values. * @return the specified command's tokens after replacing keywords by the corresponding values from the specified files. */ public static String[] getTokens(String command, AbstractFile[] files) { List tokens = new ArrayList<>(); // All tokens. command = command.trim(); StringBuilder currentToken = new StringBuilder(command.length()); // Buffer for the current token. char[] buffer = command.toCharArray(); // All the characters that compose command. boolean isInQuotes = false; // Whether we're currently within quotes or not. // Parses the command. for (int i = 0; i < command.length(); i++) { // Quote escaping: toggle isInQuotes. if (buffer[i] == '\"') { currentToken.append(buffer[i]); isInQuotes = !isInQuotes; } // Backslash escaping: the next character is not analyzed. else if (buffer[i] == '\\') { if (i + 1 != command.length()) { currentToken.append(buffer[++i]); } } // Whitespace: end of token if we're not between quotes. else if (buffer[i] == ' ' && !isInQuotes) { // Skips un-escaped blocks of spaces. while (i + 1 < command.length() && buffer[i + 1] == ' ') { i++; } // Stores the current token. tokens.add(currentToken.toString()); currentToken.setLength(0); } // Keyword: perform keyword substitution. else if (buffer[i] == KEYWORD_HEADER) { // Skips keyword replacement if we're not interested in it. if (files == null) { currentToken.append(KEYWORD_HEADER); } // If this is the last character, append it. else if (++i == buffer.length) currentToken.append(KEYWORD_HEADER); // If we've found a legal keyword, perform keyword replacement else if (isLegalKeyword(buffer[i])) { // Deals with the first file. currentToken.append(getKeywordReplacement(buffer[i], files[0])); // $j is a special case, we only ever insert it once. if (buffer[i] != KEYWORD_VM_PATH) { // If we're not between quotes and there's more than one file, // each file will be in its own token. if (!isInQuotes && files.length != 1) { tokens.add(currentToken.toString()); currentToken.setLength(0); } // Deals with all subsequent files: // - if we're in quotes, separates each files by a space. // - if we're not in quotes, each new file is its own token. for (int j = 1; j < files.length; j++) { if (isInQuotes) { currentToken.append(' '); currentToken.append(getKeywordReplacement(buffer[i], files[j])); } // When not in quotes, the last file is the beginning of a new token // rather than a single one. else if (j != files.length - 1) tokens.add(getKeywordReplacement(buffer[i], files[j])); else currentToken.append(getKeywordReplacement(buffer[i], files[j])); } } } // If we've found an illegal keyword, ignore it. else { currentToken.append(KEYWORD_HEADER); currentToken.append(buffer[i]); } } // Nothing special about this character. else { currentToken.append(buffer[i]); } } // Adds a possible last token. if (!currentToken.isEmpty()) { tokens.add(currentToken.toString()); } // Empty commands are returned as an empty token rather than an empty array. if (tokens.isEmpty()) { return new String[]{""}; } return tokens.toArray(new String[0]); } /** * Returns whether this command contains keywords referencing selected file. *

* Returns true if command contains keywords referencing selected file, e.g. $f,$n,$p,$e,$b. * Returns false otherwise, e.g. $j, $xyz, etc. * * @return whether this command contains keywords referencing selected file. */ public synchronized boolean hasSelectedFileKeyword() { String[] tokens = getTokens(); for (String token : tokens) { if (token.length() >= 2 && token.charAt(0) == KEYWORD_HEADER) { char ch2 = token.charAt(1); if (ch2 == KEYWORD_PATH || ch2 == KEYWORD_NAME || ch2 == KEYWORD_EXTENSION || ch2 == KEYWORD_NAME_WITHOUT_EXTENSION || ch2 == KEYWORD_PARENT) { return true; } } } // No token with file referencing keyword found return false; } /** * Returns true if the specified character is a legal keyword. * * @param keyword character to check. * @return true if the specified character is a legal keyword, false otherwise. */ private static boolean isLegalKeyword(char keyword) { return keyword == KEYWORD_PATH || keyword == KEYWORD_NAME || keyword == KEYWORD_PARENT || keyword == KEYWORD_VM_PATH || keyword == KEYWORD_EXTENSION || keyword == KEYWORD_NAME_WITHOUT_EXTENSION; } /** * Gets the value from file that should be used to replace keyword. * * @param keyword character to replace. * @param file file from which to retrieve the replacement value. * @return the requested replacement value. */ private static String getKeywordReplacement(char keyword, AbstractFile file) { return switch (keyword) { case KEYWORD_PATH -> file.getAbsolutePath(); case KEYWORD_NAME -> file.getName(); case KEYWORD_PARENT -> { AbstractFile parentFile = file.getParent(); yield parentFile == null ? "" : parentFile.getAbsolutePath(); } case KEYWORD_VM_PATH -> new File(System.getProperty("user.dir")).getAbsolutePath(); case KEYWORD_EXTENSION -> { String extension = file.getExtension(); yield extension != null ? extension : ""; } case KEYWORD_NAME_WITHOUT_EXTENSION -> file.getNameWithoutExtension(); default -> throw new IllegalArgumentException(); }; } /** * Returns the original, un-tokenized command. * * @return the original, un-tokenized command. */ public synchronized String getCommand() { return command; } /** * Returns this command's alias. * * @return this command's alias. */ public synchronized String getAlias() { return alias; } /** * Returns the command's type. * * @return the command's type. */ public synchronized CommandType getType() { return type; } public synchronized String getFileMask() { return fileMask; } /** * Returns the command's display name. *

* If it hasn't been set, returns this command's alias. * * @return the command's display name. */ public synchronized String getDisplayName() { return displayName != null ? displayName : alias; } /** * Returns true if the command's display name has been set. * * @return true if the command's display name has been set, false otherwise. */ synchronized boolean isDisplayNameSet() { return displayName != null; } @Override public int hashCode() { int hashCode; hashCode = alias.hashCode(); hashCode = hashCode * 31 + command.hashCode(); hashCode = hashCode * 31 + getDisplayName().hashCode(); hashCode = hashCode * 31 + type.hashCode(); return hashCode; } @Override public boolean equals(Object object) { if (!(object instanceof Command cmd)) { return false; } return command.equals(cmd.command) && alias.equals(cmd.alias) && type == cmd.type && getDisplayName().equals(cmd.getDisplayName()); } @Override public int compareTo(@NotNull Command command) { int buffer = getDisplayName().compareTo(command.getDisplayName()); if (buffer != 0) { return buffer; } if ((buffer = getAlias().compareTo(command.getAlias())) != 0) { return buffer; } return this.command.compareTo(command.command); } @Override public String toString() { return alias + (displayName == null ? "" : ":" + displayName) + ":" + command + (fileMask != null ? "[" + fileMask + "]" : ""); } } ================================================ FILE: src/main/java/com/mucommander/command/CommandAssociation.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; /** * Associates a command to a set of file filters. * @author Nicolas Rinaudo */ class CommandAssociation { /** Command associated to this file name IMAGE_FILTER. */ private Command command; private FileFilter fileFilter; /** * Creates a new CommandAssociation. * @param command command that must be executed if the association is matched. * @param filter IMAGE_FILTER that files must match in order to be taken into account by the association. */ CommandAssociation(Command command, FileFilter filter) { this.command = command; this.fileFilter = filter; } /** * Returns true if the specified file matches the association. * @param file file to match against the association. * @return true if the specified file matches the association, false otherwise. */ public boolean accept(AbstractFile file) {return fileFilter.match(file);} // - Command retrieval ----------------------------------------------------- // ------------------------------------------------------------------------- /** * Returns the command used in the association. * @return the command used in the association. */ public Command getCommand() {return command;} public FileFilter getFilter() {return fileFilter;} } ================================================ FILE: src/main/java/com/mucommander/command/CommandBuilder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; /** * Used to explore a list of commands in a format independent fashion. *

* Different classes, such as {@link CommandManager} or {@link CommandReader}, know how to * generate a list of commands - from instances loaded in memory or from a file, for example. * Implementing CommandBuilder allows classes to query these lists regardless of * their source. * *

* Instances of CommandBuilder can rely on their methods to be called in the proper order, * and on both their {@link #startBuilding()} and {@link #endBuilding()} methods to be called. Classes that * interact with such instances must make sure this contract is respected. * *

* A (fairly useless) implementation might look like: *

 * public class CommandPrinter implements CommandBuilder {
 *    public void startBuilding() {System.out.println("Beginning command list building...");}
 *
 *    public void endBuilding() {System.out.println("Done.");}
 *
 *    public void addCommand(Command command) throws CommandException {
 *        System.out.println(" - creating command '" + command.getCommand() + "' with alias '" + command.getAlias() + "'");
 *    }
 * }
 * 
* Passing an instance of CommandPrinter to {@link CommandManager#buildCommands(CommandBuilder, CommandType)} * will result in something like: *
 * Beginning command list building...
 * - creating command 'open $f' with alias 'open'
 * - creating command 'open -a Safari $f' with alias 'openURL'
 * Done.
 * 
* * @author Nicolas Rinaudo * @see CommandReader * @see CommandManager#buildCommands(CommandBuilder, CommandType) */ public interface CommandBuilder { /** * Notifies the builder that command building is about to start. * @throws CommandException if an error occurs. */ void startBuilding() throws CommandException; /** * Notifies the builder that command building is finished. * @throws CommandException if an error occurs. */ void endBuilding() throws CommandException; /** * Notifies the builder that a new command has been found. * @param command command that has been found. * @throws CommandException if an error occurs. */ void addCommand(Command command) throws CommandException; } ================================================ FILE: src/main/java/com/mucommander/command/CommandException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; /** * Exception thrown when errors occur while building custom commands. * @author Nicolas Rinaudo */ public class CommandException extends Exception { /** * Builds a new exception. */ public CommandException() {super();} /** * Builds a new exception with the specified message. * @param message exception's message. */ public CommandException(String message) {super(message);} /** * Builds a new exception with the specified cause. * @param cause exception's cause. */ public CommandException(Throwable cause) {super(cause);} /** * Builds a new exception with the specified message and cause. * @param message exception's message. * @param cause exception's cause. */ public CommandException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/command/CommandManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.*; import com.mucommander.commons.file.filter.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.PermissionTypes; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.io.backup.BackupOutputStream; /** * Manages custom commands and associations. * @author Nicolas Rinaudo */ public class CommandManager implements CommandBuilder { private static Logger logger; // - Built-in commands ----------------------------------------------------- // ------------------------------------------------------------------------- /** Alias for the system file opener. */ public static final String FILE_OPENER_ALIAS = "open"; /** Alias for the system URL opener. */ public static final String URL_OPENER_ALIAS = "openURL"; /** Alias for the system file manager. */ public static final String FILE_MANAGER_ALIAS = "openFM"; /** Alias for the system executable file opener. */ public static final String EXE_OPENER_ALIAS = "openEXE"; /** Alias for the default text viewer. */ public static final String VIEWER_ALIAS = "view"; /** Alias for the default text editor. */ public static final String EDITOR_ALIAS = "edit"; /** Alias for the default command prompt. */ public final static String CMD_OPENER_ALIAS = "openCmd"; // - Self-open command ----------------------------------------------------- // ------------------------------------------------------------------------- /** Alias of the 'init as executable' command. */ private static final String RUN_AS_EXECUTABLE_ALIAS = "execute"; /** Command used to init a file as an executable. */ private static final Command RUN_AS_EXECUTABLE_COMMAND = new Command(RUN_AS_EXECUTABLE_ALIAS, "$f", CommandType.SYSTEM_COMMAND); // - Association definitions ----------------------------------------------- // ------------------------------------------------------------------------- /** System dependent file associations. */ private static final List systemAssociations = new ArrayList<>(); /** All known file associations. */ private static final List associations = new ArrayList<>(); /** Path to the custom association file, null if the default one should be used. */ private static AbstractFile associationFile; /** Whether the associations were modified since the last time they were saved. */ private static boolean wereAssociationsModified; /** Default name of the association XML file. */ private static final String DEFAULT_ASSOCIATION_FILE_NAME = "associations.xml"; // - Commands definition --------------------------------------------------- // ------------------------------------------------------------------------- /** Default name of the custom commands file. */ private static final String DEFAULT_COMMANDS_FILE_NAME = "commands.xml"; /** All known commands. */ private static Map> commands = new HashMap<>(); /** Path to the custom commands XML file, null if the default one should be used. */ private static AbstractFile commandsFile; /** Whether the custom commands have been modified since the last time they were saved. */ private static boolean wereCommandsModified; /** Default command used when no other command is found for a specific file type. */ private static Command defaultCommand; /** * Prevents instances of CommandManager from being created. */ private CommandManager() {} // - Command handling ------------------------------------------------------ // ------------------------------------------------------------------------- /** * Returns the tokens that compose the command that must be executed to open the specified file. *

* This is a convenience method and is strictly equivalent to calling * {@link #getTokensForFile(AbstractFile,boolean) getTokensForFile(}file, true). * * @param file file for which the opening command's tokens must be returned. * @return the tokens that compose the command that must be executed to open the specified file. */ public static String[] getTokensForFile(AbstractFile file) { return getTokensForFile(file, true); } /** * Returns the tokens that compose the command that must be executed to open the specified file. * @param file file for which the opening command's tokens must be returned. * @param allowDefault whether to use the default command if none was found to match the specified file. * @return the tokens that compose the command that must be executed to open the specified file, null if not found. */ public static String[] getTokensForFile(AbstractFile file, boolean allowDefault) { Command command = getCommandForFile(file, allowDefault); return command == null ? null : command.getTokens(file); } /** * Returns the command that must be executed to open the specified file. *

* This is a convenience method and is stricly equivalent to calling * {@link #getCommandForFile(AbstractFile,boolean) getCommandForFile(}file, true). * * @param file file for which the opening command must be returned. * @return the command that must be executed to open the specified file. */ public static Command getCommandForFile(AbstractFile file) { return getCommandForFile(file, true); } private static Command getCommandForFile(AbstractFile file, List associations) { for (CommandAssociation association : associations) { if (association.accept(file)) return association.getCommand(); } return null; } /** * Returns the command that must be executed to open the specified file. * @param file file for which the opening command must be returned. * @param allowDefault whether to use the default command if none was found to match the specified file. * @return the command that must be executed to open the specified file, null if not found. */ public static Command getCommandForFile(AbstractFile file, boolean allowDefault) { Command command = getCommandForFile(file, associations); // Goes through all known associations and checks whether file matches any. if (command != null) { return command; } // Goes through all system associations and checks whether file matches any. command = getCommandForFile(file, systemAssociations); if (command != null) { return command; } // We haven't found a command explicitly associated with 'file', but we might have a generic file opener if (defaultCommand != null) { return defaultCommand; } // We don't have a generic file opener, return the 'self execute' command if we're allowed. if (allowDefault) { return RUN_AS_EXECUTABLE_COMMAND; } return null; } /** * Returns a sorted collection of all registered commands. * @return a sorted collection of all registered commands. */ public static Collection commands() { // Copy the registered commands to a new list List list = new ArrayList<>(); for (List lst : commands.values()) { list.addAll(lst); } Collections.sort(list); return list; } /** * Returns the command associated with the specified alias. * @param alias alias whose associated command should be returned. * @return the command associated with the specified alias if found, null otherwise. */ public static Command getCommandForAlias(String alias, AbstractFile file) { List list = commands.get(alias); if (list == null || list.isEmpty()) { return null; } // if we have command with specified filemask then return it for (Command cmd : list) { if (checkFileMask(cmd, file)) { return cmd; } } // else if we have command with empty filemask (default command) then return it for (Command cmd : list) { String fileMask = cmd.getFileMask(); if (fileMask == null || fileMask.isEmpty()) { return cmd; } } return null; } /** * * @param cmd command * @param file file to check * @return true if file corresponds to command filemask */ public static boolean checkFileMask(Command cmd, AbstractFile file) { String fileMask = cmd.getFileMask(); if (fileMask == null || fileMask.isEmpty()) { return false; } String[] split = fileMask.split(","); for (String aSplit : split) { String mask = aSplit.trim().toLowerCase(); if (mask.isEmpty()) { continue; } WildcardFileFilter filter = new WildcardFileFilter(mask); if (filter.accept(file)) { return true; } } return false; } private static void setDefaultCommand(Command command) { if (defaultCommand == null && command.getAlias().equals(FILE_OPENER_ALIAS)) { getLogger().debug("Registering '" + command.getCommand() + "' as default command."); defaultCommand = command; } } private static void registerCommand(Command command, boolean mark) { // Registers the command and marks command as having been modified. setDefaultCommand(command); getLogger().debug("Registering '" + command.getCommand() + "' as '" + command.getAlias() + "'"); final String alias = command.getAlias(); if (!commands.containsKey(alias)) { commands.put(alias, new ArrayList<>()); } commands.get(alias).add(command); if (mark) { wereCommandsModified = true; } // Command oldCommand = commands.put(command.getAlias(), command); // if (mark && !command.equals(oldCommand)) { // wereCommandsModified = true; // } } public static void registerDefaultCommand(Command command) throws CommandException { registerCommand(command, false); } /** * Registers the specified command at the end of the command list. * @param command command to register. */ public static void registerCommand(Command command) { registerCommand(command, true); } // - Associations handling ------------------------------------------------- // ------------------------------------------------------------------------- /** * Registers the specified association. * @param command command to execute when the association is matched. * @param filter file filters that a file must match to be accepted by the association. * @throws CommandException if an error occurs. */ public static void registerAssociation(String command, FileFilter filter) throws CommandException { associations.add(createAssociation(command, filter)); } private static CommandAssociation createAssociation(String cmd, FileFilter filter) throws CommandException { Command command = getCommandForAlias(cmd, null); if (command == null) { getLogger().debug("Failed to create association as '" + command + "' is not known."); throw new CommandException(cmd + " not found"); } return new CommandAssociation(command, filter); } public static void registerDefaultAssociation(String command, FileFilter filter) throws CommandException { systemAssociations.add(createAssociation(command, filter)); } // - Command builder code -------------------------------------------------- // ------------------------------------------------------------------------- /** * This method is public as an implementation side effect and must not be called directly. */ public void addCommand(Command command) { registerCommand(command, false); } /** * Passes all known custom commands to the specified builder. *

* This method guarantees that the builder's {@link CommandBuilder#startBuilding() startBuilding()} and * {@link CommandBuilder#endBuilding() endBuilding()} methods will both be called even if an error occurs. * If that happens however, it is entirely possible that not all commands will be passed to * the builder. * * @param builder object that will receive commands list building messages. * @param type if not null then build only commands with specified type * @throws CommandException if anything goes wrong. */ public static void buildCommands(CommandBuilder builder, CommandType type) throws CommandException { builder.startBuilding(); try { // Goes through all the registered commands. for (Command command : commands()) { if (type == null || command.getType() == type) { builder.addCommand(command); } } } finally { builder.endBuilding(); } } // - Associations building ------------------------------------------------- // ------------------------------------------------------------------------- private static void buildFilter(FileFilter filter, AssociationBuilder builder) throws CommandException { // Filter on the file type. if (filter instanceof AttributeFileFilter) { AttributeFileFilter attributeFilter = (AttributeFileFilter)filter; switch (attributeFilter.getAttribute()) { case HIDDEN: builder.setIsHidden(!attributeFilter.isInverted()); break; case SYMLINK: builder.setIsSymlink(!attributeFilter.isInverted()); break; } } else if (filter instanceof PermissionsFileFilter) { PermissionsFileFilter permissionFilter = (PermissionsFileFilter)filter; switch(permissionFilter.getPermission()) { case PermissionTypes.READ_PERMISSION: builder.setIsReadable(permissionFilter.getFilter()); break; case PermissionTypes.WRITE_PERMISSION: builder.setIsWritable(permissionFilter.getFilter()); break; case PermissionTypes.EXECUTE_PERMISSION: builder.setIsExecutable(permissionFilter.getFilter()); break; } } else if (filter instanceof RegexpFilenameFilter) { RegexpFilenameFilter regexpFilter = (RegexpFilenameFilter)filter; builder.setMask(regexpFilter.getRegularExpression(), regexpFilter.isCaseSensitive()); } } /** * Passes all known file associations to the specified builder. *

* This method guarantees that the builder's {@link AssociationBuilder#startBuilding() startBuilding()} and * {@link AssociationBuilder#endBuilding() endBuilding()} methods will both be called even if an error occurs. * If that happens however, it is entirely possible that not all associations will be passed to * the builder. * * @param builder object that will receive association list building messages. * @throws CommandException if anything goes wrong. */ public static void buildAssociations(AssociationBuilder builder) throws CommandException { builder.startBuilding(); // Goes through all the registered associations. try { for (CommandAssociation current : associations) { builder.startAssociation(current.getCommand().getAlias()); FileFilter filter = current.getFilter(); if (filter instanceof ChainedFileFilter) { Iterator filters = ((ChainedFileFilter)filter).getFileFilterIterator(); while (filters.hasNext()) { buildFilter(filters.next(), builder); } } else { buildFilter(filter, builder); } builder.endAssociation(); } } finally { builder.endBuilding(); } } // - Associations reading/writing ------------------------------------------ // ------------------------------------------------------------------------- /** * Returns the path to the custom associations XML file. *

* This method cannot guarantee the file's existence, and it's up to the caller * to deal with the fact that the user might not actually have created custom * associations. *

* This method's return value can be modified through {@link #setAssociationFile(String)}. * If this wasn't called, the default path will be used: {@link #DEFAULT_ASSOCIATION_FILE_NAME} * in the {@link com.mucommander.PlatformManager#getPreferencesFolder() preferences} folder. * * @return the path to the custom associations XML file. * @see #setAssociationFile(String) * @see #loadAssociations() * @see #writeAssociations() * @throws IOException if there was an error locating the default commands file. */ public static AbstractFile getAssociationFile() throws IOException { if (associationFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_ASSOCIATION_FILE_NAME); } return associationFile; } /** * Sets the path to the custom associations file. *

* This is a convenience method and is strictly equivalent to calling setAssociationFile(FileFactory.getFile(file)). * * @param path path to the custom associations file. * @throws FileNotFoundException if file is not accessible. * @see #getAssociationFile() * @see #loadAssociations() * @see #writeAssociations() */ public static void setAssociationFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setAssociationFile(new File(path)); } else { setAssociationFile(file); } } /** * Sets the path to the custom associations file. *

* This is a convenience method and is strictly equivalent to calling setAssociationFile(FileFactory.getFile(file.getAbsolutePath())). * * @param file path to the custom associations file. * @throws FileNotFoundException if file is not accessible. * @see #getAssociationFile() * @see #loadAssociations() * @see #writeAssociations() */ public static void setAssociationFile(File file) throws FileNotFoundException { setAssociationFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the custom associations file. * @param file path to the custom associations file. * @throws FileNotFoundException if file is not accessible. * @see #getAssociationFile() * @see #loadAssociations() * @see #writeAssociations() */ public static void setAssociationFile(AbstractFile file) throws FileNotFoundException { if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } associationFile = file; } /** * Loads the custom associations XML File. *

* The command files will be loaded as a backed-up file (see {@link BackupInputStream}). * Its format is described {@link AssociationsXmlConstants here}. * * @throws IOException if an IO error occurs. * @throws CommandException thrown when errors occur while building custom commands * @see #writeAssociations() * @see #getAssociationFile() * @see #setAssociationFile(String) */ public static void loadAssociations() throws IOException, CommandException { AbstractFile file = getAssociationFile(); getLogger().debug("Loading associations from file: " + file.getAbsolutePath()); // Tries to load the associations file. // Associations are not considered to be modified by this. //InputStream in = null; try (InputStream in = new BackupInputStream(file)) { AssociationReader.read(in, new AssociationFactory()); } finally { wereAssociationsModified = false; } } /**, * Writes all registered associations to the custom associations file. *

* Data will be written to the path returned by {@link #getAssociationFile()}. Note, however, * that this method will not actually do anything if the association list hasn't been modified * since the last time it was saved. *

* The association files will be saved as a backed-up file (see {@link BackupOutputStream}). * Its format is described {@link AssociationsXmlConstants here}. * * @throws IOException if an I/O error occurs. * @throws CommandException if an error occurs. * @see #loadAssociations() * @see #getAssociationFile() * @see #setAssociationFile(String) */ public static void writeAssociations() throws CommandException, IOException { // Do not save the associations if they were not modified. if (!wereAssociationsModified) { getLogger().debug("Custom file associations not modified, skip saving."); return; } getLogger().debug("Writing associations to file: " + getAssociationFile()); // Writes the associations. try (BackupOutputStream out = new BackupOutputStream(getAssociationFile())) { buildAssociations(new AssociationWriter(out)); wereAssociationsModified = false; } } // - Commands reading/writing ---------------------------------------------- // ------------------------------------------------------------------------- /** * Returns the path to the custom commands XML file. *

* This method cannot guarantee the file's existence, and it's up to the caller * to deal with the fact that the user might not actually have created custom * commands. *

* This method's return value can be modified through {@link #setCommandFile(String)}. * If this wasn't called, the default path will be used: {@link #DEFAULT_COMMANDS_FILE_NAME} * in the {@link com.mucommander.PlatformManager#getPreferencesFolder() preferences} folder. * * @return the path to the custom commands XML file. * @see #setCommandFile(String) * @see #loadCommands() * @see #writeCommands() * @throws IOException if there was some error locating the default commands file. */ public static AbstractFile getCommandFile() throws IOException { if (commandsFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_COMMANDS_FILE_NAME); } return commandsFile; } /** * Sets the path to the custom commands file. *

* This is a convenience method and is strictly equivalent to calling setCommandFile(FileFactory.getFile(file));. * * @param path path to the custom commands file. * @throws FileNotFoundException if file is not accessible. * @see #getCommandFile() * @see #loadCommands() * @see #writeCommands() */ public static void setCommandFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setCommandFile(new File(path)); } else { setCommandFile(file); } } /** * Sets the path to the custom commands file. *

* This is a convenience method and is strictly equivalent to calling setCommandFile(FileFactory.getFile(file.getAbsolutePath()));. * * @param file path to the custom commands file. * @throws FileNotFoundException if file is not accessible. * @see #getCommandFile() * @see #loadCommands() * @see #writeCommands() */ public static void setCommandFile(File file) throws FileNotFoundException {setCommandFile(FileFactory.getFile(file.getAbsolutePath()));} /** * Sets the path to the custom commands file. * @param file path to the custom commands file. * @throws FileNotFoundException if file is not accessible. * @see #getCommandFile() * @see #loadCommands() * @see #writeCommands() */ public static void setCommandFile(AbstractFile file) throws FileNotFoundException { // Makes sure file can be used as a command file. if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } commandsFile = file; } /** * Writes Normal registered commands to the custom commands file. *

* Data will be written to the path returned by {@link #getCommandFile()}. Note, however, * that this method will not actually do anything if the command list hasn't been modified * since the last time it was saved. *

* The command files will be saved as a backed-up file (see {@link BackupOutputStream}). * Its format is described {@link CommandsXmlConstants here}. * * @throws IOException if an I/O error occurs. * @throws CommandException if an error occurs. * @see #loadCommands() * @see #getCommandFile() * @see #setCommandFile(String) */ public static void writeCommands() throws IOException, CommandException { // Only saves the command if they were modified since the last time they were written. if (!wereCommandsModified) { getLogger().debug("Custom commands not modified, skip saving."); return; } getLogger().debug("Writing custom commands to file: " + getCommandFile()); // Writes the commands. try (BackupOutputStream out = new BackupOutputStream(getCommandFile())) { buildCommands(new CommandWriter(out), CommandType.NORMAL_COMMAND); wereCommandsModified = false; } } /** * Loads the custom commands XML File. *

* The command files will be loaded as a backed-up file (see {@link BackupInputStream}). * Its format is described {@link CommandsXmlConstants here}. * * @throws IOException if an I/O error occurs. * @see #writeCommands() * @see #getCommandFile() * @see #setCommandFile(String) */ public static void loadCommands() throws IOException, CommandException { AbstractFile file = getCommandFile(); getLogger().debug("Loading custom commands from: " + file.getAbsolutePath()); // Tries to load the commands file. // Commands are not considered to be modified by this. try (InputStream in = new BackupInputStream(file)) { CommandReader.read(in, new CommandManager()); } finally { wereCommandsModified = false; } } /* private static void registerDefaultCommand(String alias, String command, String display) { if(getCommandForAlias(alias) == null) { if(command != null) { // try {registerCommand(CommandParser.getCommand(alias, command, Command.SYSTEM_COMMAND, display));} try {registerCommand(new Command(alias, command, Command.SYSTEM_COMMAND, display));} catch(Exception e) {AppLogger.fine("Failed to register " + command + ": " + e.getMessage());} } } } */ // - Unused methods -------------------------------------------------------- // ------------------------------------------------------------------------- /** * This method is public as an implementation side effect and must not be called directly. */ public void startBuilding() {} /** * This method is public as an implementation side effect and must not be called directly. */ public void endBuilding() {} public static List getCommands(String type) { if (!commands.containsKey(type)) { commands.put(type, new ArrayList<>()); } return commands.get(type); } /** * Removes all user defined commands with Normal type. * Called before updating list in editor dialog */ public static void removeAllNormalCommands() { for (String type : commands.keySet()) { List list = commands.get(type); list.removeIf(cmd -> cmd.getType() == CommandType.NORMAL_COMMAND); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(CommandManager.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/command/CommandReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.InputStream; /** * Class used to parse custom commands XML files. *

* Command file parsing is done through the {@link #read(InputStream,CommandBuilder) read} method, which is * the only way to interact with this class. *

* Note that while this class knows how to read the content of a command XML file, its role is not to interpret it. This * is done by instances of {@link CommandBuilder}. * * @see CommandsXmlConstants * @see CommandBuilder * @see CommandWriter * @author Nicolas Rinaudo */ public class CommandReader extends DefaultHandler implements CommandsXmlConstants { /** Where to send building messages. */ private final CommandBuilder builder; /** * Creates a new command reader. * @param b where to send custom command events. */ private CommandReader(CommandBuilder b) { builder = b; } /** * Parses the content of the specified input stream. *

* This method will go through the specified input stream and notify the builder of any new command declaration it * encounters. Note that parsing is done in a very lenient fashion, and perfectly invalid XML files might not raise * an exception. This is not a flaw in the parser, and both allows muCommander to be error resilient and the commands * file format to be extended without having to rewrite most of this code. *

* Note that even if an error occurs, both of the builder's {@link CommandBuilder#startBuilding()} and * {@link CommandBuilder#endBuilding()} methods will still be called. Parsing will stop at the first error * however, so while the builder is guaranteed to receive correct messages, it might not receive all declared * commands. * * @param in where to read command data from. * @param b where to send building events to. * @throws IOException thrown if any error occurs. */ public static void read(InputStream in, CommandBuilder b) throws CommandException, IOException { b.startBuilding(); try { SAXParserFactory.newInstance().newSAXParser().parse(in, new CommandReader(b)); } catch(ParserConfigurationException | SAXException e) { throw new CommandException(e); } finally { b.endBuilding(); } } /** * This method is public as an implementation side effect and should not be called directly. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // New custom command declaration. if (qName.equals(ELEMENT_COMMAND)) { String alias = attributes.getValue(ATTRIBUTE_ALIAS); String command = attributes.getValue(ATTRIBUTE_VALUE); String fileMask = attributes.getValue(ATTRIBUTE_FILEMASK); // Makes sure the required attributes are there. if (alias != null && command != null) { CommandType type = CommandType.parseCommandType(attributes.getValue(ATTRIBUTE_TYPE)); String display = attributes.getValue(ATTRIBUTE_DISPLAY); // Creates the command and passes it to the builder. try { builder.addCommand(new Command(alias, command, type, display, fileMask)); } catch (CommandException e) { throw new SAXException(e); } } } } } ================================================ FILE: src/main/java/com/mucommander/command/CommandType.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; /** * Types of compiled shell commands. * * @author Arik Hadas */ public enum CommandType { /** Describes normal commands. */ NORMAL_COMMAND, /** Describes system commands. */ SYSTEM_COMMAND(CommandsXmlConstants.VALUE_SYSTEM), /** Describes invisible commands. */ INVISIBLE_COMMAND(CommandsXmlConstants.VALUE_INVISIBLE); private String value; CommandType() { } CommandType(String value) { this(); this.value = value; } /** * Returns the CommandType value according to specified String representation. *

* Note that this method is not strict in the arguments it receives: *

    *
  • If type equals {CommandsXmlConstants#VALUE_SYSTEM}, {@link #SYSTEM_COMMAND} will be returned.
  • *
  • If type equals {CommandsXmlConstants#VALUE_INVISIBLE}, {@link #INVISIBLE_COMMAND} will be returned.
  • *
  • In any other case, {@link #NORMAL_COMMAND} will be returned.
  • *
* * @param value String representation of type to analyze. * @return type's integer equivalent. */ public static CommandType parseCommandType(String value) { if (value == null) { return NORMAL_COMMAND; } for (CommandType type : CommandType.values()) { if (value.equals(type.value)) { return type; } } return NORMAL_COMMAND; } @Override public String toString() { return value; } } ================================================ FILE: src/main/java/com/mucommander/command/CommandWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; import java.io.IOException; import java.io.OutputStream; /** * Class used to write custom commands XML files. *

* CommandWriter is a {@link CommandBuilder} that will send * all build messages it receives into an XML stream (as defined in {@link CommandsXmlConstants}). * * @author Nicolas Rinaudo */ public class CommandWriter implements CommandsXmlConstants, CommandBuilder { /** Where to write the custom command associations to. */ private final XmlWriter out; /** * Builds a new writer that will send data to the specified output stream. * @param stream where to write the XML data. * @throws IOException if an IO error occurs. */ CommandWriter(OutputStream stream) throws IOException { out = new XmlWriter(stream); } /** * Opens the root XML element. */ public void startBuilding() throws CommandException { try { out.startElement(ELEMENT_ROOT); out.println(); } catch(IOException e) { throw new CommandException(e); } } /** * Closes the root XML element. */ public void endBuilding() throws CommandException { try { out.endElement(ELEMENT_ROOT); } catch(IOException e) { throw new CommandException(e); } } /** * Writes the specified command's XML description. * @param command command that should be written. * @throws CommandException if an error occurs. */ public void addCommand(Command command) throws CommandException { // Builds the XML description of the command. XmlAttributes attributes = new XmlAttributes(); attributes.add(ATTRIBUTE_ALIAS, command.getAlias()); attributes.add(ATTRIBUTE_VALUE, command.getCommand()); attributes.add(ATTRIBUTE_FILEMASK, command.getFileMask()); if (command.getType().toString() != null) { attributes.add(ATTRIBUTE_TYPE, command.getType().toString()); } if (command.isDisplayNameSet()) { attributes.add(ATTRIBUTE_DISPLAY, command.getDisplayName()); } // Writes the XML description. try { out.writeStandAloneElement(ELEMENT_COMMAND, attributes); } catch(IOException e) { throw new CommandException(e); } } } ================================================ FILE: src/main/java/com/mucommander/command/CommandsXmlConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; /** * Defines the structure of a custom commands XML file. *

* This interface is only meant as a convenient way of sharing the XML * file format between the {@link com.mucommander.command.CommandWriter} * and {@link CommandReader}. It will be removed at bytecode optimisation time. *

* Commands XML files must match the following DTD: *

 * <!ELEMENT commands (association*)>
 * 
 * <!ELEMENT command EMPTY>
 * <!ATTLIST command value   CDATA              #REQUIRED>
 * <!ATTLIST command alias   CDATA              #REQUIRED>
 * <!ATTLIST command type    (system|invisible) #IMPLIED>
 * <!ATTLIST command display CDATA              #IMPLIED>
 * 
* Where: *
    *
  • value is the command's value, in a format that can be understood by the {@link CommandReader}.
  • *
  • alias is the name under which the command will be known throughout trolCommander.
  • *
  • type is the command's type (system, invisible or normal). See {@link Command} for more information.
  • *
* * @see CommandReader * @see CommandWriter * @author Nicolas Rinaudo */ interface CommandsXmlConstants { /** Root element. */ String ELEMENT_ROOT = "commands"; /** Custom command definition element. */ String ELEMENT_COMMAND = "command"; /** Name of the attribute containing a command's display name. */ String ATTRIBUTE_DISPLAY = "display"; /** Name of the attribute containing a command's alias. */ String ATTRIBUTE_ALIAS = "alias"; /** Name of the attribute containing a command's value. */ String ATTRIBUTE_VALUE = "value"; String ATTRIBUTE_FILEMASK = "filemask"; /** Name of the attribute containing a command's type. */ String ATTRIBUTE_TYPE = "type"; /** Describes system commands. */ String VALUE_SYSTEM = "system"; /** Describes invisible commands. */ String VALUE_INVISIBLE = "invisible"; } ================================================ FILE: src/main/java/com/mucommander/command/PermissionsFileFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.PermissionAccesses; import com.mucommander.commons.file.PermissionTypes; import com.mucommander.commons.file.filter.AbstractFileFilter; /** * Filter on a file's permissions. * @author Nicolas Rinaudo */ public class PermissionsFileFilter extends AbstractFileFilter implements PermissionTypes, PermissionAccesses { private final int permission; private final boolean filter; /** * Creates a new PermissionsFileFilter. * @param permission permission that will be checked against as defined in {@link com.mucommander.commons.file.FilePermissions}. * @param filter whether the specified permission flag must be set for a file to be accepted. */ PermissionsFileFilter(int permission, boolean filter) { this.permission = permission; this.filter = filter; } public boolean accept(AbstractFile file) { return filter == file.getPermissions().getBitValue(USER_ACCESS, permission); } /** * Returns the permission that this IMAGE_FILTER will check. * @return the permission that this IMAGE_FILTER will check. */ public int getPermission() { return permission; } /** * Returns true if files must have the IMAGE_FILTER's permission flag set in order to be accepted. * @return true if files must have the IMAGE_FILTER's permission flag set in order to be accepted, false otherwise. */ public boolean getFilter() { return filter; } } ================================================ FILE: src/main/java/com/mucommander/command/package.html ================================================ Framework used to run system commands and associate them to file types. ================================================ FILE: src/main/java/com/mucommander/commons/DummyDecoratedFile.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons; import com.mucommander.commons.file.DummyFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.runtime.OsFamily; /** * * This class is a child of DummyFile with overwritten {@link #toString()}} method that return a "nice" value * for local files (without 'file://localhost/') * * @author Oleg Trifonov */ public class DummyDecoratedFile extends DummyFile { private static final String LOCAL_FILE_PREFIX; static { String prefix = "file://" + FileURL.LOCALHOST; LOCAL_FILE_PREFIX = OsFamily.WINDOWS.isCurrent() ? prefix + '/' : prefix; } public DummyDecoratedFile(FileURL url) { super(url); } @Override public String toString() { String result = super.toString(); if (result.startsWith(LOCAL_FILE_PREFIX)) { return result.substring(LOCAL_FILE_PREFIX.length()); } return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/HasProgress.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons; /** * @author Oleg Trifonov * Created on 05/07/16. */ public interface HasProgress { int getProgress(); boolean hasProgress(); } ================================================ FILE: src/main/java/com/mucommander/commons/collections/AlteredVector.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.collections; import java.util.Collection; import java.util.Vector; import java.util.WeakHashMap; /** * AlteredVector is a Vector that is able to notify registered listeners whenever its contents has changed. *

* Events are triggered when: *

    *
  • one or more elements has been added *
  • one or more elements has been removed *
  • an element has been changed *
* *

It is however not aware of modifications that are made to the contained objects themselves. * * @author Maxence Bernard */ public class AlteredVector extends Vector { /** Contains all registered listeners, stored as weak references */ private final WeakHashMap listeners = new WeakHashMap<>(); public AlteredVector() { super(); } public AlteredVector(Collection collection) { super(collection); } public AlteredVector(int initialCapacity, int capacityIncrement) { super(initialCapacity, capacityIncrement); } public AlteredVector(int initialCapacity) { super(initialCapacity); } /** * Adds the specified VectorChangeListener to the list of registered listeners. * *

Listeners are stored as weak references so {@link #removeVectorChangeListener(VectorChangeListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the VectorChangeListener to add to the list of registered listeners. * @see #removeVectorChangeListener(VectorChangeListener) */ public void addVectorChangeListener(VectorChangeListener listener) { listeners.put(listener, null); } /** * Removes the specified VectorChangeListener from the list of registered listeners. * * @param listener the VectorChangeListener to remove from the list of registered listeners. * @see #addVectorChangeListener(VectorChangeListener) */ public void removeVectorChangeListener(VectorChangeListener listener) { listeners.remove(listener); } /** * This method is called when one or more elements has been added to this AlteredVector to notify listeners. * * @param startIndex index at which the first element has been added * @param nbAdded number of elements added */ private void fireElementsAddedEvent(int startIndex, int nbAdded) { for (VectorChangeListener listener : listeners.keySet()) { listener.elementsAdded(startIndex, nbAdded); } } /** * This method is called when one or more elements has been removed from this AlteredVector to notify listeners. * * @param startIndex index at which the first element has been removed * @param nbRemoved number of elements removed */ private void fireElementsRemovedEvent(int startIndex, int nbRemoved) { for (VectorChangeListener listener : listeners.keySet()) { listener.elementsRemoved(startIndex, nbRemoved); } } /** * This method is called when an element has been changed in this AlteredVector to notify listeners. * * @param index index of the element that has been changed */ private void fireElementChangedEvent(int index) { for (VectorChangeListener listener : listeners.keySet()) { listener.elementChanged(index); } } @Override public void setElementAt(E o, int i) { super.setElementAt(o, i); fireElementChangedEvent(i); } @Override public E set(int i, E o) { o = super.set(i, o); fireElementChangedEvent(i); return o; } @Override public void insertElementAt(E o, int i) { super.insertElementAt(o, i); fireElementsAddedEvent(i, 1); } @Override public void add(int i, E o) { insertElementAt(o, i); fireElementsAddedEvent(i, 1); } @Override public void addElement(E o) { super.addElement(o); fireElementsAddedEvent(size()-1, 1); } @Override public boolean add(E o) { addElement(o); fireElementsAddedEvent(size()-1, 1); return true; } @Override public boolean addAll(Collection collection) { int sizeBefore = size(); boolean b = super.addAll(collection); fireElementsAddedEvent(sizeBefore, size()-sizeBefore); return b; } @Override public boolean addAll(int i, Collection collection) { int sizeBefore = size(); boolean b = super.addAll(i, collection); fireElementsAddedEvent(i, size()-sizeBefore); return b; } @Override public void removeElementAt(int i) { super.removeElementAt(i); fireElementsRemovedEvent(i, 1); } @Override public E remove(int i) { E o = super.remove(i); fireElementsRemovedEvent(i, 1); return o; } @Override public boolean removeElement(Object o) { int index = indexOf(o); if(index==-1) return false; removeElementAt(index); return true; } @Override public boolean remove(Object o) { return removeElement(o); } @Override public void removeAllElements() { int sizeBefore = size(); super.removeAllElements(); fireElementsRemovedEvent(0, sizeBefore); } @Override public void clear() { removeAllElements(); } } ================================================ FILE: src/main/java/com/mucommander/commons/collections/Enumerator.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.collections; import java.util.Enumeration; import java.util.Iterator; import java.util.NoSuchElementException; /** * Converts an Enumeration into an Iterator. * @author Nicolas Rinaudo */ public class Enumerator implements Iterator { /** Enumeration wrapped by this Enumerator. */ private final Enumeration enumeration; /** * Creates a new enumerator from the specified enumeration. * @param e enumeration that needs to be treated as an iterator. */ public Enumerator(Enumeration e) { enumeration = e; } /** * Returns true if the iterator has more elements. * (In other words, returns true if {@link #next() next} would return an element rather than throwing an exception.) * @return true if the iterator has more elements, false otherwise. */ @Override public boolean hasNext() { return enumeration.hasMoreElements(); } /** * Returns the next element in the iteration. * @return the next element in the iteration. * @throws NoSuchElementException if there is no next element in the iteration. */ @Override public T next() throws NoSuchElementException { return enumeration.nextElement(); } /** * Operation not supported. * @throws UnsupportedOperationException whenever this method is called. */ @Override public void remove() { throw new UnsupportedOperationException(); } } ================================================ FILE: src/main/java/com/mucommander/commons/collections/VectorChangeListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.collections; /** * Interface to be implemented by classes that wish to be notified of changes made to an {@link AlteredVector}. * *

Those classes need to be registered as listeners to receive those events, this can be done by calling * {@link AlteredVector#addVectorChangeListener(VectorChangeListener)}. * * @author Maxence Bernard */ public interface VectorChangeListener { /** * This method is called when one or more elements has been added to the AlteredVector. * * @param startIndex index at which the first element has been added * @param nbAdded number of elements added */ void elementsAdded(int startIndex, int nbAdded); /** * This method is called when one or more elements has been removed from the AlteredVector. * * @param startIndex index at which the first element has been removed * @param nbRemoved number of elements removed */ void elementsRemoved(int startIndex, int nbRemoved); /** * This method is called when an element has been changed in the AlteredVector. * * @param index index of the element that has been changed */ void elementChanged(int index); } ================================================ FILE: src/main/java/com/mucommander/commons/conf/BufferedConfigurationExplorer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.util.Stack; /** * Helper class meant for instances of {@link Configuration} to explore their own configuration tree. *

* This behaves exactly as a {@link ConfigurationExplorer}, but keeps track of its own path. This is meant * for instances of {@link Configuration} to prune empty branches. * * @author Nicolas Rinaudo */ class BufferedConfigurationExplorer extends ConfigurationExplorer { /** Sections that have been passed through. */ private final Stack sections = new Stack<>(); /** * Creates a new explorer on the specified section. * @param root section from which to start exploring. */ BufferedConfigurationExplorer(ConfigurationSection root) { super(root); } /** * Returns true if there are more sections in the history. * @return true if there are more sections in the history. */ boolean hasSections() { return !sections.empty(); } /** * Returns the next section in history. * @return the next section in history. */ ConfigurationSection popSection() { return sections.pop(); } /** * Move to the specified section. * @param name name of the current section's subsection in which to move. * @param create if true and name doesn't exist, it will be created. * @return true if we could move to name, false otherwise. */ @Override public boolean moveTo(String name, boolean create) { if (super.moveTo(name, create)) { sections.push(getSection()); return true; } return false; } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/Configuration.java ================================================ package com.mucommander.commons.conf; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.*; /** * Base class for all configuration related tasks. *

* A Configuration instance's main goal is to act as a configuration data repository. * Once created, it can be used to {@link #getVariable(String) retrieve}, {@link #removeVariable(String) delete} * and {@link #setVariable(String,String) set} configuration variables. * *

Naming conventions

*

* Configuration variable names follow the same convention as Java System properties: a serie of strings * separated by periods. By convention, all but the last string are called configuration sections, while * the last one is the variable's name. When we refer to a variable's fully qualified name, we're talking * about the whole period-separated name.
* For example, startup_folder.right.last_folder is interpreted as a variable called * last_folder contained in a section called right, itself contained in * another section called startup_folder.
* *

Variable types

*

* While the com.mucommander.commons.conf really only handles one type of variables, strings, it offers * tools to cast them as primitive Java types (int, long, float, double, boolean). This is done through the use * of the various primitive types' class implementation parseXXX method.
* When a variable hasn't been set but ant attempt is made to cast it, the standard Java default value will * be returned: *

    *
  • String: null
  • *
  • Integer: 0
  • *
  • Long: 0
  • *
  • Float: 0
  • *
  • Double: 0
  • *
  • Boolean: false
  • *
* *

Configuration file format

*

* By default, configuration data is assumed to be in the standard muCommander file format (described in * {@link XmlConfigurationReader}). However, application writers can modify that to any format they want * through the {@link #setReaderFactory(ConfigurationReaderFactory) setReaderFactory} and * {@link #setWriterFactory(ConfigurationWriterFactory) setWriterFactory} methods. * *

Configuration data location

*

* While Configuration provides read and write methods that accept streams as parameters, it's * also possible to set the data source once and for all and let the API deal with the details. This can * be achieved through the {@link #setSource(ConfigurationSource) setSource} method.
* Note that a default implementation, {@link FileConfigurationSource}, is provided. It covers the most * common case of configuration sources, a local configuration file.
* For application writers who wish to be able to retrieve configuration files through a variety of file systems, * we suggest creating a source using the com.mucommander.file API. * *

Monitoring configuration changes

*

* Classes that need to monitor the state of the configuration in order, for example, to react to changes * dynamically rather than wait for an application reboot can implement the {@link ConfigurationListener} * interface and register themselves through * {@link #addConfigurationListener(ConfigurationListener) addConfigurationListener}. This guarantees that they * will receive configuration events whenever a modification occurs.
* Note that LISTENERS are stored as weak references, meaning that application writers must ensure that they keep * direct references to the listener instances they register if they do not want them to be garbaged collected * out of existence randomly. * * @author Nicolas Rinaudo */ public class Configuration { /** Used to get access to the configuration source's input and output streams. */ private ConfigurationSource source; /** Used to create objects that will read from the configuration source. */ private ConfigurationReaderFactory readerFactory; /** Used to create objects that will write to the configuration source. */ private ConfigurationWriterFactory writerFactory; /** Holds the content of the configuration file. */ private final ConfigurationSection root = new ConfigurationSection(); /** Contains all registered configuration LISTENERS, stored as weak references. */ private final WeakHashMap LISTENERS = new WeakHashMap<>(); /** Used to synchronize concurrent access of the configuration source. */ private final Object sourceLock = new Object(); /** Used to synchronize concurrent access of the reader factory. */ private final Object readerLock = new Object(); /** Used to synchronie concurrent access of the writer factory. */ private final Object writerLock = new Object(); /** * Creates a new instance of Configuration. *

* The resulting instance will use default {@link XmlConfigurationReader readers} and * {@link XmlConfigurationWriter writers}. *

* Note that until the {@link #setSource(ConfigurationSource) setSource} method has been * invoked, calls to read or write methods without a stream parameter will fail. */ public Configuration() { } /** * Creates a new instance of Configuration using the specified source. *

* The resulting instance will use the default {@link XmlConfigurationReader readers} and * {@link XmlConfigurationWriter writers}. * * @param source where the resulting instance will look for its configuration data. */ public Configuration(ConfigurationSource source) { setSource(source); } /** * Creates a new instance of Configuration using the specified format. *

* Note that until the {@link #setSource(ConfigurationSource) setSource} method has been * invoked, calls to read or write methods without a stream parameter will fail. * * @param reader factory for configuration readers. * @param writer factory for configuration writers. */ public Configuration(ConfigurationReaderFactory reader, ConfigurationWriterFactory writer) { setReaderFactory(reader); setWriterFactory(writer); } /** * Creates a new instance of Configuration using the specified source and format. * @param source where the resulting instance will look for its configuration data. * @param reader factory for configuration readers. * @param writer factory for configuration writers. */ public Configuration(ConfigurationSource source, ConfigurationReaderFactory reader, ConfigurationWriterFactory writer) { setSource(source); setReaderFactory(reader); setWriterFactory(writer); } /** * Sets the source that will be used to read and write configuration information. * @param s new configuration source. * @see #getSource() */ public void setSource(ConfigurationSource s) { synchronized(sourceLock) { source = s; } } /** * Returns the current configuration source. * @return the current configuration source, or null if it hasn't been set. * @see #setSource(ConfigurationSource) */ public ConfigurationSource getSource() { synchronized(sourceLock) { return source; } } /** * Sets the factory that will be used to create {@link ConfigurationReader reader} instances. *

* In order to reset the configuration to its default reader factory, application writers can call this method * with a null parameter. * * @param f factory that will be used to create reader instances. * @see #getReaderFactory() */ void setReaderFactory(ConfigurationReaderFactory f) { synchronized(readerLock) { readerFactory = f; } } /** * Returns the factory that is being used to create {@link ConfigurationReader reader} instances. *

* By default, this method will return an {@link XmlConfigurationReader XML reader} factory. * This can be modified by calling {@link #setReaderFactory(ConfigurationReaderFactory) setReaderFactory}. * * @return the factory that is being used to create reader instances. * @see #setReaderFactory(ConfigurationReaderFactory) */ private ConfigurationReaderFactory getReaderFactory() { synchronized(readerLock) { if (readerFactory == null) { return XmlConfigurationReader.FACTORY; } return readerFactory; } } /** * Sets the factory that will be used to create writer instances. *

* In order to reset the configuration to its default writer factory, application writers can call * this method will a null parameter. * * @param f factory that will be used to create writer instances. * @see #getWriterFactory() */ void setWriterFactory(ConfigurationWriterFactory f) { synchronized(writerLock) { writerFactory = f; } } /** * Returns the factory that is being used to create writer instances. *

* By default, this method will return an {@link XmlConfigurationWriter} factory. However, this * can be modified by calling {@link #setWriterFactory(ConfigurationWriterFactory) setWriterFactory}. * * @return the factory that is being used to create writer instances. * @see #setWriterFactory(ConfigurationWriterFactory) */ private ConfigurationWriterFactory getWriterFactory() { synchronized(writerLock) { if (writerFactory == null) { return XmlConfigurationWriter.FACTORY; } return writerFactory; } } /** * Loads configuration from the specified input stream, using the specified configuration reader. * @param in where to read the configuration from. * @param reader reader that will be used to interpret the content of in. * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a configuration error occurs. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @see #read(InputStream) * @see #read(ConfigurationReader) * @see #read() */ synchronized void read(Reader in, ConfigurationReader reader) throws IOException, ConfigurationException { reader.read(in, new ConfigurationLoader(root)); } /** * Loads configuration from the specified input stream. *

* This method will use the configuration reader set by {@link #setReaderFactory(ConfigurationReaderFactory)} if any, * or an {@link XmlConfigurationReader} instance if not. * * @param in where to read the configuration from. * @throws ConfigurationException if a configuration error occurs. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @throws ReaderConfigurationException if the {@link ConfigurationReaderFactory} isn't properly configured. * @throws IOException if an I/O error occurs. * @see #read() * @see #read(ConfigurationReader) * @see #read(Reader,ConfigurationReader) * @deprecated Application developers should use {@link #read(Reader)} instead. This method assumes the specified * {@link InputStream} to be UTF-8 encoded. */ @Deprecated public void read(InputStream in) throws ConfigurationException, IOException { read(new InputStreamReader(in, StandardCharsets.UTF_8), getReaderFactory().getReaderInstance()); } /** * Loads configuration from the specified reader. *

* This method will use the configuration reader set by {@link #setReaderFactory(ConfigurationReaderFactory)} if any, * or an {@link XmlConfigurationReader} instance if not. * * @param in where to read the configuration from. * @throws ConfigurationException if a configuration error occurs. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @throws ReaderConfigurationException if the {@link ConfigurationReaderFactory} isn't properly configured. * @throws IOException if an I/O error occurs. * @see #read() * @see #read(ConfigurationReader) * @see #read(Reader,ConfigurationReader) */ public void read(Reader in) throws ConfigurationException, IOException { read(in, getReaderFactory().getReaderInstance()); } /** * Loads configuration using the specified configuration reader. *

* This method will use the input stream provided by {@link #setSource(ConfigurationSource)} if any, or * fail otherwise. * * @param reader reader that will be used to interpret the content of in. * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a configuration error occurs. * @throws SourceConfigurationException if no {@link ConfigurationSource} has been set. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @see #read(InputStream) * @see #read() * @see #read(Reader,ConfigurationReader) */ public void read(ConfigurationReader reader) throws IOException, ConfigurationException { ConfigurationSource source = getSource(); // Configuration source. // Makes sure the configuration source has been properly set. if (source == null) { throw new SourceConfigurationException("Configuration source hasn't been set."); } // Reads the configuration data. try (Reader in = source.getReader()) { read(in, reader); } } /** * Loads configuration. *

* If a reader has been specified through {@link #setReaderFactory(ConfigurationReaderFactory)}, it * will be used to analyse the configuration. Otherwise, an {@link XmlConfigurationReader} * instance will be used. * *

* If a configuration source has been specified through {@link #setSource(ConfigurationSource)}, it will be * used. Otherwise, this method will fail. * * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a configuration error occurs. * @throws SourceConfigurationException if no {@link ConfigurationSource} hasn't been set. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @throws ReaderConfigurationException if the {@link ConfigurationReaderFactory} isn't properly configured. * @see #write() * @see #read(InputStream) * @see #read(ConfigurationReader) * @see #read(Reader,ConfigurationReader) */ public void read() throws ConfigurationException, IOException { read(getReaderFactory().getReaderInstance()); } /** * Writes the configuration to the specified {@link Writer}. *

* This method will use {@link #getWriterFactory()} to create instances of configuration writer. * * @param out where to write the configuration to. * @throws ConfigurationException if any error occurs. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @throws WriterConfigurationException if the {@link ConfigurationWriterFactory} isn't properly configured. * @see #read(InputStream) * @see #write() */ public void write(Writer out) throws ConfigurationException { write(getWriterFactory().getWriterInstance(out)); } /** * Writes the configuration. *

* If a configuration source was specified through {@link #setSource(ConfigurationSource)}, it will be used * to open an output stream. Otherwise, this method will fail. * * @throws ConfigurationException if any error occurs. * @throws SourceConfigurationException if no {@link ConfigurationSource} has been set. * @throws ConfigurationFormatException if a syntax error occurs in the configuration data. * @throws ConfigurationStructureException if the configuration data doesn't describe a valid configuration tree. * @throws IOException if any I/O error occurs. * @see #read(ConfigurationReader) * @see #write() */ public void write() throws IOException, ConfigurationException { ConfigurationSource source = getSource(); // Configuration source. // Makes sure the source has been set. if (source == null) { throw new SourceConfigurationException("No configuration source has been set"); } try (Writer out = source.getWriter()) { write(out); } } /** * Writes the configuration data to the specified builder. * @param builder object that will receive configuration building messages. * @throws ConfigurationException if any error occurs while going through the configuration tree. */ public void write(ConfigurationBuilder builder) throws ConfigurationException { builder.startConfiguration(); build(builder, root); builder.endConfiguration(); } /** * Recursively explores the specified section and invokes the specified builder's callback methods. * @param builder object that will receive building events. * @param root section to explore. * @throws ConfigurationException if any error occurs. */ private synchronized void build(ConfigurationBuilder builder, ConfigurationSection root) throws ConfigurationException { // Explores the section's variables. Set variables = root.variableNames(); // Enumeration on the section's variables, then subsections. for (String name : variables) { builder.addVariable(name, root.getVariable(name)); } // Explores the section's subsections. Set sectionNames = root.sectionNames(); for (String name : sectionNames) { ConfigurationSection section = root.getSection(name); // We only go through subsections if contain either variables or subsections of their own. if (section.hasSections() || section.hasVariables()) { builder.startSection(name); build(builder, section); builder.endSection(name); } } } // - Variable setting ---------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Moves the value of fromVar to toVar. *

* At the end of this call, fromVar will have been deleted. Note that if fromVar doesn't * exist, but toVar does, toVar will be deleted. * *

* This method might trigger as many as two {@link ConfigurationEvent events}: *

    *
  • One when fromVar is removed.
  • *
  • One when toVar is set.
  • *
* The removal event will always be triggered first. * * @param fromVar fully qualified name of the variable to rename. * @param toVar fully qualified name of the variable that will receive fromVar's value. */ public void renameVariable(String fromVar, String toVar) { setVariable(toVar, removeVariable(fromVar)); } /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. Note that this doesn't * mean the call failed, but that name's value was already equal to value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed * to all LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getVariable(String) * @see #getVariable(String,String) */ public synchronized boolean setVariable(String name, String value) { ConfigurationExplorer explorer = new ConfigurationExplorer(root); // Used to navigate to the variable's parent section. // Moves to the parent section. String buffer = moveToParent(explorer, name, true); // Buffer for the variable's name trimmed of section information. // If the variable's value was actually modified, triggers an event. if (explorer.getSection().setVariable(buffer, value)) { triggerEvent(new ConfigurationEvent(this, name, value)); return true; } return false; } /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. This, however, is not a * way of indicating that the call failed: false is only ever returned if the previous value is equal * to the new value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed * to all LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getIntegerVariable(String) * @see #getVariable(String,int) */ public boolean setVariable(String name, int value) { return setVariable(name, ConfigurationSection.getValue(value)); } /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. This, however, is not a * way of indicating that the call failed: false is only ever returned if the previous value is equal * to the new value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed to all * LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @param separator string used to separate each element of the list. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getListVariable(String,String) * @see #getVariable(String,List,String) */ public boolean setVariable(String name, List value, String separator) { return setVariable(name, ConfigurationSection.getValue(value, separator)); } /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. This, however, is not * a way of indicating that the call failed: false is only ever returned if the previous value is equal * to the new value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed * to all LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getFloatVariable(String) * @see #getVariable(String,float) */ public boolean setVariable(String name, float value) {return setVariable(name, ConfigurationSection.getValue(value));} /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. This, however, is not a * way of indicating that the call failed: false is only ever returned if the previous value is equal * to the new value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed * to all LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getBooleanVariable(String) * @see #getVariable(String,boolean) */ public boolean setVariable(String name, boolean value) { return setVariable(name, ConfigurationSection.getValue(value)); } /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. This, however, is not a * way of indicating that the call failed: false is only ever returned if the previous value is equal * to the new value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed * to all LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getLongVariable(String) * @see #getVariable(String,long) */ public boolean setVariable(String name, long value) { return setVariable(name, ConfigurationSection.getValue(value)); } /** * Sets the value of the specified variable. *

* This method will return false if it didn't modify name's value. This, however, is not a * way of indicating that the call failed: false is only ever returned if the previous value is equal * to the new value. * *

* If the value of the specified variable is actually modified, an {@link ConfigurationEvent event} will be passed * to all LISTENERS. * * @param name fully qualified name of the variable to set. * @param value new value for the variable. * @return true if this call resulted in a modification of the variable's value, * false otherwise. * @see #getDoubleVariable(String) * @see #getVariable(String,double) */ public boolean setVariable(String name, double value) {return setVariable(name, ConfigurationSection.getValue(value));} /** * Returns the value of the specified variable. * @param name fully qualified name of the variable whose value should be retrieved. * @return the variable's value if set, null otherwise. * @see #setVariable(String,String) * @see #getVariable(String,String) */ public synchronized String getVariable(String name) { // If the variable's 'path' doesn't exist, return null. ConfigurationExplorer explorer = new ConfigurationExplorer(root); // Used to navigate to the variable's parent section. name = moveToParent(explorer, name, false); return name == null ? null : explorer.getSection().getVariable(name); } /** * Returns the value of the specified variable as a {@link ValueList}. * @param name fully qualified name of the variable whose value should be retrieved. * @param separator character used to split the variable's value into a list. * @return the variable's value if set, null otherwise. * @see #setVariable(String,List,String) * @see #getVariable(String,List,String) */ public ValueList getListVariable(String name, String separator) { return ConfigurationSection.getListValue(getVariable(name), separator); } /** * Returns the value of the specified variable as an integer. * @param name fully qualified name of the variable whose value should be retrieved. * @return the variable's value if set, 0 otherwise. * @throws NumberFormatException if the variable's value cannot be cast to an integer. * @see #setVariable(String,int) * @see #getVariable(String,int) */ public int getIntegerVariable(String name) { return ConfigurationSection.getIntegerValue(getVariable(name)); } /** * Returns the value of the specified variable as a long. * @param name fully qualified name of the variable whose value should be retrieved. * @return the variable's value if set, 0 otherwise. * @throws NumberFormatException if the variable's value cannot be cast to a long. * @see #setVariable(String,long) * @see #getVariable(String,long) */ public long getLongVariable(String name) { return ConfigurationSection.getLongValue(getVariable(name)); } /** * Returns the value of the specified variable as a float. * @param name fully qualified name of the variable whose value should be retrieved. * @return the variable's value if set, 0 otherwise. * @throws NumberFormatException if the variable's value cannot be cast to a float. * @see #setVariable(String,float) * @see #getVariable(String,float) */ public float getFloatVariable(String name) { return ConfigurationSection.getFloatValue(getVariable(name)); } /** * Returns the value of the specified variable as a double. * @param name fully qualified name of the variable whose value should be retrieved. * @return the variable's value if set, 0 otherwise. * @throws NumberFormatException if the variable's value cannot be cast to a double. * @see #setVariable(String,double) * @see #getVariable(String,double) */ public double getDoubleVariable(String name) { return ConfigurationSection.getDoubleValue(getVariable(name)); } /** * Returns the value of the specified variable as a boolean. * @param name fully qualified name of the variable whose value should be retrieved. * @return the variable's value if set, false otherwise. * @see #setVariable(String,boolean) * @see #getVariable(String,boolean) */ public boolean getBooleanVariable(String name) { return ConfigurationSection.getBooleanValue(getVariable(name)); } /** * Checks whether the specified variable has been set. * @param name fully qualified name of the variable to check for. * @return true if the variable is set, false otherwise. */ public boolean isVariableSet(String name) { return getVariable(name) != null; } /** * Prunes dead branches from the specified configuration tree. * @param explorer used to backtrack through the configuration tree. */ private void prune(BufferedConfigurationExplorer explorer) { // If we're at the root level, nothing to prune. if (!explorer.hasSections()) { return; } ConfigurationSection current = explorer.popSection(); // Look for branches to prune until we've either found a non-empty one // or reached the root of the three. while (current.isEmpty() && current != root) { // Gets the current section's parent and prune. ConfigurationSection parent = explorer.hasSections() ? explorer.popSection() : root; parent.removeSection(current); current = parent; } } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @return the variable's old value, or null if it wasn't set. */ public synchronized String removeVariable(String name) { BufferedConfigurationExplorer explorer = new BufferedConfigurationExplorer(root); // Used to navigate to the variable's parent section. String buffer = moveToParent(explorer, name, false); // Buffer for the variable's name trimmed of section information. // If the variable's 'path' doesn't exist, return null. if (buffer == null) { return null; } // If the variable was actually set, triggers an event. if ((buffer = explorer.getSection().removeVariable(buffer)) != null) { prune(explorer); triggerEvent(new ConfigurationEvent(this, name, null)); } return buffer; } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @param separator character used to split the variable's value into a list. * @return the variable's old value, or null if it wasn't set. */ public ValueList removeListVariable(String name, String separator) { return ConfigurationSection.getListValue(removeVariable(name), separator); } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @return the variable's old value, or 0 if it wasn't set. */ public int removeIntegerVariable(String name) { return ConfigurationSection.getIntegerValue(removeVariable(name)); } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @return the variable's old value, or 0 if it wasn't set. */ public long removeLongVariable(String name) { return ConfigurationSection.getLongValue(removeVariable(name)); } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @return the variable's old value, or 0 if it wasn't set. */ public float removeFloatVariable(String name) { return ConfigurationSection.getFloatValue(removeVariable(name)); } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @return the variable's old value, or 0 if it wasn't set. */ public double removeDoubleVariable(String name) { return ConfigurationSection.getDoubleValue(removeVariable(name)); } /** * Deletes the specified variable from the configuration. *

* If the variable was set, a configuration {@link ConfigurationEvent event} will be passed to * all registered LISTENERS. * * @param name name of the variable to remove. * @return the variable's old value, or false if it wasn't set. */ public boolean removeBooleanVariable(String name) { return ConfigurationSection.getBooleanValue(removeVariable(name)); } /** * Remove all variables and sub-sections under the root section */ public void clear() { root.clear(); } // - Advanced variable retrieval ----------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Retrieves the value of the specified variable. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if name is not set. * @return the specified variable's value. * @see #setVariable(String,String) * @see #getVariable(String) */ public synchronized String getVariable(String name, String defaultValue) { ConfigurationExplorer explorer = new ConfigurationExplorer(root); // Used to navigate to the variable's parent section. // Navigates to the parent section. We do not have to check for null values here, // as the section will be created if it doesn't exist. String buffer = moveToParent(explorer, name, true); // Buffer for the variable's name trimmed of section information. // If the variable isn't set, set it to defaultValue and triggers an event. String value = explorer.getSection().getVariable(buffer); // Buffer for the variable's value. if (value == null) { explorer.getSection().setVariable(buffer, defaultValue); triggerEvent(new ConfigurationEvent(this, name, defaultValue)); return defaultValue; } return value; } /** * Retrieves the value of the specified variable as a {@link ValueList}. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if variable name is not set. * @param separator separator to use for defaultValue if variable name is not set. * @return the specified variable's value. * @see #setVariable(String,List,String) * @see #getListVariable(String,String) */ public ValueList getVariable(String name, List defaultValue, String separator) { return ConfigurationSection.getListValue(getVariable(name, ConfigurationSection.getValue(defaultValue, separator)), separator); } /** * Retrieves the value of the specified variable as an integer. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if name is not set. * @return the specified variable's value. * @throws NumberFormatException if the variable's value cannot be cast to an integer. * @see #setVariable(String,int) * @see #getIntegerVariable(String) */ public int getVariable(String name, int defaultValue) { return ConfigurationSection.getIntegerValue(getVariable(name, ConfigurationSection.getValue(defaultValue))); } /** * Retrieves the value of the specified variable as a long. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if name is not set. * @return the specified variable's value. * @throws NumberFormatException if the variable's value cannot be cast to a long. * @see #setVariable(String,long) * @see #getLongVariable(String) */ public long getVariable(String name, long defaultValue) { return ConfigurationSection.getLongValue(getVariable(name, ConfigurationSection.getValue(defaultValue))); } /** * Retrieves the value of the specified variable as a float. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if name is not set. * @return the specified variable's value. * @throws NumberFormatException if the variable's value cannot be cast to a float. * @see #setVariable(String,float) * @see #getFloatVariable(String) */ public float getVariable(String name, float defaultValue) { return ConfigurationSection.getFloatValue(getVariable(name, ConfigurationSection.getValue(defaultValue))); } /** * Retrieves the value of the specified variable as a boolean. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if name is not set. * @return the specified variable's value. * @see #setVariable(String,boolean) * @see #getBooleanVariable(String) */ public boolean getVariable(String name, boolean defaultValue) { return ConfigurationSection.getBooleanValue(getVariable(name, ConfigurationSection.getValue(defaultValue))); } /** * Retrieves the value of the specified variable as a double. *

* If the variable isn't set, this method will set it to defaultValue before * returning it. If this happens, a configuration {@link ConfigurationEvent event} will * be sent to all registered LISTENERS. * * @param name name of the variable to retrieve. * @param defaultValue value to use if name is not set. * @return the specified variable's value. * @throws NumberFormatException if the variable's value cannot be cast to a double. * @see #setVariable(String,double) * @see #getDoubleVariable(String) */ public double getVariable(String name, double defaultValue) { return ConfigurationSection.getDoubleValue(getVariable(name, ConfigurationSection.getValue(defaultValue))); } // - Helper methods ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Navigates the specified explorer to the parent section of the specified variable. * @param root where to start exploring from. * @param name name of the variable to seek. * @param create whether the path to the variable should be created if it doesn't exist. * @return the name of the variable trimmed of section information, null if not found. */ private String moveToParent(ConfigurationExplorer root, String name, boolean create) { // Goes through each element of the path. StringTokenizer parser = new StringTokenizer(name, "."); while (parser.hasMoreTokens()) { // If we've reached the variable's name, return it. name = parser.nextToken(); if (!parser.hasMoreTokens()) return name; // If we've reached a dead-end, return null. if (!root.moveTo(name, create)) { return null; } } return name; } // - Configuration listening --------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Adds the specified object to the list of registered configuration LISTENERS. * @param listener object to register as a configuration listener. * @see #removeConfigurationListener(ConfigurationListener) */ public void addConfigurationListener(ConfigurationListener listener) { LISTENERS.put(listener, null);} /** * Removes the specified object from the list of registered configuration LISTENERS. * @param listener object to remove from the list of registered configuration LISTENERS. * @see #addConfigurationListener(ConfigurationListener) */ public void removeConfigurationListener(ConfigurationListener listener) { LISTENERS.remove(listener);} /** * Passes the specified event to all registered configuration LISTENERS. * @param event event to propagate. */ private void triggerEvent(ConfigurationEvent event) { for (ConfigurationListener listener : LISTENERS.keySet()) { listener.configurationChanged(event); } } /** * Returns the configuration's root section. * @return the configuration's root section. */ ConfigurationSection getRoot() { return root; } /** * Used to load configuration. * @author Nicolas Rinaudo */ private class ConfigurationLoader implements ConfigurationBuilder { /** Parents of {@link #currentSection}. */ private Stack sections; /** Fully qualified names of {@link #currentSection}. */ private Stack sectionNames; /** Section that we're currently building. */ private ConfigurationSection currentSection; /** * Creates a new configuration loader. * @param root where to create the configuration in. */ ConfigurationLoader(ConfigurationSection root) { currentSection = root; } /** * Initializes the configuration building. */ public void startConfiguration() { sections = new Stack<>(); sectionNames = new Stack<>(); } /** * Ends the configuration building. * @throws ConfigurationException if not all opened sections have been closed. */ public void endConfiguration() throws ConfigurationException { // Makes sure currentSection is the root section. if (!sections.empty()) { throw new ConfigurationStructureException("Not all sections have been closed."); } sections = null; sectionNames = null; } /** * Creates a new sub-section to the current section. * @param name name of the new section. */ public void startSection(String name) { ConfigurationSection buffer; buffer = currentSection.addSection(name); sections.push(currentSection); if (sectionNames.empty()) { sectionNames.push(name + '.'); } else { sectionNames.push(sectionNames.peek() + name + '.'); } currentSection = buffer; } /** * Ends the current section. * @param name name of the section that's being closed. * @throws ConfigurationException if we're not closing a legal section. */ public void endSection(String name) throws ConfigurationException { ConfigurationSection buffer; // Makes sure there is a section to close. try { buffer = sections.pop(); sectionNames.pop(); } catch(EmptyStackException e) { throw new ConfigurationStructureException("Section " + name + " was already closed."); } // Makes sure we're closing the right section. if (buffer.getSection(name) != currentSection) { throw new ConfigurationStructureException("Section " + name + " is not the currently opened section."); } currentSection = buffer; } /** * Adds the specified variable to the current section. * @param name name of the variable. * @param value value of the variable. */ public void addVariable(String name, String value) { // If the variable's value was modified, trigger an event. if (currentSection.setVariable(name, value)) { if (sectionNames.empty()) { triggerEvent(new ConfigurationEvent(Configuration.this, name, value)); } else { triggerEvent(new ConfigurationEvent(Configuration.this, sectionNames.peek() + name, value)); } } } } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationBuilder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Receive notification of the logical structure of a {@link Configuration configuration} instance. *

* If a class needs to be informed of the logical structure of a configuration instance, * it implements this interface and registers an instance with the {@link Configuration} using * its {@link Configuration#write(ConfigurationBuilder) build} method. The {@link Configuration} * uses the instance to report configuration related events such as the start of sections and * variable declarations. * *

* The com.mucommander.commons.conf API comes with a default no-op implementation, * {@link DefaultConfigurationBuilder}. This can be used instead of ConfigurationBuilder * when only a subset of the possible events are of interest. * * @author Nicolas Rinaudo * @see DefaultConfigurationBuilder */ public interface ConfigurationBuilder { /** * Receives notification at the beginning of the configuration. *

* This method will only be invoked once, before any other method in this interface. * * @throws ConfigurationException any Configuration error, possibly wrapping another exception. */ void startConfiguration() throws ConfigurationException; /** * Receives notification at the end of the configuration. *

* This method will be invoked at most once, and if it is, it will be the last one. If an * unrecoverable error happens, this method might never be called. * * @throws ConfigurationException any Configuration error, possibly wrapping another exception. */ void endConfiguration() throws ConfigurationException; /** * Receives notification at the beginning of a section. *

* This method will be invoked once at the beginning of every configuration section. Unless an * unrecoverable error happens, there will be an {@link #endSection(String) endSection} event for every * startSection event, even if the section is empty. All the section's content will be * reported, in order, before the corresponding {@link #endSection(String) endSection} event. * * @param name name of the new section. * @throws ConfigurationException any Configuration error, possibly wrapping another exception. */ void startSection(String name) throws ConfigurationException; /** * Receives notification at the end of a section. *

* This method will be invoked once at the end of every configuration section. There will be a * corresponding {@link #startSection(String) startSection} event for every endSection * event, even if the section is empty. * * @param name name of the finished section. * @throws ConfigurationException any Configuration error, possibly wrapping another exception. */ void endSection(String name) throws ConfigurationException; /** * Receives notification of variable definition. *

* This method will be invoked once per variable found in a section. The declared variable * will always belong to the section defined in the last {@link #startSection(String) startSection} * event which hasn't yet been closed by an {@link #endSection(String) endSection} event. If there is * no such section, the variable belongs to the unnamed root section. * * @param name name of the new variable. * @param value value of the new variable. * @throws ConfigurationException any Configuration error, possibly wrapping another exception. */ void addVariable(String name, String value) throws ConfigurationException; } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationEvent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * An event which indicates that configuration has been modified. *

* The event is passed to every {@link ConfigurationListener} object which registered to receive such events using the * {@link Configuration}'s {@link Configuration#addConfigurationListener(ConfigurationListener) addConfigurationListener} method. * Each such listener object gets this ConfigurationEvent when the event occurs. * * @see ConfigurationListener * @see Configuration * @author Nicolas Rinaudo */ public class ConfigurationEvent { /** Name of the variable that has been modified. */ private final String name; /** Variable's new value. */ private final String value; /** Configuration to which the event relates. */ private final Configuration configuration; /** * Creates a new configuration event. *

* The event will describe a modification of variable name in configuration * configuration, and indicate that it has been set to value. * *

* null is an accepted value for parameter value, and will be * interpreted to mean that the variable has been deleted. * * @param configuration configuration to which the event relates. * @param name name of the variable that was modified. * @param value value of the variable that was modified. */ public ConfigurationEvent(Configuration configuration, String name, String value) { this.name = name; this.value = value; this.configuration = configuration; } /** * Returns the configuration to which the event relates. * @return the configuration to which the event relates. */ public Configuration getConfiguration() { return configuration; } /** * Returns the name of the variable that was modified. *

* The returned value will be the variable's fully qualified name. If, for example, the * modified variable is test.somevar, this is what this method will return, * not somevar. * * @return the name of the variable that was modified. */ public String getVariable() { return name; } /** * Returns the new value for the modified variable. *

* If the variable has been deleted, this method will return null. * * @return the new value for the modified variable. */ public String getValue() { return value; } /** * Returns the new value for the modified variable cast as an integer. *

* If the variable has been deleted, this method will return 0. * * @return the new value for the modified variable. * @throws NumberFormatException if {@link #getValue()} cannot be cast as an integer. */ public int getIntegerValue() throws NumberFormatException { return ConfigurationSection.getIntegerValue(value); } /** * Returns the new value for the modified variable cast as a float. *

* If the variable has been deleted, this method will return 0. * * @return the new value for the modified variable. * @throws NumberFormatException if {@link #getValue()} cannot be cast as a float */ public float getFloatValue() throws NumberFormatException { return ConfigurationSection.getFloatValue(value); } /** * Returns the new value for the modified variable cast as a boolean. *

* If the variable has been deleted, this method will return false. * * @return the new value for the modified variable. */ public boolean getBooleanValue() { return ConfigurationSection.getBooleanValue(value); } /** * Returns the new value for the modified variable as a long. *

* If the variable has been deleted, this method will return 0. * * @return the new value for the modified variable. * @throws NumberFormatException if {@link #getValue()} cannot be cast as a long. */ public long getLongValue() throws NumberFormatException { return ConfigurationSection.getLongValue(value); } /** * Returns the new value for the modified variable cast as a double. *

* If the variable has been deleted, this method will return 0. * * @return the new value for the modified variable. * @throws NumberFormatException if {@link #getValue()} cannot be cast as a double. */ public double getDoubleValue() { return ConfigurationSection.getDoubleValue(value); } /** * Returns the new value for the modified variable cast as a {@link ValueList}. *

* If the variable has been deleted, this method will return null. * * @param separator string used to tokenise the variable's value. * @return the new value for the modified variable. */ public ValueList getListValue(String separator) { return ConfigurationSection.getListValue(value, separator); } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Encapsulates a general configuration error. *

* This class can contain basic error information from either the com.mucommander.commons.conf API * or the application. Application writers can subclass it to provide additional functionality. Different * classes of the com.mucommander.commons.conf API may throw this exception or any exception subclassed * from it. *

* If the application needs to pass through other types of exceptions, it must wrap them in a * ConfigurationException or an exception derived from it. * * @author Nicolas Rinaudo */ public class ConfigurationException extends Exception { /** * Creates a new configuration exception. * @param message the error message. */ public ConfigurationException(String message) {super(message);} /** * Creates a new configuration exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the ConfigurationException. * * @param cause the exception to be wrapped in a ConfigurationException. */ public ConfigurationException(Throwable cause) {super(cause);} /** * Creates a new configuration exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a ConfigurationException. */ public ConfigurationException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationExplorer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Helper class meant for instances of {@link Configuration} to explore their own configuration tree. * @author Nicolas Rinaudo */ class ConfigurationExplorer { /** Current section. */ private ConfigurationSection section; /** * Creates a new explorer on the specified section. * @param root section from which to start exploring. */ public ConfigurationExplorer(ConfigurationSection root) { section = root; } /** * Returns the current section. * @return the current section. */ public ConfigurationSection getSection() { return section; } /** * Move to the specified section. * @param name name of the current section's subsection in which to move. * @param create if true and name doesn't exist, it will be created. * @return true if we could move to name, false otherwise. */ public boolean moveTo(String name, boolean create) { ConfigurationSection buffer = section.getSection(name); // Buffer for the subsection. // Checks whether the requested subsection exists. if (buffer == null) { // If it doesn't exist, either return false or create it depending on parameters. if (create) { section = section.addSection(name); return true; } return false; } section = buffer; return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationFormatException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Encapsulate a configuration format error. *

* Within the scope of the com.mucommander.commons.conf API, format errors * are syntax errors in a configuration source. *

* This exception is mostly meant to be used by implementations of {@link ConfigurationReader}, * as they're the ones who will analyse the syntax of a configuration stream. *

* When applicable, instances of ConfigurationFormatException might provide information * about the position in the source at which the error occurred. See the documentation of * {@link #getLineNumber() getLineNumber} and {@link #getColumnNumber() getColumnNumber} for more * information on location conventions. *

* Since ConfigurationFormatException subclasses {@link ConfigurationException}, it * inherits his capacity to wrap other exceptions. * * @author Nicolas Rinaudo */ public class ConfigurationFormatException extends ConfigurationException { /** Describes an unknown {@link #getLineNumber() line} or {@link #getColumnNumber() column} value.*/ public static final int UNKNOWN_LOCATION = -1; /** Line at which the error occurred. */ private int line = UNKNOWN_LOCATION; /** Column at which the error occurred. */ private int column = UNKNOWN_LOCATION; /** * Creates a new configuration format exception. * @param message the error message. */ public ConfigurationFormatException(String message) {super(message);} /** * Creates a new configuration format exception. *

* {@link #UNKNOWN_LOCATION} is a legal value for both line and column. * See the documentation of {@link #getLineNumber() getLineNumber} and * {@link #getColumnNumber() getColumnNumber} for more information on location conventions. * * @param message the error message. * @param line line at which the error occurred. * @param column column at which the error occurred. */ public ConfigurationFormatException(String message, int line, int column) { this(message); setLocationInformation(line, column); } /** * Creates a new configuration format exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the ConfigurationFormatException. * * @param cause the exception to be wrapped in a ConfigurationFormatException. */ public ConfigurationFormatException(Throwable cause) {super(cause == null ? null : cause.getMessage(), cause);} /** * Creates a new configuration format exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the ConfigurationFormatException. *

* {@link #UNKNOWN_LOCATION} is a legal value for both line and column. * See the documentation of {@link #getLineNumber() getLineNumber} and * {@link #getColumnNumber() getColumnNumber} for more information on location conventions. * * @param cause the exception to be wrapped in a ConfigurationFormatException. * @param line line at which the error occurred. * @param column column at which the error occurred. */ public ConfigurationFormatException(Throwable cause, int line, int column) { this(cause); setLocationInformation(line, column); } /** * Creates a new configuration format exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a ConfigurationFormatException. */ public ConfigurationFormatException(String message, Throwable cause) { super(message, cause); } /** * Creates a new configuration format exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. *

* {@link #UNKNOWN_LOCATION} is a legal value for both line and column. * See the documentation of {@link #getLineNumber() getLineNumber} and * {@link #getColumnNumber() getColumnNumber} for more information on location conventions. * * @param message the detail message. * @param cause the exception to be wrapped in a ConfigurationFormatException. * @param line line at which the error occurred. * @param column column at which the error occurred. */ public ConfigurationFormatException(String message, Throwable cause, int line, int column) { this(message, cause); setLocationInformation(line, column); } /** * Sets the position in the stream at which the error occurred. * @param line line at which the error occurred. * @param column column at which the error occurred. */ private void setLocationInformation(int line, int column) { this.line = line; this.column = column; } /** * Returns the line at which the error occurred. *

* By convention, the line at which an error occurs is equal to the number of line breaks encountered * before the problem, plus one. This means that a line number of 1 will describe the * first line in the configuration source. * * @return the line at which the error occurred, {@link #UNKNOWN_LOCATION} if the information is not * available or relevant. * @see #getColumnNumber() */ public int getLineNumber() {return line;} /** * Returns the column at which the error occurred. *

* By convention, the column at which an error occurs is equal to the number of character encountered * after the last line break and before the problem, plus one. This means that a column number of * 1 will describe the first character in the current line. * * @return the column at which the error occurred, {@link #UNKNOWN_LOCATION} if the information is not * available or relevant. * @see #getLineNumber() */ public int getColumnNumber() {return column;} } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Listener interface for receiving configuration events. *

* Implementations of this interface can register themselves to a {@link Configuration configuration} instance through * its {@link Configuration#addConfigurationListener(ConfigurationListener) addConfigurationListener} method to be * notified of configuration changes. * * @author Nicolas Rinaudo */ public interface ConfigurationListener { /** * Invoked when the configuration changes. * @param event describes the configuration modification. */ void configurationChanged(ConfigurationEvent event); } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.io.IOException; import java.io.Reader; /** * Interface for reading from a configuration source using callbacks. *

* Application writers that need to implement a specific configuration format need to subclass this. * Reader implementations have the task of parsing an input stream for configuration data and invoking the * relevant callback methods of {@link ConfigurationBuilder}. *

* The com.mucommander.commons.conf package comes with a default implementation, * {@link XmlConfigurationReader}, which handles the standard muCommander configuration file format. *

* In order for an implementation of ConfigurationReader to be usable by * {@link Configuration configuration} instances, it must come with an associated implementation of * {@link ConfigurationReaderFactory}. *

* In addition, most readers will have an associated writer used to write configuration files in a * format that the reader will understand. * * @author Nicolas Rinaudo * @see ConfigurationReaderFactory */ public interface ConfigurationReader { /** * Reads configuration information from the specified input stream and invokes the specified builder's callback methods. *

* When applicable, this method is expected to throw {@link ConfigurationFormatException format} exceptions rather * than {@link ConfigurationException configuration} exceptions. This will allow applications to report errors in a * way that is useful for users. * * @param in where to read the configuration information from. * @param builder where to send configuration messages to. * @throws IOException if an I/O error occurs. * @throws ConfigurationException if another type of error occurs, in which case that error must be returned by ConfigurationException.getCause(). */ void read(Reader in, ConfigurationBuilder builder) throws ConfigurationException, IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationReaderFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Interface used to provide instances of {@link Configuration} with a way of creating {@link ConfigurationReader reader} instances. *

* A ConfigurationReaderFactory's sole purpose is to create instances of {@link ConfigurationReader}. In most cases, a * factory class will be associated with a reader class, and its code will look something like: *

 * public class MyReaderFactory implements ConfigurationReaderFactory {
 *    public ConfigurationReader getReaderInstance() {return new MyReader();}
 * }
 * 
* * @author Nicolas Rinaudo * @see ConfigurationReader */ public interface ConfigurationReaderFactory { /** * Creates an instance of {@link ConfigurationReader}. * @return an instance of {@link ConfigurationReader}. * @throws ReaderConfigurationException if the factory wasn't properly configured. */ T getReaderInstance() throws ReaderConfigurationException; } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationSection.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.util.*; /** * Represents a section in the configuration tree. * @author Nicolas Rinaudo */ class ConfigurationSection { /** Contains all the variables defined in the section. */ private final Map variables = new HashMap<>(); /** Contains all the subsections defined the section. */ private final Map sections = new HashMap<>(); /** * Removes the specified variable from the section. * @param name name of the variable to remove. * @return the value to which this variable was previously set, null if none. */ public String removeVariable(String name) { return variables.remove(name); } /** * Returns the value of the specified variable. * @param name name of the variable whose value should be returned. * @return the value of the specified variable, or null if it wasn't set. */ public String getVariable(String name) { return variables.get(name); } /** * Sets the specified variable to the specified value. *

* If value is either null or an empty string, * the call will be equivalent to {@link #removeVariable(String)}. * * @param name name of the variable to set. * @param value value for the variable. * @return true if the variable's value was changed as a result of this call, false * otherwise. */ public boolean setVariable(String name, String value) { // If the specified value is empty, deletes the variable. if (value == null || value.trim().isEmpty()) { // If the variable wasn't set, we haven't changed its value. if (getVariable(name) == null) { return false; } // Otherwise, deletes it and returns true. removeVariable(name); return true; } // Compares the variable's new and old values. String buffer = variables.put(name, value); return buffer == null || !buffer.equals(value); } /** * Returns a set on the names of the variables that are defined in the section. *

* Note that the order in which variable names are returned needs not be that in which they were added to the * section. Callers should not rely on the order being consistent over time. * * @return an iterator on the names of the variables that are defined in the section. */ public Set variableNames() { return variables.keySet(); } /** * Returns true if the section contains any variable. * @return true if the section contains any variable, false otherwise. */ public boolean hasVariables() { return !variables.isEmpty(); } /** * Casts the specified value into an integer. *

* If value is null, this method will return 0. * * @param value value to cast to an integer. * @return value as an integer. */ public static int getIntegerValue(String value) { return value == null ? 0 : Integer.parseInt(value); } /** * Casts the specified value into a value list. *

* If value is null, this method will return null. * * @param value value to cast to a value list. * @param separator string used to separate data in tokens. * @return value as a value list. */ public static ValueList getListValue(String value, String separator) { return value == null ? null : new ValueList(value, separator); } /** * Casts the specified value into a float. *

* If value is null, this method will return 0. * * @param value value to cast to a float. * @return value as a float. */ public static float getFloatValue(String value) { return value == null ? 0 : Float.parseFloat(value); } /** * Casts the specified value into an boolean. *

* If value is null, this method will return false. * * @param value value to cast to an boolean. * @return value as an boolean. */ public static boolean getBooleanValue(String value) { return Boolean.TRUE.toString().equals(value); } /** * Casts the specified value into an long. *

* If value is null, this method will return 0. * * @param value value to cast to an long. * @return value as an long. */ public static long getLongValue(String value) { return value == null ? 0 : Long.parseLong(value); } /** * Casts the specified value into an double. *

* If value is null, this method will return 0. * * @param value value to cast to an double. * @return value as an double. */ public static double getDoubleValue(String value) { return value == null ? 0 : Double.parseDouble(value); } /** * Casts the specified value into a string. * @param value value to cast as a string. * @return value as a string. */ public static String getValue(int value) { return Integer.toString(value); } /** * Casts the specified value into a string. * @param value value to cast as a string. * @param separator string to use as a separator. * @return value as a string. */ public static String getValue(List value, String separator) { return ValueList.toString(value, separator); } /** * Casts the specified value into a string. * @param value value to cast as a string. * @return value as a string. */ public static String getValue(float value) { return Float.toString(value); } /** * Casts the specified value into a string. * @param value value to cast as a string. * @return value as a string. */ public static String getValue(boolean value) { return Boolean.toString(value); } /** * Casts the specified value into a string. * @param value value to cast as a string. * @return value as a string. */ public static String getValue(long value) { return Long.toString(value); } /** * Casts the specified value into a string. * @param value value to cast as a string. * @return value as a string. */ public static String getValue(double value) { return Double.toString(value); } // - Section access ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Creates a subsection wit the specified name in the section. *

* If a subsection with the specified name already exists, it will be returned. * * @param name name of the new section. * @return the subsection with the specified name. */ public ConfigurationSection addSection(String name) { ConfigurationSection section = getSection(name); // The section already exists, returns it. if (section != null) { return section; } // Creates the new section. section = new ConfigurationSection(); sections.put(name, section); return section; } /** * Deletes the specified section. * @param name name of the section to delete. * @return the section that was deleted if any, null otherwise. */ public ConfigurationSection removeSection(String name) { return sections.remove(name); } /** * Deletes the specified section. *

* Note that this method is very inefficient and should only be called when strictly necessary. * * @param section section to remove. * @return true if the specified section was removed, false if it didn't exist. */ public boolean removeSection(ConfigurationSection section) { Set sectionNames = sectionNames(); // Goes through each key / value pair and checks whether we've found the section // we were looking for. for (String name : sectionNames) { // If we have, remove it and break. if (getSection(name).equals(section)) { removeSection(name); return true; } } return false; } /** * Returns the subsection with the specified name. * @param name name of the section to retrieve. * @return the requested section if found, null otherwise. */ public ConfigurationSection getSection(String name) { return sections.get(name); } /** * Returns an set on all of this section's subsections' names. *

* Note that the order in which section names are returned needs not be that in which they were added to the * section. Callers should not rely on the order being consistent over time. * * @return an enumeration on all of this section's subsections' names. */ public Set sectionNames() { return sections.keySet(); } /** * Returns true if this section has subsections. * @return true if this section has subsections, false otherwise. */ public boolean hasSections() { return !sections.isEmpty(); } /** * Returns true if the section doesn't contain either variables or sub-sections. *

* This method is meant for {@link Configuration} instances to prune dead branches. * * @return true if the section doesn't contain either variables or sub-sections, false * otherwise. */ public boolean isEmpty() { return !hasSections() && !hasVariables(); } /** * Remove all variables and sub-sections of the section */ public void clear() { variables.clear(); sections.clear(); } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationSource.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.io.IOException; import java.io.Reader; import java.io.Writer; /** * Provides instances of {@link Configuration} with streams to configuration data. *

* Application writers that need to retrieve configuration data from a non-standard source (over the network, from a * database, ...) need to subclass this. *

* Implementations of this interface can be registered through {@link Configuration}'s * {@link Configuration#setSource(ConfigurationSource) setSource} method. Their purpose is * to provide the system with streams to a configuration source. This system allows applications * to retrieve their configuration information from non-standard sources, such as over the network, * in a database, ... *

* The com.mucommander.commons.conf package comes with a default implementation, * {@link FileConfigurationSource}, * which will open input and output streams on a local file. * * @author Nicolas Rinaudo * @see FileConfigurationSource */ public interface ConfigurationSource { /** * Returns an input stream on the configuration source. * @return an input stream on the configuration source. * @throws IOException if any I/O error occurs. */ Reader getReader() throws IOException; /** * Returns an output stream on the configuration source. * @return an output stream on the configuration source. * @throws IOException if any I/O error occurs. */ Writer getWriter() throws IOException; /** * Returns whether this source exists * @return true if the source exists, false otherwise. * * @throws IOException if any I/O error occurs. */ boolean isExists() throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationStructureException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Encapsulate a configuration structure error. *

* Within the scope of the com.mucommander.commons.conf API, structure errors * are inconsistencies in the structure of the configuration tree - a section closed more * than once, for example, or never closed at all. *

* This exception is mostly meant to be used by implementations of {@link ConfigurationBuilder}, * as they have to analyse the structure of the configuration they're receiving events for. *

* Since ConfigurationStructureException subclasses {@link ConfigurationException}, it * inherits his capacity to wrap other exceptions. * * @author Nicolas Rinaudo */ public class ConfigurationStructureException extends ConfigurationException { /** * Creates a new configuration structure exception. * @param message the error message. */ public ConfigurationStructureException(String message) {super(message);} /** * Creates a new configuration structure exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the ConfigurationStructureException. * * @param cause the exception to be wrapped in a ConfigurationStructureException. */ public ConfigurationStructureException(Throwable cause) {super(cause);} /** * Creates a new configuration structure exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a ConfigurationStructureException. */ public ConfigurationStructureException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ConfigurationWriterFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.io.Writer; /** * Interface used to provide interfaces of {@link Configuration} with a way of creating writer instances. *

* A ConfigurationWriterFactory's sole purpose is to create instances of writer. In most cases, a * factory class will be associated with a writer class, and its code will look something like: *

 * public class MyWriterFactory implements ConfigurationWriterFactory {
 *    public ConfigurationWriter getWriterInstance() {return new MyWriter();}
 * }
 * 
* * @author Nicolas Rinaudo */ public abstract class ConfigurationWriterFactory { /** The name of the root element of the XML file */ private final String rootElementName; /** * Constructor * For backward compatibility using root element "prefs" it is not mentioned otherwise */ public ConfigurationWriterFactory() { this("prefs"); } /** * Constructor * * @param rootElementName the name of the root element in the XML file */ public ConfigurationWriterFactory(String rootElementName) { this.rootElementName = rootElementName; } /** * Returns the name of the root element of the XML file * @return the name of the root element of the XML file */ protected String getRootElementName() { return rootElementName; } /** * Creates an instance of {@link ConfigurationBuilder}. *

* The returned builder instance will serialize configuration events to the specified writer. * * @param out where to write the configuration data. * @return an instance of {@link ConfigurationBuilder}. * @throws WriterConfigurationException if the factory wasn't properly configured. */ public abstract T getWriterInstance(Writer out) throws WriterConfigurationException; } ================================================ FILE: src/main/java/com/mucommander/commons/conf/DefaultConfigurationBuilder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Default base class for the analysis of the logical structure of a {@link Configuration}. *

* This class is available as a convenience for applications that need to explore the content * of a configuration tree. It provides no-op implementations for the methods defined in * {@link ConfigurationBuilder}, and application writers can override the ones that are of use * to them and ignore the others. * * @author Nicolas Rinaudo */ public class DefaultConfigurationBuilder implements ConfigurationBuilder { /** * Receive notification at the beginning of the configuration. *

* By default, do nothing. Application writers may override this method in a subclass to take * specific actions at the beginning of a document (such as allocating the root node of a tree * or creating an output file). * */ public void startConfiguration() {} /** * Receive notification at the end of the configuration. *

* By default, do nothing. Application writers may override this method in a subclass to take * specific actions at the end of a document (such as finalising a tree or closing an output file). * */ public void endConfiguration() {} /** * Receive notification at the beginning of a section. *

* By default, do nothing. Application writers may override this method in a subclass to take * specific actions at the start of each element (such as allocating a new tree node or writing * output to a file). * * @param name name of the new section. */ public void startSection(String name) {} /** * Receive notification at the end of a section. *

* By default, do nothing. Application writers may override this method in a subclass to take * specific actions at the end of each element (such as finalising a tree node or writing output * to a file). * * @param name name of the finished section. */ public void endSection(String name) {} /** * Receive notification of variable definition. *

* By default, do nothing. Application writers may override this method to take specific actions for * each variable definition (such as adding a leaf to a tree node, or printing it to a file). * * @param name name of the new variable. * @param value value of the new variable. */ public void addVariable(String name, String value) {} } ================================================ FILE: src/main/java/com/mucommander/commons/conf/FileConfigurationSource.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.io.*; import java.nio.charset.Charset; /** * Configuration source that work on a {@link File} instance. * @author Nicolas Rinaudo */ public class FileConfigurationSource implements ConfigurationSource { /** Path to the file on which to open input and output streams. */ private final File file; /** File's charset. */ private final Charset charset; /** * Creates a source that will open streams on the specified file. * @param file file in which the configuration data is located. * @deprecated Application developers should use {@link #FileConfigurationSource(File, Charset)} instead. This * constructor assumes the specified file to be UTF-8 encoded. */ @Deprecated public FileConfigurationSource(File file) { this(file, "utf-8"); } /** * Creates a source on the specified file and charset. * @param file file in which the configuration data is located. * @param charset charset in which the file is encoded. */ public FileConfigurationSource(File file, Charset charset) { this.file = file; this.charset = charset; } /** * Creates a source on the specified file and charset. * @param file file in which the configuration data is located. * @param charset charset in which the file is encoded. */ FileConfigurationSource(File file, String charset) { this(file, Charset.forName(charset)); } /** * Creates a source that will open streams on the specified file. * @param path path to the file in which the configuration data is located. * @deprecated Application developers should use {@link #FileConfigurationSource(String, Charset)} instead. This * constructor assumes the specified file to be UTF-8 encoded. */ @Deprecated public FileConfigurationSource(String path) { this(new File(path)); } /** * Creates a source on the specified file and charset. * @param path path to the file in which the configuration data is located. * @param charset charset in which the file is encoded. */ FileConfigurationSource(String path, String charset) { this(new File(path), charset); } /** * Creates a source on the specified file and charset. * @param path path to the file in which the configuration data is located. * @param charset charset in which the file is encoded. */ public FileConfigurationSource(String path, Charset charset) { this(new File(path), charset); } /** * Returns the file on which input and output streams are opened. * @return the file on which input and output streams are opened. */ public File getFile() { return file; } /** * Returns the charset in which the {@link #getFile() configuration file} is encoded. * @return the charset in which the {@link #getFile() configuration file} is encoded. */ public Charset getCharset() { return charset; } @Override public Reader getReader() throws IOException { InputStream is = new FileInputStream(file); return new BufferedReader(new InputStreamReader(is, charset)); } @Override public Writer getWriter() throws IOException { return new OutputStreamWriter(new FileOutputStream(file), charset); } @Override public boolean isExists() { return file.exists(); } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ReaderConfigurationException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Encapsulate a reader configuration error. *

* This exception is mostly meant to be used by implementations of {@link ConfigurationReaderFactory}, * as they're the ones who will configure instances of {@link ConfigurationReader}. *

* Since ReaderConfigurationException subclasses {@link ConfigurationException}, it * inherits his capacity to wrap other exceptions. * * @author Nicolas Rinaudo */ public class ReaderConfigurationException extends ConfigurationException { /** * Creates a new reader configuration exception. * @param message the error message. */ public ReaderConfigurationException(String message) {super(message);} /** * Creates a new reader configuration exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the ReaderConfigurationException. * * @param cause the exception to be wrapped in a ReaderConfigurationException. */ public ReaderConfigurationException(Throwable cause) {super(cause);} /** * Creates a new reader configuration exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a ReaderConfigurationException. */ public ReaderConfigurationException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/commons/conf/SourceConfigurationException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Encapsulate a source configuration error. *

* This exception is meant to be thrown by {@link Configuration} whenever a method that requires a * {@link ConfigurationSource} to have been set is called. *

* Since SourceConfigurationException subclasses {@link ConfigurationException}, it * inherits his capacity to wrap other exceptions. * * @author Nicolas Rinaudo */ public class SourceConfigurationException extends ConfigurationException { /** * Creates a new source configuration exception. * @param message the error message. */ public SourceConfigurationException(String message) {super(message);} /** * Creates a new source configuration exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the SourceConfigurationException. * * @param cause the exception to be wrapped in a SourceConfigurationException. */ public SourceConfigurationException(Throwable cause) {super(cause);} /** * Creates a new source configuration exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a SourceConfigurationException. */ public SourceConfigurationException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ValueIterator.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.util.Iterator; import java.util.NoSuchElementException; /** * Iterator with support for value casting. *

* Instances of this class can only be retrieved through {@link ValueList#valueIterator()}. * * @author Nicolas Rinaudo */ public class ValueIterator implements Iterator { /** Wrapped iterator. */ private final Iterator iterator; /** * Creates a new ValueIterator wrapping the specified iterator. * @param iterator iterator to wrap. */ ValueIterator(Iterator iterator) { this.iterator = iterator; } /** * Returns true if the iteration has more elements. * (In other words, returns true if next would return an element rather than throwing an exception.) * @return true if the iteration has more elements. */ @Override public boolean hasNext() { return iterator.hasNext(); } /** * Returns the next element in the iteration. * @return the next element in the iteration. * @throws NoSuchElementException if the iteration has no more elements. */ @Override public String next() { return iterator.next(); } /** * Throws an UnsupportedOperationException. */ @Override public void remove() { throw new UnsupportedOperationException(); } /** * Returns the next value in the iterator as a string. * @return the next value in the iterator as a string. * @throws NoSuchElementException if the iteration has no more elements. */ public String nextValue() { return iterator.next(); } /** * Returns the next value in the iterator as a integer. * @return the next value in the iterator as a integer. * @throws NoSuchElementException if the iteration has no more elements. * @throws NumberFormatException if the value cannot be cast to an integer. */ public int nextIntegerValue() { return ConfigurationSection.getIntegerValue(nextValue()); } /** * Returns the next value in the iterator as a float. * @return the next value in the iterator as a float. * @throws NoSuchElementException if the iteration has no more elements. * @throws NumberFormatException if the value cannot be cast to a float. */ public float nextFloatValue() { return ConfigurationSection.getFloatValue(nextValue()); } /** * Returns the next value in the iterator as a long. * @return the next value in the iterator as a long. * @throws NoSuchElementException if the iteration has no more elements. * @throws NumberFormatException if the value cannot be cast to a long. */ public long nextLongValue() { return ConfigurationSection.getLongValue(nextValue()); } /** * Returns the next value in the iterator as a double. * @return the next value in the iterator as a double. * @throws NoSuchElementException if the iteration has no more elements. * @throws NumberFormatException if the value cannot be cast to a double. */ public double nextDoubleValue() { return ConfigurationSection.getDoubleValue(nextValue()); } /** * Returns the next value in the iterator as a boolean. * @return the next value in the iterator as a boolean. * @throws NoSuchElementException if the iteration has no more elements. */ public boolean nextBooleanValue() { return ConfigurationSection.getBooleanValue(nextValue()); } /** * Returns the next value in the iterator as a {@link ValueList}. * @param separator stirng used to tokenise the next value. * @return the next value in the iterator as a {@link ValueList}. * @throws NoSuchElementException if the iteration has no more elements. */ public ValueList nextListValue(String separator) { return ConfigurationSection.getListValue(nextValue(), separator); } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/ValueList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; /** * Provides support for variables whose value is a list of tokens. *

* Such values will simply be split using a StringTokenizer and stored as a java.util.List. *

* In addition to the regular List methods, this class provides the same value casting mechanisms as * {@link Configuration} and {@link ConfigurationEvent}. These have been extended to iterators through the * {@link #valueIterator()} method. * * @author Nicolas Rinaudo */ public class ValueList extends ArrayList { /** * Creates a new ValueList initialised with the specified data. * @param data data contained by the list. * @param separator string used to separate data in tokens. */ public ValueList(String data, String separator) { StringTokenizer tokenizer = new StringTokenizer(data, separator); while(tokenizer.hasMoreTokens()) { add(tokenizer.nextToken()); } } /** * Returns the value found at the specified index of the list as a string. * @param index index of the value to retrieve. * @return the value found at the specified index of the list as a string. */ public String valueAt(int index) { return get(index); } /** * Returns the value found at the specified index of the list as an integer. * @param index index of the value to retrieve. * @return the value found at the specified index of the list as an integer. * @throws NumberFormatException if the value cannot be cast to an integer. */ public int integerValueAt(int index) { return ConfigurationSection.getIntegerValue(valueAt(index)); } /** * Returns the value found at the specified index of the list as a float. * @param index index of the value to retrieve. * @return the value found at the specified index of the list as a float. * @throws NumberFormatException if the value cannot be cast to a float. */ public float floatValueAt(int index) { return ConfigurationSection.getFloatValue(valueAt(index)); } /** * Returns the value found at the specified index of the list as a double. * @param index index of the value to retrieve. * @return the value found at the specified index of the list as a double. * @throws NumberFormatException if the value cannot be cast to a double. */ public double doubleValueAt(int index) { return ConfigurationSection.getDoubleValue(valueAt(index)); } /** * Returns the value found at the specified index of the list as a long. * @param index index of the value to retrieve. * @return the value found at the specified index of the list as a long. * @throws NumberFormatException if the value cannot be cast to a long. */ public long longValueAt(int index) { return ConfigurationSection.getLongValue(valueAt(index)); } /** * Returns the value found at the specified index of the list as an boolean. * @param index index of the value to retrieve. * @return the value found at the specified index of the list as an boolean. */ public boolean booleanValueAt(int index) { return ConfigurationSection.getBooleanValue(valueAt(index)); } /** * Returns the value found at the specified index of the list as a {@link ValueList}. * @param index index of the value to retrieve. * @param separator string used to split the value into tokens. * @return the value found at the specified index of the list as a {@link ValueList}. */ public ValueList listValueAt(int index, String separator) { return ConfigurationSection.getListValue(valueAt(index), separator); } /** * Returns a {@link ValueIterator} on the list. * @return a {@link ValueIterator} on the list. */ public ValueIterator valueIterator() { return new ValueIterator(iterator()); } /** * Returns a string representation of the specified list. * @param data values to represent as a string. * @param separator string used to separate one element from the other. * @return a string representation of the specified list. */ public static String toString(List data, String separator) { StringBuilder buffer = new StringBuilder(); Iterator values = data.iterator(); // Deals with the first value separately. if (values.hasNext()) { buffer.append(values.next().toString()); } // All subsequent values will be concatenated after a separator. while (values.hasNext()) { buffer.append(separator); buffer.append(values.next().toString()); } // Returns the final value. return buffer.toString(); } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/WriterConfigurationException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; /** * Encapsulate a writer configuration error. *

* This exception is mostly meant to be used by implementations of writer, * as they're the ones who will configure instances of {@link ConfigurationBuilder}. *

* Since WriterConfigurationException subclasses {@link ConfigurationException}, it * inherits his capacity to wrap other exceptions. * @author Nicolas Rinaudo */ public class WriterConfigurationException extends ConfigurationException { /** * Creates a new writer configuration exception. * @param message the error message. */ public WriterConfigurationException(String message) { super(message); } /** * Creates a new writer configuration exception wrapping an existing exception. *

* The existing exception will be embedded in the new one, and its message will * become the default message for the WriterConfigurationException. * * @param cause the exception to be wrapped in a WriterConfigurationException. */ public WriterConfigurationException(Throwable cause) { super(cause); } /** * Creates a new writer configuration exception from an existing exception. *

* The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a WriterConfigurationException. */ public WriterConfigurationException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/XmlConfigurationReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.Reader; /** * Implementation of {@link ConfigurationReader} used to read XML configuration streams. *

* The format of XML files parsed by instances of XmlConfigurationReader is fairly simple: *

    *
  • * Any element that doesn't contain other elements is considered to be a variable. Its value will be * the CDATA contained by the element. *
  • *
  • * Any element that contains other elements is considered to be a section. Any CDATA it might contain * will be ignored. *
  • *
  • * The XML file's first element is traditionally called prefs, but this isn't enforced. It * will be excluded from section names. *
  • *
* *

* For example: *

 * <prefs>
 *   <some>
 *     Random CDATA
 *     <section>
 *       <var1>value1</var1>
 *       <var2>value2</var2>
 *     </section>
 *   </some>
 * </prefs>
 * 
* This will be interpreted as follows: *
    *
  • Random CDATA will be ignored.
  • *
  • A variable called some.section.var1 will be created with a value of value1.
  • *
  • A variable called some.section.var2 will be created with a value of value2.
  • *
* * @author Nicolas Rinaudo * @see XmlConfigurationWriter */ public class XmlConfigurationReader extends DefaultHandler implements ConfigurationReader { /** Factory used to create {@link XmlConfigurationReader} instances. */ public static final ConfigurationReaderFactory FACTORY; /** Current depth in the configuration tree. */ private int depth; /** Buffer for each element's CDATA. */ private final StringBuilder buffer; /** Name of the item being parsed. */ private String itemName; /** Class notified whenever a new configuration item is found. */ protected ConfigurationBuilder builder; /** Whether the current element is a variable. */ private boolean isVariable; /** Used to track the parser's position in the XML file. */ private Locator locator; static { FACTORY = XmlConfigurationReader::new; } /** * Creates a new instance of XML configuration reader. */ public XmlConfigurationReader() { buffer = new StringBuilder(); } // - Reader methods ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Reads the content of in a passes build messages to builder. * @param in input stream from which to read the configuration data. * @param builder object to notify of build events. * @throws IOException if an I/O error occurs. * @throws ConfigurationFormatException if a configuration file format occurs. * @throws ConfigurationException if a non-specific error occurs. */ public void read(Reader in, ConfigurationBuilder builder) throws IOException, ConfigurationException { this.builder = builder; locator = null; try { SAXParserFactory.newInstance().newSAXParser().parse(new InputSource(in), this); } catch (ParserConfigurationException e) { throw new ConfigurationException("Failed to create a SAX parser", e); } catch (SAXParseException e) { throw new ConfigurationFormatException(e.getMessage(), e.getLineNumber(), e.getColumnNumber()); } catch (SAXException e) { throw new ConfigurationFormatException(e.getException() == null ? e : e.getException()); } } // - XML handling -------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void characters(char[] ch, int start, int length) {buffer.append(ch, start, length);} /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { depth++; if (depth == 1) { return; } if (itemName != null) { try { builder.startSection(itemName); } catch(Exception e) { throw new SAXParseException(e.getMessage(), locator, e); } } buffer.setLength(0); itemName = qName; isVariable = true; } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { depth--; if (depth == 0) { return; } // If the current element doesn't have subsections, considers it to be a variable. if(isVariable) { String value = buffer.toString().trim(); // Ignores empty values, otherwise notifies the builder of a new variable. if(!value.isEmpty()) { try { builder.addVariable(qName, value); } catch(Exception e) { throw new SAXParseException(e.getMessage(), locator, e); } } } // The current element is a container, closes it. else { try { builder.endSection(qName); } catch(Exception e) { throw new SAXParseException(e.getMessage(), locator, e); } } isVariable = false; itemName = null; } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void startDocument() throws SAXException { try { builder.startConfiguration(); } catch(Exception e) { throw new SAXParseException(e.getMessage(), locator, e); } } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void endDocument() throws SAXException { try { builder.endConfiguration(); } catch(Exception e) { throw new SAXParseException(e.getMessage(), locator, e); } } /** * This method is public as an implementation side effect and should never be called directly. */ @Override public void setDocumentLocator(Locator locator) { this.locator = locator; } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/XmlConfigurationWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.conf; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import javax.xml.transform.OutputKeys; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import java.io.Writer; /** * Implementation of {@link ConfigurationBuilder} used to write XML configuration streams. *

* Information on the XML file format can be found {@link XmlConfigurationReader here}. * * @author Nicolas Rinaudo */ public class XmlConfigurationWriter implements ConfigurationBuilder { /** Factory used to create instances of {@link XmlConfigurationWriter}. */ public static final ConfigurationWriterFactory FACTORY; /** Writer on the destination XML stream. */ protected final ContentHandler out; /** Empty XML attributes (avoids creating a new instance on each startElement call). */ private final Attributes emptyAttributes = new AttributesImpl(); /** Root element name. */ protected final String rootElementName; static { FACTORY = new ConfigurationWriterFactory() { public XmlConfigurationWriter getWriterInstance(Writer out) { return new XmlConfigurationWriter(out, getRootElementName()); } }; } /** * Creates a new instance of XML configuration writer. * @param out where to write the configuration data. * @param rootElementName the name of root element */ protected XmlConfigurationWriter(Writer out, String rootElementName) { this.rootElementName = rootElementName; this.out = createHandler(out); } private static ContentHandler createHandler(Writer out) { // Initializes the transformer factory. SAXTransformerFactory factory = (SAXTransformerFactory)SAXTransformerFactory.newInstance(); //factory.setAttribute("indent-number", 4); // Creates a new transformer. TransformerHandler transformer; try { transformer = factory.newTransformerHandler(); } catch(TransformerConfigurationException e) { throw new IllegalStateException(e); } // Enables indentation. transformer.getTransformer().setOutputProperty(OutputKeys.INDENT, "yes"); // Sets the standalone property. transformer.getTransformer().setOutputProperty(OutputKeys.STANDALONE, "yes"); // Plugs the transformer into the specified stream. transformer.setResult(new StreamResult(out)); return transformer; } protected void startElement(String name) throws ConfigurationException { try { out.startElement("", name, name, emptyAttributes); } catch(SAXException e) { throw new ConfigurationException(e); } } protected void endElement(String name) throws ConfigurationException { try { out.endElement("", name, name); } catch(SAXException e) { throw new ConfigurationException(e); } } /** * Starts a new configuration section. * @param name name of the new section. * @throws ConfigurationException as a wrapper for any IOException that might have occurred. */ public void startSection(String name) throws ConfigurationException { startElement(name); } /** * Ends a configuration section. * @param name name of the closed section. * @throws ConfigurationException as a wrapper for any IOException that might have occurred. */ public void endSection(String name) throws ConfigurationException { endElement(name); } /** * Creates a new variable in the current section. * @param name name of the new variable. * @param value value of the new variable. * @throws ConfigurationException as a wrapper for any IOException that might have occurred. */ public void addVariable(String name, String value) throws ConfigurationException { char[] data; try { startElement(name); data = value.toCharArray(); out.characters(data, 0, data.length); endElement(name); } catch(SAXException e) { throw new ConfigurationException(e); } } /** * Writes the XML header. * @throws ConfigurationException as a wrapper for any exception that might have occurred. */ public void startConfiguration() throws ConfigurationException { try { out.startDocument(); startElement(rootElementName); } catch(SAXException e) { throw new ConfigurationException(e); } } /** * Writes the XML footer. * @throws ConfigurationException as a wrapper for any IOException that might have occurred. */ public void endConfiguration() throws ConfigurationException { try { endElement(rootElementName); out.endDocument(); } catch(SAXException e) { throw new ConfigurationException(e); } } } ================================================ FILE: src/main/java/com/mucommander/commons/conf/package-info.java ================================================ /** * Provides classes to deal with software configuration. *

Configuration variables

*

* Configuration data is stored as a set of variables organised in sections. A typical variable * name is: section.subsection.name where: *

    *
  • section and subsection are both sections.
  • *
  • name is the variable's name.
  • *
*

* Configuration data is stored in instances of {@link com.mucommander.commons.conf.Configuration}, which offers a set * of methods manipulate variables: *

    *
  • * {@link com.mucommander.commons.conf.Configuration#getVariable(String) Basic retrieval}, which returns a * variable's value if known. *
  • *
  • * {@link com.mucommander.commons.conf.Configuration#getVariable(String,String) Advanced retrieval}, which returns * a variable's value and set it to a default value if not known. *
  • *
  • * Existence {@link com.mucommander.commons.conf.Configuration#isVariableSet(String) checking}, which checks * whether a variable exists or not. *
  • *
  • * {@link com.mucommander.commons.conf.Configuration#renameVariable(String,String) Renaming}, which changes a * variable's name as well as the section it belongs to. *
  • *
  • * {@link com.mucommander.commons.conf.Configuration#removeVariable(String) Removal}, which deletes a variable from * the configuration. *
  • *
  • * {@link com.mucommander.commons.conf.Configuration#setVariable(String,String) Setting}, which sets a variable's * value. *
  • *
*

Loading and storing configuration

*

* The com.mucommander.commons.conf package offers various ways of loading and storing configuration.
* The most obvious way is by using the {@link com.mucommander.commons.conf.Configuration#read(java.io.Reader) read} and * {@link com.mucommander.commons.conf.Configuration#write(java.io.Writer)} methods, but this has the disadvantage * of forcing application writers to manage streams themselves.
* The preferred method is to create a dedicated {@link com.mucommander.commons.conf.ConfigurationSource} class and * register it through {@link com.mucommander.commons.conf.Configuration#setSource(ConfigurationSource) setSource}. * This allows an instance of {@link com.mucommander.commons.conf.Configuration} to know how to read from and write to * its configuration file (or socket or any other medium that provides input and output streams). * *

Changing the default configuration format

*

* The default configuration format is described in {@link com.mucommander.commons.conf.XmlConfigurationReader}. * Application writers who wish to change this can do so by: *

    *
  • * Creating custom {@link com.mucommander.commons.conf.ConfigurationBuilder writers} and * {@link com.mucommander.commons.conf.ConfigurationReader readers}. *
  • *
  • * Creating associated {@link com.mucommander.commons.conf.ConfigurationWriterFactory writer factories} and * {@link com.mucommander.commons.conf.ConfigurationReaderFactory reader factories}. *
  • *
  • * Registering them through * {@link com.mucommander.commons.conf.Configuration#setWriterFactory(ConfigurationWriterFactory) setWriterFactory} * and * {@link com.mucommander.commons.conf.Configuration#setReaderFactory(ConfigurationReaderFactory) setReaderFactory}. *
  • *
* *

Listening to the configuration

*

* Classes that need to be notified when the configuration has changed can do so by: *

    *
  • Implementing the {@link com.mucommander.commons.conf.ConfigurationListener} interface.
  • *
  • * Registering themselves through * {@link com.mucommander.commons.conf.Configuration#addConfigurationListener(ConfigurationListener) * addConfigurationLister}. *
  • *
*/ package com.mucommander.commons.conf; ================================================ FILE: src/main/java/com/mucommander/commons/file/AbstractArchiveEntryFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.io.ByteUtils; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * AbstractArchiveEntryFile represents a file entry inside an archive. * An AbstractArchiveEntryFile is always associated with an {@link ArchiveEntry} object which contains * information about the entry (name, size, date, ...) and with an {@link AbstractArchiveFile} which acts as an entry * repository and provides operations such as listing a directory entry's files, adding or removing entries * (if the archive is writable), etc... * *

* AbstractArchiveEntryFile implements {@link com.mucommander.commons.file.AbstractFile} by delegating methods to * the ArchiveEntry and AbstractArchiveFile instances. * AbstractArchiveEntryFile is agnostic to the actual archive format. In other words, there is no need to * extend this class for a particular archive format, ArchiveEntry and AbstractArchiveFile * provide a generic framework that isolates from the archive format's specifics. *

* This class is abstract (as the name implies) and implemented by two subclasses: *

    *
  • {@link ROArchiveEntryFile}: represents an entry inside a read-only archive
  • *
  • {@link RWArchiveEntryFile}: represents an entry inside a {@link AbstractArchiveFile#isWritable() read-write} archive
  • *
* * @see AbstractArchiveFile * @see ArchiveEntry * @author Maxence Bernard */ public abstract class AbstractArchiveEntryFile extends AbstractFile { /** The archive file that contains this entry */ final AbstractArchiveFile archiveFile; /** This entry file's parent, can be the archive file itself if this entry is located at the top level */ protected AbstractFile parent; /** The ArchiveEntry object that contains information about this entry */ final protected ArchiveEntry entry; /** * Creates a new AbstractArchiveEntryFile. * * @param url the FileURL instance that represents this file's location * @param archiveFile the AbstractArchiveFile instance that contains this entry * @param entry the ArchiveEntry object that contains information about this entry */ protected AbstractArchiveEntryFile(FileURL url, AbstractArchiveFile archiveFile, ArchiveEntry entry) { super(url); this.archiveFile = archiveFile; this.entry = entry; } /** * Returns the ArchiveEntry instance that contains information about the archive entry (path, size, date, ...). * * @return the ArchiveEntry instance that contains information about the archive entry (path, size, date, ...) */ public ArchiveEntry getEntry() { return entry; } /** * Returns the {@link AbstractArchiveFile} that contains the entry represented by this file. * * @return the AbstractArchiveFile that contains the entry represented by this file */ AbstractArchiveFile getArchiveFile() { return archiveFile; } /** * Returns the relative path of this entry, with respect to the archive file. The path separator of the returned * path is the one returned by {@link #getSeparator()}. As a relative path, the returned path does not start * with a separator character. * * @return the relative path of this entry, with respect to the archive file. */ private String getRelativeEntryPath() { String path = entry.getPath(); // Replace all occurrences of the entry's separator by the archive file's separator, only if the separator is // not "/" (i.e. the entry path separator). String separator = getSeparator(); if (!separator.equals("/")) { path = path.replace("/", separator); } return path; } ///////////////////////////////// // AbstractFile implementation // ///////////////////////////////// @Override public long getLastModifiedDate() { return entry.getLastModifiedDate(); } @Override public long getSize() { return entry.getSize(); } @Override public boolean isDirectory() { return entry.isDirectory(); } @Override public boolean isArchive() { // Archive entries files may be wrapped by archive files but they are not archive files per se return false; } @Override public AbstractFile[] ls() throws IOException { return archiveFile.ls(this, null, null); } @Override public AbstractFile[] ls(FilenameFilter filter) throws IOException { return archiveFile.ls(this, filter, null); } @Override public AbstractFile[] ls(FileFilter filter) throws IOException { return archiveFile.ls(this, null, filter); } @Override public AbstractFile getParent() { return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; } /** * Returns true if this entry exists within the archive file. * * @return true if this entry exists within the archive file */ @Override public boolean exists() { return entry.exists(); } @Override public FilePermissions getPermissions() { // Return the entry's permissions return entry.getPermissions(); } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { changePermissions(ByteUtils.setBit(getPermissions().getIntValue(), (permission << (access*3)), enabled)); } @Override public String getOwner() { return entry.getOwner(); } @Override public boolean canGetOwner() { return entry.getOwner()!=null; } @Override public String getGroup() { return entry.getGroup(); } @Override public boolean canGetGroup() { return entry.getGroup()!=null; } /** * Always returns false. */ @Override public boolean isSymlink() { return false; } /** * Always returns false. */ @Override public boolean isSystem() { return false; } /** * Delegates to the archive file's {@link AbstractArchiveFile#getFreeSpace()} method. * * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if the underlying archive file does not support * {@link FileOperation#GET_FREE_SPACE} operations. */ @Override public long getFreeSpace() throws IOException { return archiveFile.getFreeSpace(); } /** * Delegates to the archive file's {@link AbstractArchiveFile#getTotalSpace()} method. * * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if the underlying archive file does not support * {@link FileOperation#GET_TOTAL_SPACE} operations. */ @Override public long getTotalSpace() throws IOException { return archiveFile.getTotalSpace(); } /** * Delegates to the archive file's {@link AbstractArchiveFile#getEntryInputStream(ArchiveEntry,ArchiveEntryIterator)}} * method. * * @throws UnsupportedFileOperationException if the underlying archive file does not support * {@link FileOperation#READ_FILE} operations. */ @Override public InputStream getInputStream() throws IOException { return archiveFile.getEntryInputStream(entry, null); } /** * Always throws an {@link UnsupportedFileOperationException}: append is not available for archive entries. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); } /** * Always throws an {@link UnsupportedFileOperationException}: random read access is not available for archive * entries. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE); } /** * Always throws an {@link UnsupportedFileOperationException}: random write access is not available for archive * entries. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } /** * Always throws an {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { // TODO: we could consider adding remote copy support to RWArchiveEntryFile throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Always throws an {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void renameTo(AbstractFile destFile) throws UnsupportedFileOperationException { // TODO: we could consider adding renaming support to RWArchiveEntryFile throw new UnsupportedFileOperationException(FileOperation.RENAME); } /** * Returns the same ArchiveEntry instance as {@link #getEntry()}. */ @Override public Object getUnderlyingFileObject() { return entry; } //////////////////////// // Overridden methods // //////////////////////// /** * This method is overridden to return the separator of the {@link #getArchiveFile() archive file} that contains * this entry. * * @return the separator of the archive file that contains this entry */ @Override public String getSeparator() { return archiveFile.getSeparator(); } /** * This method is overridden to use the archive file's absolute path as the base path of this entry file. */ @Override public String getAbsolutePath() { // Use the archive file's absolute path and append the entry's relative path to it return archiveFile.getAbsolutePath(true)+getRelativeEntryPath(); } /** * This method is overridden to use the archive file's canonical path as the base path of this entry file. */ @Override public String getCanonicalPath() { // Use the archive file's canonical path and append the entry's relative path to it return archiveFile.getCanonicalPath(true)+getRelativeEntryPath(); } /** * This method is overridden to return the archive's root folder. */ @Override public AbstractFile getRoot() { return archiveFile.getRoot(); } /** * This method is overridden to blindly return false, an archive entry cannot be a root folder. * * @return false, always */ @Override public boolean isRoot() { return false; } /** * This method is overridden to return the archive's volume folder. */ @Override public AbstractFile getVolume() { return archiveFile.getVolume(); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/AbstractArchiveFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.ProxyFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.tree.DefaultMutableTreeNode; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Vector; import java.util.WeakHashMap; /** * AbstractArchiveFile is the superclass of all archive files. It allows archive file to be browsed as if * they were regular directories, independently of the underlying protocol used to access the actual file. *

* AbstractArchiveFile extends {@link ProxyFile} to delegate the AbstractFile * implementation to the actual archive file and overrides some methods to provide the added functionality.
* There are two kinds of AbstractArchiveFile, both of which extend this class: *

    *
  • {@link AbstractROArchiveFile}: read-only archives, these are only able to perform read operations such as * listing the archive's contents or retrieving a particular entry's contents. *
  • {@link AbstractRWArchiveFile}: read-write archives, these are also able to modify the archive by adding or * deleting an entry from the archive. These operations usually require random access to the underlying file, * so write operations may not be available on all underlying file types. The {@link #isWritable()} method allows * to determine whether the archive file is able to carry out write operations or not. *
* When implementing a new archive file/format, either AbstractROArchiveFile or AbstractRWArchiveFile * should be subclassed, but not this class. * *

The first time one of the ls() methods is called to list the archive's contents, * {@link #getEntryIterator()} is called to retrieve a list of *all* the entries contained by the archive, not only the * ones at the top level but also the ones nested one of several levels below. Using this list of entries, it creates * a tree to map the structure of the archive and list the content of any particular directory within the archive. * This tree is recreated (getEntryIterator() is called again) only if the archive file has changed, i.e. * if its date has changed since the tree was created. * *

Files returned by the ls() are {@link AbstractArchiveEntryFile} instances which use an {@link ArchiveEntry} * object to retrieve the entry's attributes. In turn, these AbstractArchiveEntryFile instances query the * associated AbstractArchiveFile to list their content. *
From an implementation perspective, one only needs to deal with {@link ArchiveEntry} instances, all the nuts * and bolts are taken care of by this class. * *

Note that an instance of AbstractArchiveFile may or may not actually be an archive: * {@link #isArchive()} returns true only if the file currently exists and is not a directory. The value * returned by {@link #isArchive()} may change over time as the file is modified. When an * AbstractArchiveFile is not currently an archive, it acts just as a 'normal' file and delegates * ls() methods to the underlying {@link AbstractFile} * * @see com.mucommander.commons.file.FileFactory * @see com.mucommander.commons.file.ArchiveFormatProvider * @see com.mucommander.commons.file.ArchiveEntry * @see AbstractArchiveEntryFile * @see com.mucommander.commons.file.archiver.Archiver * @author Maxence Bernard */ public abstract class AbstractArchiveFile extends ProxyFile { private static Logger logger; /** Archive entries tree */ private ArchiveEntryTree entryTreeRoot; /** Date this file had when the entries tree was created. Used to detect if the archive file has changed and entries * need to be reloaded */ private long entryTreeDate; /** The password to use for a password-protected archive */ protected String password; /** Caches {@link AbstractArchiveEntryFile} instances so that there is only one AbstractArchiveEntryFile * corresponding to the same entry at any given time, to avoid attribute inconsistencies. The key is the * corresponding ArchiveEntry. */ private WeakHashMap archiveEntryFiles; /** * Creates an AbstractArchiveFile on top of the given file. * * @param file the file on top of which to create the archive */ protected AbstractArchiveFile(AbstractFile file) { super(file); } /** * Creates the entries tree, used by {@link #ls(AbstractArchiveEntryFile , com.mucommander.commons.file.filter.FilenameFilter, com.mucommander.commons.file.filter.FileFilter)} * to quickly list the contents of an archive's subfolder. * * @throws IOException if an error occurred while retrieving this archive's entries * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ private void createEntriesTree() throws IOException { // TODO: this method is not thread-safe and needs to be synchronized ArchiveEntryTree treeRoot = new ArchiveEntryTree(); archiveEntryFiles = new WeakHashMap<>(); long start = System.currentTimeMillis(); ArchiveEntryIterator entries = getEntryIterator(); try { ArchiveEntry entry; while ((entry = entries.nextEntry()) != null) { treeRoot.addArchiveEntry(entry); } getLogger().info("entries tree created in "+(System.currentTimeMillis()-start)+" ms"); this.entryTreeRoot = treeRoot; declareEntriesTreeUpToDate(); } finally { try { entries.close(); } catch (IOException e) { // Not much we can do about it } } } /** * Checks if the entries tree exists and if this file hasn't been modified since the tree was last created. * If any of those 2 conditions isn't met, the entries tree is (re)created. * * @throws IOException if an error occurred while creating the tree * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ private void checkEntriesTree() throws IOException { if (this.entryTreeRoot == null || getLastModifiedDate() != this.entryTreeDate) { createEntriesTree(); } } /** * Declares the entries tree up-to-date by setting the current tree date to the archive file's. * This method should be called by {@link AbstractRWArchiveFile} implementations when the archive file has been * modified and the entries propagated in the tree, to avoid the tree from being automatically re-created when * {@link #checkEntriesTree()} is called. */ protected void declareEntriesTreeUpToDate() { this.entryTreeDate = getLastModifiedDate(); } /** * Adds the given {@link ArchiveEntry} to the entries tree. This method will create the tree if it doesn't already * exist, or re-create it if the archive file has changed since it was last created. * * @param entry the ArchiveEntry to add to the tree * @throws IOException if an error occurred while creating the entries tree * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ protected void addToEntriesTree(ArchiveEntry entry) throws IOException { checkEntriesTree(); entryTreeRoot.addArchiveEntry(entry); } /** * Removes the given {@link ArchiveEntry} from the entries tree. This method will create the tree if it doesn't * already exist, or re-create it if the archive file has changed since it was last created. * * @param entry the ArchiveEntry to remove from the tree * @throws IOException if an error occurred while creating the entries tree * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ protected void removeFromEntriesTree(ArchiveEntry entry) throws IOException { checkEntriesTree(); DefaultMutableTreeNode entryNode = entryTreeRoot.findEntryNode(entry.getPath()); if (entryNode != null) { DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)entryNode.getParent(); parentNode.remove(entryNode); } } /** * Returns the {@link ArchiveEntryTree} instance corresponding to the root of the archive entry tree. * The returned value can be null if the tree hasn't been intialized yet. * * @return the ArchiveEntryTree instance corresponding to the root of the archive entry tree */ ArchiveEntryTree getArchiveEntryTree() { return entryTreeRoot; } /** * Returns the contents of the specified folder entry. * * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ protected AbstractFile[] ls(AbstractArchiveEntryFile entryFile, FilenameFilter filenameFilter, FileFilter fileFilter) throws IOException { // Make sure the entries tree is created and up-to-date checkEntriesTree(); if (!entryFile.isBrowsable()) { throw new IOException(); } DefaultMutableTreeNode matchNode = entryTreeRoot.findEntryNode(entryFile.getEntry().getPath()); if (matchNode == null) { throw new IOException(); } return ls(matchNode, entryFile, filenameFilter, fileFilter); } /** * Returns the contents (direct children) of the specified tree node. * * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ private AbstractFile[] ls(DefaultMutableTreeNode treeNode, AbstractFile parentFile, FilenameFilter filenameFilter, FileFilter fileFilter) throws IOException { AbstractFile[] files; int nbChildren = treeNode.getChildCount(); // No FilenameFilter, create entry files and store them directly into an array if (filenameFilter == null) { files = new AbstractFile[nbChildren]; for (int c=0; c < nbChildren; c++) { files[c] = getArchiveEntryFile((ArchiveEntry)(((DefaultMutableTreeNode)treeNode.getChildAt(c)).getUserObject()), parentFile); } } // Use provided FilenameFilter and temporarily store created entry files that match the filter in a Vector else { List filesV = new Vector<>(); for(int c=0; cIOException will be thrown. * *

Important note: the given path's separator character must be '/' and the path must be relative to the * archive's root, i.e. not start with a leading '/', otherwise the entry will not be found. * * @param entryPath path to an entry within this archive * @return an AbstractFile that corresponds to the given entry path * @throws IOException if neither the entry nor its parent exist within the archive * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ public AbstractFile getArchiveEntryFile(String entryPath) throws IOException { // Make sure the entries tree is created and up-to-date checkEntriesTree(); // Todo: check if that's really necessary / if there is a way to remove this entryPath = entryPath.replace(File.separatorChar, ArchiveEntry.SEPARATOR_CHAR); // Find the entry node corresponding to the given path DefaultMutableTreeNode entryNode = entryTreeRoot.findEntryNode(entryPath); if(entryNode==null) { int depth = ArchiveEntry.getDepth(entryPath); AbstractFile parentFile; if(depth==1) parentFile = this; else { String parentPath = entryPath; if(parentPath.endsWith("/")) parentPath = parentPath.substring(0, parentPath.length()-1); parentPath = parentPath.substring(0, parentPath.lastIndexOf('/')); parentFile = getArchiveEntryFile(parentPath); if(parentFile==null) // neither the entry nor the parent exist throw new IOException(); } return getArchiveEntryFile(new ArchiveEntry(entryPath, false, 0, 0, false), parentFile); } return getArchiveEntryFile(entryNode); } /** * Creates and returns an {@link AbstractFile} instance corresponding to the given entry node. * This method recurses to resolve the entry's parent file. * * @param entryNode tree node corresponding to the entry for which to return a file * @return an {@link AbstractFile} instance corresponding to the given entry node */ private AbstractFile getArchiveEntryFile(DefaultMutableTreeNode entryNode) throws IOException { DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode)entryNode.getParent(); return getArchiveEntryFile( (ArchiveEntry)entryNode.getUserObject(), parentNode == entryTreeRoot ? this : getArchiveEntryFile(parentNode) ); } /** * Returns an iterator of {@link ArchiveEntry} that iterates through all the entries of this archive. * Implementations of this method should as much as possible return entries in their "natural order", i.e. the order * in which they are stored in the archive. *

* This method is called the first time one of the ls() is called. It will not be called anymore, * unless the file's date has changed since the last time one of the ls() methods was called. * * @return an iterator of {@link ArchiveEntry} that iterates through all the entries of this archive * @throws IOException if an error occurred while reading the archive, either because the archive is corrupt or * because of an I/O error * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ public abstract ArchiveEntryIterator getEntryIterator() throws IOException; /** * Returns an InputStream to read from the given archive entry. The specified {@link ArchiveEntry} * instance must be one of the entries that were returned by the {@link ArchiveEntryIterator} returned by * {@link #getEntryIterator()}. * * @param entry the archive entry to read * @param entryIterator the iterator that is used to iterate through entries by the caller (if any). This parameter * may be null, but when it is known, specifying may improve the performance of this method * by an order of magnitude. * @return an InputStream to read from the given archive entry * @throws IOException if an error occurred while reading the archive, either because the archive is corrupt or * because of an I/O error, or if the given entry wasn't found in the archive * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ public abstract InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException; /** * Returns true if this archive file is writable, i.e. is capable of adding and deleting entries from * the underlying archive file. * *

* This method is implemented by {@link com.mucommander.commons.file.AbstractROArchiveFile} and * {@link com.mucommander.commons.file.AbstractRWArchiveFile} to respectively return false and * true. This method may be overridden by AbstractRWArchiveFile implementations if write * access is only available under certain conditions, for example if it requires random write access to the * proxied archive file (which may not always be available). * Therefore, this method should be used to test if an AbstractArchiveFile is writable, rather than * testing if it is an instance of AbstractRWArchiveFile. * * @return true if this archive is writable, i.e. is capable of adding and deleting entries from * the underlying archive file. */ public abstract boolean isWritable(); @Override public boolean isArchive() { return exists() && !isDirectory(); } /** * This method is overridden to list and return the topmost entries contained by this archive. * The returned files are {@link AbstractArchiveEntryFile} instances. * * @return the topmost entries contained by this archive * @throws IOException if the archive entries could not be listed * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ @Override public AbstractFile[] ls() throws IOException { // Delegate to the ancestor if this file isn't actually an archive if (!isArchive()) return super.ls(); // Make sure the entries tree is created and up-to-date checkEntriesTree(); return ls(entryTreeRoot, this, null, null); } /** * This method is overridden to list and return the topmost entries contained by this archive, filtering out * the ones that do not match the specified {@link FilenameFilter}. The returned files are {@link AbstractArchiveEntryFile} * instances. * * @param filter the FilenameFilter to be used to filter files out from the list, may be null * @return the topmost entries contained by this archive * @throws IOException if the archive entries could not be listed * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ @Override public AbstractFile[] ls(FilenameFilter filter) throws IOException { // Delegate to the ancestor if this file isn't actually an archive if (!isArchive()) return super.ls(filter); // Make sure the entries tree is created and up-to-date checkEntriesTree(); return ls(entryTreeRoot, this, filter, null); } /** * This method is overridden to list and return the topmost entries contained by this archive, filtering out * the ones that do not match the specified {@link FileFilter}. The returned files are {@link AbstractArchiveEntryFile} instances. * * @param filter the FilenameFilter to be used to filter files out from the list, may be null * @return the topmost entries contained by this archive * @throws IOException if the archive entries could not be listed * @throws UnsupportedFileOperationException if {@link FileOperation#READ_FILE} operations are not supported by the * underlying file protocol. */ @Override public AbstractFile[] ls(FileFilter filter) throws IOException { // Delegate to the ancestor if this file isn't actually an archive if (!isArchive()) { return super.ls(filter); } // Make sure the entries tree is created and up-to-date checkEntriesTree(); return ls(entryTreeRoot, this, null, filter); } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } // Note: do not override #isDirectory() to always return true, as AbstractArchiveFile instances may be created when // the file does not exist yet, and then be mkdir(): in that case, the file will be a directory and not an archive. private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(AbstractArchiveFile.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/AbstractFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.awt.Dimension; import java.io.*; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import javax.swing.Icon; import com.mucommander.commons.HasProgress; import com.mucommander.commons.file.compat.CompatURLStreamHandler; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.ProxyFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.ChecksumInputStream; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.io.StreamUtils; import com.mucommander.commons.runtime.OsFamily; /** * AbstractFile is the superclass of all files. * *

AbstractFile classes should never be instantiated directly. Instead, the {@link FileFactory} getFile * methods should be used to get a file instance from a path or {@link FileURL} location. * * @see com.mucommander.commons.file.FileFactory * @see com.mucommander.commons.file.impl.ProxyFile * @author Maxence Bernard */ public abstract class AbstractFile implements FileAttributes, PermissionTypes, PermissionAccesses { /** URL representing this file */ protected final FileURL fileURL; /** Default path separator */ protected final static String DEFAULT_SEPARATOR = "/"; /** Size of the read/write buffer */ // Note: raising buffer size from 8192 to 65536 makes a huge difference in SFTP read transfer rates but beyond // 65536, no more gain (not sure why). protected final static int IO_BUFFER_SIZE = 65536; /** * Used for method getPushBackInputStream() */ private MuPushbackInputStream pushbackInputStream; /** * Creates a new file instance with the given URL. * * @param url the FileURL instance that represents this file's location */ protected AbstractFile(FileURL url) { this.fileURL = url; } /** * Returns the {@link FileURL} instance that represents this file's location. * * @return the FileURL instance that represents this file's location */ public FileURL getURL() { return fileURL; } /** * Creates and returns a java.net.URL referring to the same location as the {@link FileURL} associated * with this AbstractFile. * The java.net.URL is created from the string representation of this file's FileURL. * Thus, any credentials this FileURL contains are preserved, but properties are lost. * *

The returned URL uses this {@link AbstractFile} to access the associated resource, via the * underlying URLConnection which delegates to this class. * *

It is important to note that this method is provided for interoperability purposes, for the sole purpose of * connecting to APIs that require a java.net.URL. * * @return a java.net.URL referring to the same location as this FileURL * @throws java.net.MalformedURLException if the java.net.URL could not parse the location of this FileURL */ public URL getJavaNetURL() throws MalformedURLException { return new URL(null, getURL().toString(true), new CompatURLStreamHandler(this)); } /** * Returns this file's name. * *

The returned name is the filename extracted from this file's FileURL * as returned by {@link FileURL#getFilename()}. If the filename is null (e.g. http://google.com), the * FileURL's host will be returned instead. If the host is null (e.g. smb://), an empty * String will be returned. Thus, the returned name will never be null. * *

This method should be overridden if a special processing (e.g. URL-decoding) needs to be applied to the * returned filename. * * @return this file's name */ public String getName() { String name = fileURL.getFilename(); // If filename is null, use host instead if (name == null) { name = fileURL.getHost(); // If host is null, return an empty string if (name == null) { return ""; } } return name; } /** * Returns this file's extension, null if this file's name doesn't have an extension. * *

* A filename has an extension if and only if:
* - it contains at least one . character
* - the last . is not the last character of the filename
* - the last . is not the first character of the filename * * @return this file's extension, null if this file's name doesn't have an extension */ public String getExtension() { return getExtension(getName()); } /** * Returns the absolute path to this file: *

    *
  • For local filesystems, the local file's path should be returned, and not a full URL with the scheme * and host parts (e.g. /path/to/file, not file://localhost/path/to/file)
  • *
  • For any other filesystems, the full URL including the protocol and host parts should be returned * (e.g. smb://192.168.1.1/root/blah)
  • *
*

* This default implementation returns the string representation of this file's {@link #getURL() url}, without * the login and password parts. File implementations overriding this method should always return a path free of * any login and password, so that it can safely be displayed to the end user or stored, without risking to * compromise sensitive information. * * * @return the absolute path to this file */ public String getAbsolutePath() { return getURL().toString(false); } /** * Returns the canonical path to this file, resolving any symbolic links or '..' and '.' occurrences. * *

This implementation simply returns the value of {@link #getAbsolutePath()}, and thus should be overridden * if canonical path resolution is available. * * @return the canonical path to this file */ public String getCanonicalPath() { return getAbsolutePath(); } /** * Returns an AbstractFile representing the canonical path of this file, or this if the * absolute and canonical path of this file are identical.
* Note that the returned file may or may not exist, for example if this file is a symlink to a file that doesn't * exist. * * @return an AbstractFile representing the canonical path of this file, or this if the absolute and canonical * path of this file are identical. */ public AbstractFile getCanonicalFile() { String canonicalPath = getCanonicalPath(false); if (canonicalPath.equals(getAbsolutePath(false))) { return this; } try { FileURL canonicalURL = FileURL.getFileURL(canonicalPath); canonicalURL.setCredentials(fileURL.getCredentials()); return FileFactory.getFile(canonicalURL); } catch (IOException e) { return this; } } /** * Returns the path separator used by this file. * *

This default implementation returns the default separator "/", this method should be overridden if the path * separator used by the file implementation is different. * * @return the path separator used by this file */ public String getSeparator() { return DEFAULT_SEPARATOR; } /** * Returns true if this file is hidden. * *

This default implementation is solely based on the filename and returns true if this * file's name starts with '.'. This method should be overridden if the underlying filesystem has a notion * of hidden files. * * @return true if this file is hidden */ public boolean isHidden() { return getName().startsWith("."); } /** * Returns true if this file is executable. * * @return true if this file is executable */ public boolean isExecutable() { if (OsFamily.WINDOWS.isCurrent()) { if (isDirectory()) { return false; } String ext = getExtension(); return ext != null && (ext.equalsIgnoreCase("exe") || ext.equalsIgnoreCase("com") || ext.equalsIgnoreCase("bat") || ext.equalsIgnoreCase("cmd")); } return !isDirectory() && getPermissions().getBitValue(USER_ACCESS, EXECUTE_PERMISSION); } /** * Return true if the application can read this file. * TODO: need to be overridden with a correct check for each file type. * * @return true if the application can read this file. * **/ public boolean canRead() { return true; } /** * Returns the root folder of this file, i.e. the top-level parent folder that has no parent folder. The returned * folder necessarily contains this file, directly or indirectly. If this file already is a root folder, the same * file will be returned. *

* This default implementation returns the file whose URL has the same scheme as this one, same credentials (if any), * and a path equal to /. * * @return the root folder that contains this file */ public AbstractFile getRoot() { FileURL rootURL = (FileURL)getURL().clone(); rootURL.setPath("/"); return FileFactory.getFile(rootURL); } /** * Returns true if this file is a root folder. *

* This default implementation returns true if this file's URL path is /. * * @return true if this file is a root folder */ public boolean isRoot() { return getURL().getPath().equals("/"); } /** * Returns the volume on which this file is located, or this if this file is itself a volume. * The returned file may never be null. Furthermore, the returned file may not always * {@link #exists() exist}, for instance if the returned volume corresponds to a removable drive that's currently * unavailable. If the returned file does exist, it must always be a {@link #isDirectory() directory}. * In other words, archive files may not be considered as volumes. *

* The notion of volume may or may not have a meaning depending on the kind of filesystem. On local filesystems, * the notion of volume can be assimilated into that of mount point for UNIX-based OSes, or drive * for the Windows platform. Volumes may also have a meaning for certain network filesystems such as SMB, for which * shares can be considered as volumes. Filesystems that don't have a notion of volume should return the * {@link #getRoot() root folder}. *

* This default implementation returns this file's {@link #getRoot() root folder}. This method should be overridden * if this is not adequate. * * @return the volume on which this file is located. */ public AbstractFile getVolume() { return getRoot(); } /** * Returns an InputStream to read this file's contents, starting at the specified offset (in bytes). * A java.io.IOException is thrown if the file doesn't exist. * *

This implementation starts by checking whether the {@link FileOperation#RANDOM_READ_FILE} operation is * supported or not. * If it is, a {@link #getRandomAccessInputStream() random input stream} to this file is retrieved and used to seek * to the specified offset. If it's not, a regular {@link #getInputStream() input stream} is retrieved, and * {@link java.io.InputStream#skip(long)} is used to position the stream to the specified offset, which on most * InputStream implementations is very slow as it causes the bytes to be read and discarded. * For this reason, file implementations that do not provide random read access may want to override this method * if a more efficient implementation can be provided. * * @param offset the offset in bytes from the beginning of the file, must be not negative * @throws IOException if this file cannot be read or is a folder. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. * @return an InputStream to read this file's contents, skipping the specified number of bytes */ public InputStream getInputStream(long offset) throws IOException { // Use random access input stream when available if (isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { RandomAccessInputStream rais = getRandomAccessInputStream(); rais.seek(offset); return rais; } InputStream in = getInputStream(); // Skip exactly the specified number of bytes StreamUtils.skipFully(in, offset); return in; } /** * Copies the contents of the given InputStream to this file, appending or overwriting the file * if it exists. It is noteworthy that the provided InputStream will not be closed by this method. * *

This method should be overridden by filesystems that do not offer a {@link #getOutputStream()} * implementation, but that can take an InputStream and use it to write the file. * For this reason, it is recommended to use this method to write a file, rather than copying streams manually using * {@link #getOutputStream()} * *

The length parameter is optional. Setting its value help certain protocols which need to know * the length in advance. This is the case for instance for some HTTP-based protocols like Amazon S3, which require * the Content-Length header to be set in the request. Callers should thus set the length if it is * known. * *

Read and write operations are buffered, with a buffer of {@link #IO_BUFFER_SIZE} bytes. For performance * reasons, this buffer is provided by {@link BufferPool}. Thus, there is no need to surround the InputStream * with a {@link java.io.BufferedInputStream}. * *

Copy progress can optionally be monitored by supplying a {@link com.mucommander.commons.io.CounterInputStream}. * * @param in the InputStream to read from * @param append if true, data written to the OutputStream will be appended to the end of this file. If false, any * existing data will be overwritten. * @param length length of the stream before EOF is reached, -1 if unknown. * @throws FileTransferException if something went wrong while reading from the InputStream or writing to this file */ public void copyStream(InputStream in, boolean append, long length) throws FileTransferException { OutputStream out; try { out = append ? getAppendOutputStream() : getOutputStream(); } catch (UnsupportedFileOperationException e) { throw new FileTransferException(FileTransferException.UNSUPPORTED_OPERATION, e); } catch (IOException e) { throw new FileTransferException(FileTransferException.OPENING_DESTINATION, e); } try { StreamUtils.copyStream(in, out, IO_BUFFER_SIZE); } finally { // Close stream even if copyStream() threw an IOException try { out.close(); } catch(IOException e) { throw new FileTransferException(FileTransferException.CLOSING_DESTINATION, e); } } } /** * Copies this file to a specified destination file, overwriting the destination if it exists. If this file is a * directory, any file or directory it contains will also be copied. * *

This method throws an {@link IOException} if the operation failed, for any of the following reasons: *

    *
  • this file and the destination file are the same
  • *
  • this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)
  • *
  • this file (or one if its children) cannot be read
  • *
  • the destination file (or one of its children) can not be written
  • *
  • an I/O error occurred
  • *
* *

If this file supports the {@link FileOperation#COPY_REMOTELY} file operation, an attempt to perform a * {@link #copyRemotelyTo(AbstractFile) remote copy} of the file to the destination is made. If the operation isn't * supported or wasn't successful, the file is copied manually, by transferring its contents to the destination * using {@link #copyRecursively(AbstractFile, AbstractFile)}.
* In that case, no clean up is performed if an error occurs in the midst of a transfer: files that have been copied * (even partially) are left in the destination.
* It is also worth noting that symbolic links are not copied to the destination when encountered: neither the link * nor the linked file is copied * * @param destFile the destination file to copy this file to * @throws IOException in any of the error cases listed above */ public final void copyTo(AbstractFile destFile) throws IOException { // First, try to perform a remote copy of the file if the operation is supported if (isFileOperationSupported(FileOperation.COPY_REMOTELY)) { try { copyRemotelyTo(destFile); return; // Operation was a success, all done. } catch (IOException ignore) {} } // Fall back to copying the file manually checkCopyPrerequisites(destFile, false); // Copy the file and its contents if the file is a directory copyRecursively(this, destFile); } /** * Moves this file to a specified destination file, overwriting the destination if it exists. If this file is a * directory, any file or directory it contains will also be moved. * After normal completion, this file will not exist anymore: {@link #exists()} will return false. * *

This method throws an {@link IOException} if the operation failed, for any of the following reasons: *

    *
  • this file and the destination file are the same
  • *
  • this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)
  • *
  • this file (or one if its children) cannot be read
  • *
  • this file (or one of its children) cannot be written
  • *
  • the destination file (or one of its children) can not be written
  • *
  • an I/O error occurred
  • *
* *

If this file supports the {@link FileOperation#RENAME} file operation, an attempt to * {@link #renameTo(AbstractFile) rename} the file to the destination is made. If the operation isn't supported * or wasn't successful, the file is moved manually, by transferring its contents to the destination using * {@link #copyTo(AbstractFile)} and then deleting the source.
* In that case, deletion of the source occurs only after all files have been successfully transferred. * No clean up is performed if an error occurs in the midst of a transfer: files that have been copied * (even partially) are left in the destination.
* It is also worth noting that symbolic links are not moved to the destination when encountered: neither the link * nor the linked file is moved, and the symlink file is deleted. * * @param destFile the destination file to move this file to * @throws IOException in any of the error cases listed above */ public final void moveTo(AbstractFile destFile) throws IOException { // First, try to rename the file if the operation is supported if (isFileOperationSupported(FileOperation.RENAME)) { try { renameTo(destFile); // Rename was a success, all done. return; } catch (IOException ignore) {} } // Fall back to moving the file manually copyTo(destFile); // Delete the source file and its contents now that it has been copied OK. // Note that the file won't be deleted if copyTo() failed (threw an IOException) try { deleteRecursively(); } catch(IOException e) { throw new FileTransferException(FileTransferException.DELETING_SOURCE); } } /** * Creates this file as an empty, non-directory file. This method will fail (throw an IOException) * if this file already exists. Note that this method may not always yield a zero-byte file (see below). * *

This generic implementation simply creates a zero-byte file. {@link AbstractRWArchiveFile} implementations * may want to override this method so that it creates a valid archive with no entry. To illustrate, an empty Zip * file with proper headers is 22-byte long. * * @throws IOException if the file could not be created, either because it already exists or because of an I/O error * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public void mkfile() throws IOException { if (exists()) { throw new IOException(); } if (isFileOperationSupported(FileOperation.WRITE_FILE)) { getOutputStream().close(); } else { copyStream(new ByteArrayInputStream(new byte[]{}), false, 0); } } /** * Returns the children files that this file contains, filtering out files that do not match the specified FileFilter. * For this operation to be successful, this file must be 'browsable', i.e. {@link #isBrowsable()} must return * true. * * @param filter the FileFilter to be used to filter files out from the list, may be null * @return the children files that this file contains * @throws IOException if this operation is not possible (file is not browsable) or if an error occurred. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public AbstractFile[] ls(FileFilter filter) throws IOException { return filter == null ? ls() : filter.filter(ls()); } /** * Returns the children files that this file contains, filtering out files that do not match the specified FilenameFilter. * For this operation to be successful, this file must be 'browsable', i.e. {@link #isBrowsable()} must return * true. * *

This default implementation filters out files *after* they have been created. This method * should be overridden if a more efficient implementation can be provided by subclasses. * * @param filter the FilenameFilter to be used to filter out files from the list, may be null * @return the children files that this file contains * @throws IOException if this operation is not possible (file is not browsable) or if an error occurred. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public AbstractFile[] ls(FilenameFilter filter) throws IOException { return filter == null ? ls() : filter.filter(ls()); } /** * Changes this file's permissions to the specified permissions int. * The permissions int should be constructed using the permission types and accesses defined in * {@link com.mucommander.commons.file.PermissionTypes} and {@link com.mucommander.commons.file.PermissionAccesses}. * *

Implementation note: the default implementation of this method calls sequentially {@link #changePermission(int, int, boolean)}, * for each permission and access (that's a total 9 calls). This may affect performance on filesystems which need * to perform an I/O request to change each permission individually. In that case, and if the filesystem allows * to change all permissions at once, this method should be overridden. * * @param permissions new permissions for this file * @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because * of an I/O error. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public void changePermissions(int permissions) throws IOException { int bitShift = 0; PermissionBits mask = getChangeablePermissions(); for (int a = OTHER_ACCESS; a <= USER_ACCESS; a++) { for (int p = EXECUTE_PERMISSION; p <= READ_PERMISSION; p = p<<1) { if (mask.getBitValue(a, p)) { changePermission(a, p, (permissions & (1 << bitShift)) != 0); } bitShift++; } } } /** * Returns a string representation of this file's permissions. * *

The first character is 'l' if this file is a symbolic link,'d' if it is a directory, '-' otherwise. Then * the string contains up to 3 character triplets, for each of the 'user', 'group' and 'other' access types, each * containing the following characters: *

    *
  • 'r' if this file has read permission, '-' otherwise *
  • 'w' if this file has write permission, '-' otherwise *
  • 'x' if this file has executable permission, '-' otherwise *
* *

The first character triplet for 'user' access will always be added to the permissions. Then the 'group' and * 'other' triplets will only be added if at least one of the user permission bits is supported, as tested with * this file's permissions mask. * Here are a couple examples to illustrate: *

    *
  • a directory for which the file permissions' mask is 0 will return the string d---, no matter * what permission values the FilePermissions returned by {@link #getPermissions()} contains
  • *
  • a regular file for which the file permissions' mask returns 777 (full permissions support) and which * has read/write/executable permissions for all three 'user', 'group' and 'other' access types will return * -rwxrwxrwx
  • *
* * @return a string representation of this file's permissions */ public String getPermissionsString() { FilePermissions permissions = getPermissions(); if (permissions == null) { return isSymlink() ? "l???" : isDirectory() ? "d???" : "-???"; } int supportedPerms = permissions.getMask().getIntValue(); StringBuilder sb = new StringBuilder(); sb.append(isSymlink() ? 'l' : isDirectory() ? 'd' : '-'); int perms = permissions.getIntValue(); int bitShift = USER_ACCESS *3; // Permissions go by triplets (rwx), there are 3 of them for respectively 'owner', 'group' and 'other' accesses. // The first one ('owner') will always be displayed, regardless of the permission bit mask. 'Group' and 'other' // will be displayed only if the permission mask contains information about them (at least one permission bit). for (int a = USER_ACCESS; a >= OTHER_ACCESS; a--) { if (a == USER_ACCESS || (supportedPerms & (7<= EXECUTE_PERMISSION; p = p >> 1) { if ((perms & (p<true if the specified file operation and corresponding method is supported by this * file implementation. See the {@link FileOperation} enum for a complete list of file operations and their * corresponding AbstractFile methods. *

* Note that even if true is returned, this doesn't ensure that the file operation will succeed: * additional conditions may be required for the operation to succeed and the corresponding method may throw an * IOException if those conditions are not met. * * @param op a file operation * @return true if the specified file operation is supported by this filesystem. * @see FileOperation */ public boolean isFileOperationSupported(FileOperation op) { return isFileOperationSupported(op, getClass()); } /** * Returns true if this file is browsable. A file is considered browsable if it contains children files * that can be retrieved by calling the ls() methods. Archive files will usually return * true, as will directories (directories are always browsable). * * @return true if this file is browsable */ public final boolean isBrowsable() { return isDirectory() || isArchive(); } /** * Returns the name of the file without its extension. * *

A filename has an extension if and only if:
* - it contains at least one . character
* - the last . is not the last character of the filename
* - the last . is not the first character of the filename
* If this file has no extension, its full name is returned. * * @return this file's name, without its extension. * @see #getName() * @see #getExtension() */ public final String getNameWithoutExtension() { String name = getName(); int position = name.lastIndexOf('.'); if (position <= 0 || position == name.length() - 1) { return name; } return name.substring(0, position); } /** * Shorthand for {@link #getAbsolutePath()}. * * @return the value returned by {@link #getAbsolutePath()}. */ public final String getPath() { return getAbsolutePath(); } /** * Returns the absolute path to this file. * A separator character will be appended to the returned path if true is passed. * * @param appendSeparator if true, a separator will be appended to the returned path * @return the absolute path to this file */ public final String getAbsolutePath(boolean appendSeparator) { String path = getAbsolutePath(); return appendSeparator?addTrailingSeparator(path): removeTrailingSeparator(path); } /** * Returns the canonical path to this file, resolving any symbolic links or '..' and '.' occurrences. * A separator character will be appended to the returned path if true is passed. * * @param appendSeparator if true, a separator will be appended to the returned path * @return the canonical path to this file */ public final String getCanonicalPath(boolean appendSeparator) { String path = getCanonicalPath(); return appendSeparator ? addTrailingSeparator(path) : removeTrailingSeparator(path); } /** * Returns a child of this file, whose path is the concatenation of this file's path and the given relative path. * Although this method does not enforce it, the specified path should be relative, i.e. should not start with * a separator.
* An IOException may be thrown if the child file could not be instantiated but the returned file * instance should never be null. * * @param relativePath the child's path, relative to this file's path * @return an AbstractFile representing the requested child file, never null * @throws IOException if the child file could not be instantiated */ public final AbstractFile getChild(String relativePath) throws IOException { FileURL childURL = (FileURL)getURL().clone(); childURL.setPath(addTrailingSeparator(childURL.getPath()) + relativePath); return FileFactory.getFile(childURL, true); } /** * Convenience method that acts as {@link #getChild(String)} except that it does not throw {@link IOException} but * returns null if the child could not be instantiated. * * @param relativePath the child's path, relative to this file's path * @return an AbstractFile representing the requested child file, null if it could not be instantiated */ public final AbstractFile getChildSilently(String relativePath) { try { return getChild(relativePath); } catch(IOException e) { return null; } } /** * Returns a direct child of this file, whose path is the concatenation of this file's path and the given filename. * An IOException will be thrown in any of the following cases: *

    *
  • if the filename contains one or several path separator (the file would not be a direct child)
  • *
  • if the child file could not be instantiated
  • *
* This method never returns null. * *

Although {@link #getChild} can be used to retrieve a direct child file, this method should be favored because * it allows to use this file instance as the parent of the returned child file. * * @param filename the name of the child file to be created * @return an AbstractFile representing the requested direct child file, never null * @throws IOException in any of the cases listed above */ public final AbstractFile getDirectChild(String filename) throws IOException { if (filename.contains(getSeparator())) { throw new IOException(); } AbstractFile childFile = getChild(filename); // Use this file as the child's parent, it avoids creating a new AbstractFile instance when getParent() is called childFile.setParent(this); return childFile; } /** * Convenience method that creates a directory as a direct child of this directory. * This method will fail if this file is not a directory. * * @param name name of the directory to create * @throws IOException if the directory could not be created, either because the file already exists or for any * other reason. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final void mkdir(String name) throws IOException { getChild(name).mkdir(); } /** * Creates this file as a directory and any parent directory that does not already exist. This method will fail * (throw an IOException) if this file already exists. It may also fail because of an I/O error ; * in this case, this method will not remove the parent directories it has created (if any). * * @throws IOException if this file already exists or if an I/O error occurred. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final void mkdirs() throws IOException { AbstractFile parent = getParent(); if (parent != null && !parent.exists()) { parent.mkdirs(); } mkdir(); } /** * Convenience method that creates a file as a direct child of this directory. * This method will fail if this file is not a directory. * * @param name name of the file to create * @throws IOException if the file could not be created, either because the file already exists or for any * other reason. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final void mkfile(String name) throws IOException { getChild(name).mkfile(); } /** * Returns the immediate ancestor of this AbstractFile if it has one, this otherwise: *

    *
  • if this file is a {@link ProxyFile}, returns the return value of {@link ProxyFile#getProxiedFile()} *
  • if this file is not a ProxyFile, returns this *
* * @return the immediate ancestor of this AbstractFile if it has one, this otherwise */ public final AbstractFile getAncestor() { if (this instanceof ProxyFile) { return ((ProxyFile) this).getProxiedFile(); } return this; } /** * Returns the first ancestor of this file that is an instance of the given Class or of a subclass of it, * or this if this instance's class matches those criteria. Returns null if this * file has no such ancestor. *
* Note that this method will always return this if AbstractFile.class is specified. * * @param abstractFileClass a Class corresponding to an AbstractFile subclass * @return the first ancestor of this file that is an instance of the given Class or of a subclass of the given * Class, or this if this instance's class matches those criteria. Returns null if this * file has no such ancestor. */ public final T getAncestor(Class abstractFileClass) { AbstractFile ancestor = this; AbstractFile lastAncestor; do { if (abstractFileClass.isAssignableFrom(ancestor.getClass())) { return (T) ancestor; } lastAncestor = ancestor; ancestor = ancestor.getAncestor(); } while (lastAncestor != ancestor); return null; } /** * Iterates through the ancestors returned by {@link #getAncestor()} until the top-most ancestor is reached and * returns it. If this file has no ancestor, this will be returned. * * @return returns the top-most ancestor of this file, this if this file has no ancestor */ public final AbstractFile getTopAncestor() { AbstractFile topAncestor = this; while (topAncestor.hasAncestor()) { topAncestor = topAncestor.getAncestor(); } return topAncestor; } /** * Returns true if this AbstractFile has an ancestor, i.e. if this file is a * {@link ProxyFile}, false otherwise. * * @return true if this AbstractFile has an ancestor, false otherwise. */ public final boolean hasAncestor() { return this instanceof ProxyFile; } /** * Returns true if this file is or has an ancestor (immediate or not) that is an instance of the given * Class or of a subclass of the Class. Note that the specified must correspond to an * AbstractFile subclass. Specifying any other Class will always yield to this method returning * false. Also note that this method will always return true if * AbstractFile.class is specified. * * @param abstractFileClass a Class corresponding to an AbstractFile subclass * @return true if this file has an ancestor (immediate or not) that is an instance of the given Class * or of a subclass of the given Class. */ public final boolean hasAncestor(Class abstractFileClass) { AbstractFile ancestor = this; AbstractFile lastAncestor; do { if (abstractFileClass.isAssignableFrom(ancestor.getClass())) { return true; } lastAncestor = ancestor; ancestor = ancestor.getAncestor(); } while (lastAncestor != ancestor); return false; } /** * Returns true if this file is a parent folder of the given file, or if the two files are equal. * * @param file the AbstractFile to test * @return true if this file is a parent folder of the given file, or if the two files are equal */ public final boolean isParentOf(AbstractFile file) { return isBrowsable() && file.getCanonicalPath(true).startsWith(getCanonicalPath(true)); } /** * Convenience method that returns the parent {@link AbstractArchiveFile} that contains this file. If this file * is an {@link AbstractArchiveFile} or an ancestor of {@link AbstractArchiveFile}, this is returned. * If this file is neither contained by an archive nor is an archive, null is returned. * *

* Important note: the returned {@link AbstractArchiveFile}, if any, may not necessarily be an * archive, as specified by {@link #isArchive()}. This is the case for files that were resolved as * {@link AbstractArchiveFile} instances based on their path, but that do not yet exist or were created as * directories. On the contrary, an existing archive will necessarily return a non-null value. * * @return the parent {@link AbstractArchiveFile} that contains this file */ public final AbstractArchiveFile getParentArchive() { if (hasAncestor(AbstractArchiveFile.class)) { return getAncestor(AbstractArchiveFile.class); } else if (hasAncestor(AbstractArchiveEntryFile.class)) { AbstractArchiveEntryFile ancestor = getAncestor(AbstractArchiveEntryFile.class); return ancestor != null ? ancestor.getArchiveFile() : null; } return null; } /** * Returns an icon representing this file, using the default {@link com.mucommander.commons.file.icon.FileIconProvider} * registered in {@link FileFactory}. The specified preferred resolution will be used as a hint, but the returned * icon may have different dimension; see {@link com.mucommander.commons.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension)} * for full details. * This method may return null if the JVM is running on a headless environment. * * @param preferredResolution the preferred icon resolution * @return an icon representing this file, null if the JVM is running on a headless environment * @see com.mucommander.commons.file.FileFactory#getDefaultFileIconProvider() * @see com.mucommander.commons.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension) */ public final Icon getIcon(Dimension preferredResolution) { return FileFactory.getDefaultFileIconProvider().getFileIcon(this, preferredResolution); } /** * Returns an icon representing this file, using the default {@link com.mucommander.commons.file.icon.FileIconProvider} * registered in {@link FileFactory}. The default preferred resolution for the icon is 16x16 pixels. * This method may return null if the JVM is running on a headless environment. * * @return an icon representing this file, null if the JVM is running on a headless environment * @see com.mucommander.commons.file.FileFactory#getDefaultFileIconProvider() * @see com.mucommander.commons.file.icon.FileIconProvider#getFileIcon(AbstractFile, java.awt.Dimension) */ public final Icon getIcon() { // Note: the Dimension object is created here instead of returning a final static field, because creating // a Dimension object triggers the AWT and Swing classes loading. Since these classes are not // needed in a headless environment, we want them to be loaded only if strictly necessary. return getIcon(new java.awt.Dimension(16, 16)); } /** * Returns a checksum of this file (also referred to as hash or digest) calculated by reading this * file's contents and feeding the bytes to the given MessageDigest, until EOF is reached. * *

The checksum is returned as an hexadecimal string, such as "6d75636f0a". The length of this string depends on * the kind of algorithm. * *

Note: this method does not reset the MessageDigest after the checksum has been calculated. * * @param algorithm the algorithm to use for calculating the checksum * @return this file's checksum, as an hexadecimal string * @throws IOException if an I/O error occurred while calculating the checksum * @throws NoSuchAlgorithmException if the specified algorithm does not correspond to any MessageDigest registered * with the Java Cryptography Extension. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final String calculateChecksum(String algorithm) throws IOException, NoSuchAlgorithmException { return calculateChecksum(MessageDigest.getInstance(algorithm)); } /** * Returns a checksum of this file (also referred to as hash or digest) calculated by reading this * file's contents and feeding the bytes to the given MessageDigest, until EOF is reached. * *

The checksum is returned as an hexadecimal string, such as "6d75636f0a". The length of this string depends on * the kind of MessageDigest. * *

Note: this method does not reset the MessageDigest after the checksum has been calculated. * * @param messageDigest the MessageDigest to use for calculating the checksum * @return this file's checksum, as an hexadecimal string * @throws IOException if an I/O error occurred while calculating the checksum * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final String calculateChecksum(MessageDigest messageDigest) throws IOException { try (InputStream in = getInputStream()) { return calculateChecksum(in, messageDigest); } } /** * Tests if the given path contains a trailing separator, and if not, adds one to the returned path. * The separator used is the one returned by {@link #getSeparator()}. * * @param path the path for which to add a trailing separator * @return the path with a trailing separator */ public final String addTrailingSeparator(String path) { // Even though getAbsolutePath() is not supposed to return a trailing separator, root folders ('/', 'c:\' ...) // are exceptions that's why we still have to test if path ends with a separator String separator = getSeparator(); if (!path.endsWith(separator)) { return path + separator; } return path; } /** * Tests if the given path contains a trailing separator, and if it does, removes it from the returned path. * The separator used is the one returned by {@link #getSeparator()}. * * @param path the path for which to remove the trailing separator * @return the path free of a trailing separator */ protected final String removeTrailingSeparator(String path) { // Remove trailing slash if path is not '/' or trailing backslash if path does not end with ':\' // (Reminder: C: is C's current folder, while C:\ is C's root) String separator = getSeparator(); if (path.endsWith(separator) && !((separator.equals("/") && path.length() == 1) || (separator.equals("\\") && path.charAt(path.length()-2)==':'))) path = path.substring(0, path.length()-1); return path; } /** * Checks the prerequisites of a copy (or move) operation. * Throws a {@link FileTransferException} if any of the following conditions are true, does nothing otherwise: *

    *
  • this file does not exist
  • *
  • this file and the destination file are the same, unless allowCaseVariations is true * and the destination filename is a case variation of the source
  • *
  • this file is a parent of the destination file
  • *
* * @param destFile the destination file to copy this file to * @param allowCaseVariations prevents throwing an exception if both file names are a case variation of one another * @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to * know the reason. */ protected final void checkCopyPrerequisites(AbstractFile destFile, boolean allowCaseVariations) throws FileTransferException { boolean isAllowedCaseVariation = false; // Throw an exception of a specific kind if the source and destination files refer to the same file boolean filesEqual = this.equalsCanonical(destFile); if (filesEqual) { // If case variations are allowed and the destination filename is a case variation of the source, // do not throw an exception. if (allowCaseVariations) { String sourceFileName = getName(); String destFileName = destFile.getName(); if (sourceFileName.equalsIgnoreCase(destFileName) && !sourceFileName.equals(destFileName)) { isAllowedCaseVariation = true; } } if (!isAllowedCaseVariation) { throw new FileTransferException(FileTransferException.SOURCE_AND_DESTINATION_IDENTICAL); } } // Throw an exception if source is a parent of destination if (!filesEqual && isParentOf(destFile)) { // Note: isParentOf(destFile) returns true if both files are equal throw new FileTransferException(FileTransferException.SOURCE_PARENT_OF_DESTINATION); } // Throw an exception if the source file does not exist if (!exists()) { throw new FileTransferException(FileTransferException.FILE_NOT_FOUND); } } /** * Checks the prerequisites of a {@link #copyRemotelyTo(AbstractFile)} operation. * This method starts by verifying the following requirements and throws an IOException if one of them * isn't met: *
    *
  • both files' schemes are equal
  • *
  • both files' {@link #getTopAncestor() top ancestors} are equal
  • *
  • both files' hosts are equal, or allowDifferentHosts is true
  • *
* If all those requirements are met, {@link #checkCopyPrerequisites(AbstractFile, boolean)} is called with the * destination file and allowCaseVariations flag to perform prerequisites verifications. * * @param destFile the destination file to copy this file to * @param allowCaseVariations prevents throwing an exception if both file names are a case variation of one another * @param allowDifferentHosts prevents throwing an exception if both files have the same host * @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to * know the reason. * @see #checkCopyPrerequisites(AbstractFile, boolean) */ protected final void checkCopyRemotelyPrerequisites(AbstractFile destFile, boolean allowCaseVariations, boolean allowDifferentHosts) throws IOException { if (!fileURL.schemeEquals(fileURL) || !destFile.getTopAncestor().getClass().equals(getTopAncestor().getClass()) || (!allowDifferentHosts && !destFile.getURL().hostEquals(fileURL))) throw new IOException(); checkCopyPrerequisites(destFile, allowCaseVariations); } /** * Checks the prerequisites of a {@link #renameTo(AbstractFile)} operation. * This method starts by verifying the following requirements and throws an IOException if one of them * isn't met: *
    *
  • both files' schemes are equal
  • *
  • both files' {@link #getTopAncestor() top ancestors} are equal
  • *
  • both files' hosts are equal, or allowDifferentHosts is true
  • *
* If all those requirements are met, {@link #checkCopyPrerequisites(AbstractFile, boolean)} is called with the * destination file and allowCaseVariations flag to perform further prerequisites verifications. * * @param destFile the destination file to copy this file to * @param allowCaseVariations prevents throwing an exception if both file names are a case variation of one another * @param allowDifferentHosts prevents throwing an exception if both files have the same host * @throws FileTransferException in any of the cases listed above, use {@link FileTransferException#getReason()} to * know the reason. * @see #checkCopyPrerequisites(AbstractFile, boolean) */ protected final void checkRenamePrerequisites(AbstractFile destFile, boolean allowCaseVariations, boolean allowDifferentHosts) throws IOException { checkCopyRemotelyPrerequisites(destFile, allowCaseVariations, allowDifferentHosts); } /** * Copies the source file to the destination one and recurses on directory contents. * This method assumes that the destination file does not exists, this must be checked prior to calling this method. * Symbolic links are skipped when encountered: neither the link nor the linked file are copied. * * @param sourceFile the file to copy * @param destFile the destination file * @throws FileTransferException if an error occurred while copying the file */ protected final void copyRecursively(AbstractFile sourceFile, AbstractFile destFile) throws FileTransferException { if (sourceFile.isSymlink()) { return; } if (sourceFile.isDirectory()) { try { destFile.mkdir(); } catch(IOException e) { throw new FileTransferException(FileTransferException.WRITING_DESTINATION); } AbstractFile[] children; try { children = sourceFile.ls(); } catch(IOException e) { throw new FileTransferException(FileTransferException.READING_SOURCE); } AbstractFile destChild; for (AbstractFile child : children) { try { destChild = destFile.getDirectChild(child.getName()); } catch (IOException e) { throw new FileTransferException(FileTransferException.OPENING_DESTINATION); } copyRecursively(child, destChild); } } else { // try (InputStream in = sourceFile.getInputStream()) { // destFile.copyStream(in, false, sourceFile.getSize()); // } catch (IOException e) { // throw new FileTransferException(FileTransferException.OPENING_SOURCE); // } InputStream in; try { in = sourceFile.getInputStream(); } catch(IOException e) { throw new FileTransferException(FileTransferException.OPENING_SOURCE); } try { destFile.copyStream(in, false, sourceFile.getSize()); } finally { // Close stream even if copyStream() threw an IOException try { in.close(); } catch (IOException e) { throw new FileTransferException(FileTransferException.CLOSING_SOURCE); } } } } /** * Deletes the given file. If the file is a directory, enclosing files are deleted recursively. * Symbolic links to directories are simply deleted, without deleting the contents of the linked directory. * * @param file the file to delete * @throws IOException if an error occurred while deleting a file or listing a directory's contents * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ private void deleteRecursively(AbstractFile file) throws IOException { if (file.isDirectory() && !file.isSymlink()) { AbstractFile[] children = file.ls(); for (AbstractFile child : children) { deleteRecursively(child); } } file.delete(); } /** * Convenience method that calls {@link #changePermissions(int)} with the given permissions' int value. * * @param permissions new permissions for this file * @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because * of an I/O error. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final void changePermissions(FilePermissions permissions) throws IOException { changePermissions(permissions.getIntValue()); } /** * This method is a shorthand for {@link #importPermissions(AbstractFile, FilePermissions)} called with * {@link FilePermissions#DEFAULT_DIRECTORY_PERMISSIONS} if this file is a directory or * {@link FilePermissions#DEFAULT_FILE_PERMISSIONS} if this file is a regular file. * * @param sourceFile the file from which to import permissions * @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because * of an I/O error. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. */ public final void importPermissions(AbstractFile sourceFile) throws IOException { importPermissions(sourceFile, isDirectory() ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS); } /** * Imports the given source file's permissions, overwriting this file's permissions. Only the bits that are * supported by the source file (as reported by the permissions' mask) are preserved. Other bits are be * set to those of the specified default permissions. * See {@link SimpleFilePermissions#padPermissions(FilePermissions, FilePermissions)} for more information about * permissions padding. * * @param sourceFile the file from which to import permissions * @param defaultPermissions default permissions to use * @throws IOException if the permissions couldn't be changed, either because of insufficient permissions or because * of an I/O error. * @throws UnsupportedFileOperationException if this method relies on a file operation that is not supported * or not implemented by the underlying filesystem. * @see SimpleFilePermissions#padPermissions(FilePermissions, FilePermissions) */ public final void importPermissions(AbstractFile sourceFile, FilePermissions defaultPermissions) throws IOException { changePermissions(SimpleFilePermissions.padPermissions(sourceFile.getPermissions(), defaultPermissions).getIntValue()); } //////////////////// // Static methods // //////////////////// /** * Returns true if the specified file operation and corresponding method is supported by the * given AbstractFile implementation.
* See the {@link FileOperation} enum for a complete list of file operations and their corresponding * AbstractFile methods. * * @param op a file operation * @param c the file implementation to test * @return true if the specified file operation is supported by this filesystem. * @see FileOperation */ public static boolean isFileOperationSupported(FileOperation op, Class c) { Method method = op.getCorrespondingMethod(c); return method != null && !method.isAnnotationPresent(UnsupportedFileOperation.class); } /** * Returns the given filename's extension, null if the filename doesn't have an extension. * *

A filename has an extension if and only if:
* - it contains at least one . character
* - the last . is not the last character of the filename
* - the last . is not the first character of the filename * *

* The returned extension (if any) is free of any extension separator character (.). For instance, * this method will return "ext" for a file named "name.ext", not ".ext". * * @param filename a filename, not a full path * @return the given filename's extension, null if the filename doesn't have an extension */ public static String getExtension(String filename) { int lastDotPos = filename.lastIndexOf('.'); if (lastDotPos <= 0) { return null; } int len = filename.length(); if (lastDotPos == len-1) { return null; } return filename.substring(lastDotPos+1, len); } /** * Returns the given filename without its extension (base name). if the filename doesn't have an extension, returns the filename as received * *

A filename has an extension if and only if:
* - it contains at least one . character
* - the last . is not the last character of the filename
* - the last . is not the first character of the filename * * @return the file's base name - without its extension, if the filename doesn't have an extension returns the filename as received */ public String getBaseName() { String fileName = getName(); int lastDotPos = fileName.lastIndexOf('.'); if (lastDotPos <= 0 || lastDotPos == fileName.length()-1) { return fileName; } return fileName.substring(0, lastDotPos); } /** * Returns the checksum (also referred to as hash or digest) of the given InputStream * calculated by reading the stream and feeding the bytes to the given MessageDigest until EOF is * reached. * *

Important: this method does not close the InputStream, and does not reset the * MessageDigest after the checksum has been calculated. * * @param in the InputStream for which to calculate the checksum * @param messageDigest the MessageDigest to use for calculating the checksum * @return the given InputStream's checksum, as an hexadecimal string * @throws IOException if an I/O error occurred while calculating the checksum */ public static String calculateChecksum(InputStream in, MessageDigest messageDigest) throws IOException { ChecksumInputStream cin = new ChecksumInputStream(in, messageDigest); try { StreamUtils.readUntilEOF(cin); return cin.getChecksumString(); } catch (IOException e) { throw new FileTransferException(FileTransferException.READING_SOURCE); } } //////////////////////// // Overridden methods // //////////////////////// /** * Tests a file for equality by comparing both files' {@link #getURL() URL}. Returns true if the URL * of this file and the specified one are equal according to {@link FileURL#equals(Object, boolean, boolean)} called * with credentials and properties comparison enabled. * *

* Unlike {@link #equalsCanonical(Object)}, this method is not allowed to perform I/O operations and block * the caller thread. * * @param o the object to compare against this instance * @return Returns true if the URL of this file and the specified one are equal * @see FileURL#equals(Object, boolean, boolean) * @see #equalsCanonical(Object) */ public boolean equals(Object o) { return o instanceof AbstractFile && getURL().equals(((AbstractFile) o).getURL(), true, true); } /** * Tests a file for equality by comparing both files' {@link #getCanonicalPath() canonical path}. * Returns true if the canonical path of this file and the specified one are equal. * *

It is noteworthy that this method uses java.lang.String#equals(Object) to compare paths, which * in some rare cases may return false for non-ascii/Unicode paths that have the same written * representation but are not equal according to java.lang.String#equals(Object). Handling such cases * would require a locale-aware String comparison which is not an option here. * *

It is also worth noting that hostnames are not resolved, which means this method does not consider * a hostname and its corresponding IP address as being equal. * *

Unlike {@link #equals(Object)}, this method is allowed to perform I/O operations and block * the caller thread. * * @param o the object to compare against this instance * @return true if the canonical path of this file and the specified one are equal. * @see #equals(Object) */ public boolean equalsCanonical(Object o) { if (o instanceof AbstractFile) { // TODO: resolve hostnames ? return getCanonicalPath(false).equals(((AbstractFile)o).getCanonicalPath(false)); } return false; } /** * Returns the hashCode of this file's {@link #getURL() URL}. * * @return the hashCode of this file's {@link #getURL() URL}. */ public int hashCode() { return getURL().hashCode(); } /** * Returns a String representation of this file. The returned String is this file's path as returned by * {@link #getAbsolutePath()}. */ public String toString() { return getAbsolutePath(); } ////////////////////// // Abstract methods // ////////////////////// /** * Returns this file's last modified date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). * * @return this file's last modified date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) */ public abstract long getLastModifiedDate(); /** * Returns this file's creation date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). * * @throws IOException if an I/O error occurred * @return creation date */ public long getCreationDate() throws IOException { throw new IOException("operation not supported"); } /** * Returns this file's last access date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). * * @throws IOException if an I/O error occurred * @return last access date */ public long getLastAccessDate() throws IOException { throw new IOException("operation not supported"); } /** * Changes this file's last modified date to the specified one. Throws an IOException if the date * couldn't be changed, either because of insufficient permissions or because of an I/O error. * *

This {@link FileOperation#CHANGE_DATE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @param lastModified last modified date, in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) * @throws IOException if the date couldn't be changed, either because of insufficient permissions or because of * an I/O error. * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract void setLastModifiedDate(long lastModified) throws IOException; public abstract void changeReplication(short replication) throws IOException; /** * Returns this file's size in bytes, 0 if this file doesn't exist, -1 if the size is * undetermined. * * @return this file's size in bytes, 0 if this file doesn't exist, -1 if the size is undetermined */ public abstract long getSize(); /** * Returns this file's parent, null if it doesn't have one. * * @return this file's parent, null if it doesn't have one */ public abstract AbstractFile getParent(); /** * Sets this file's parent. null can be specified if this file doesn't have a parent. * * @param parent the new parent of this file */ public abstract void setParent(AbstractFile parent); /** * Returns true if this file exists. * * @return true if this file exists */ public abstract boolean exists(); /** * Returns this file's permissions, as a {@link FilePermissions} object. Note that this file may only support * certain permission bits, use the {@link com.mucommander.commons.file.FilePermissions#getMask() permission mask} to find * out which bits are supported. * *

This method may return permissions for which none of the bits are supported, but may never return * null. * * @return this file's permissions, as a FilePermissions object */ public abstract FilePermissions getPermissions(); /** * Returns a bit mask describing the permission bits that can be changed on this file when calling * {@link #changePermission(int, int, boolean)} and {@link #changePermissions(int)}. * * @return a bit mask describing the permission bits that can be changed on this file */ public abstract PermissionBits getChangeablePermissions(); /** * Changes the specified permission bit. * *

This {@link FileOperation#CHANGE_PERMISSION file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @param access see {@link PermissionTypes} for allowed values * @param permission see {@link PermissionAccesses} for allowed values * @param enabled true to enable the flag, false to disable it * @throws IOException if the permission couldn't be changed, either because of insufficient permissions or because * of an I/O error. * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. * @see #getChangeablePermissions() */ public abstract void changePermission(int access, int permission, boolean enabled) throws IOException; /** * Returns information about the owner of this file. The kind of information that is returned is implementation-dependant. * It may typically be a username (e.g. 'bob') or a user ID (e.g. '501'). * If the owner information is not available to the AbstractFile implementation (cannot be retrieved or * the filesystem doesn't have any notion of owner) or not available for this particular file, null * will be returned. * * @return information about the owner of this file */ public abstract String getOwner(); /** * Returns information about the owner of this file. The kind of information that is returned is implementation-dependant. * It may typically be a username (e.g. 'bob') or a user ID (e.g. '501'). * If the owner information is not available to the AbstractFile implementation (cannot be retrieved or * the filesystem doesn't have any notion of owner) or not available for this particular file, null * will be returned. * * @return information about the owner of this file * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract short getReplication() throws UnsupportedFileOperationException; /** * Returns information about the owner of this file. The kind of information that is returned is implementation-dependant. * It may typically be a username (e.g. 'bob') or a user ID (e.g. '501'). * If the owner information is not available to the AbstractFile implementation (cannot be retrieved or * the filesystem doesn't have any notion of owner) or not available for this particular file, null * will be returned. * * @return information about the owner of this file * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract long getBlocksize() throws UnsupportedFileOperationException; /** * Returns true if this file implementation is able to return some information about file owners, not * necessarily for all files or this file in particular but at least for some of them. In other words, a * true return value doesn't mean that {@link #getOwner()} will necessarily return a non-null value, * but rather that there is a chance that it does. * * @return true if this file implementation is able to return information about file owners */ public abstract boolean canGetOwner(); /** * Returns information about the group this file belongs to. The kind of information that is returned is implementation-dependant. * It may typically be a group name (e.g. 'www-data') or a group ID (e.g. '501'). * If the group information is not available to the AbstractFile implementation (cannot be retrieved or * the filesystem doesn't have any notion of owner) or not available for this particular file, null * will be returned. * * @return information about the owner of this file */ public abstract String getGroup(); /** * Returns true if this file implementation is able to return some information about file groups, not * necessarily for all files or this file in particular but at least for some of them. In other words, a * true return value doesn't mean that {@link #getGroup()} will necessarily return a non-null value, * but rather that there is a chance that it does. * * @return true if this file implementation is able to return information about file groups */ public abstract boolean canGetGroup(); /** * Returns true if this file is a directory, false in any of the following cases: *

    *
  • this file does not exist
  • *
  • this file is a regular file
  • *
  • this file is an {@link #isArchive() archive}
  • *
* * @return true if this file is a directory, false in any of the cases listed above */ public abstract boolean isDirectory(); /** * Returns true if this file is an archive. *

* An archive is a file container that can be {@link #isBrowsable() browsed}. Archive files may not be * {@link #isDirectory() directories}, and vice-versa. * * @return true if this file is an archive. */ public abstract boolean isArchive(); /** * Returns true if this file is a symbolic link. Symbolic links need to be handled with special care, * especially when manipulating files recursively. * * @return true if this file is a symbolic link */ public abstract boolean isSymlink(); /** * Returns true if this file is a system file. * Note that system file attribute depends on the OS, so we can know it only for local files: * - For MAC OS, {@link MacOsSystemFolder} defines the group of system files * - On Windows, files has special attribute that mark them as system files * * @return true if this file is a system file */ public abstract boolean isSystem(); /** * Returns the children files that this file contains. For this operation to be successful, this file must be * 'browsable', i.e. {@link #isBrowsable()} must return true. * This method may return a zero-length array if it has no children but may never return null. * *

This {@link FileOperation#LIST_CHILDREN file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return the children files that this file contains * @throws IOException if this operation is not possible (file is not browsable) or if an error occurred. * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract AbstractFile[] ls() throws IOException; /** * Creates this file as a directory. This method will fail (throw an IOException) if this file * already exists. * *

This {@link FileOperation#CREATE_DIRECTORY file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @throws IOException if the directory could not be created, either because this file already exists or for any * other reason. * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract void mkdir() throws IOException; /** * Returns an InputStream to read the contents of this file. * Throws an IOException in any of the following cases: *

    *
  • this file does not exist
  • *
  • this file is a directory
  • *
  • this file cannot be read
  • *
  • an I/O error occurs
  • *
* This method may never return null. * *

This {@link FileOperation#READ_FILE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return an InputStream to read the contents of this file * @throws IOException in any of the cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract InputStream getInputStream() throws IOException; /** * Returns an OutputStream to write the contents of this file, overwriting the existing contents, if any. * This file will be created as a zero-byte file if it does not yet exist. *

* This method may throw an IOException in any of the following cases, but may never return * null: *

    *
  • this file is a directory
  • *
  • this file cannot be written
  • *
  • an I/O error occurs
  • *
* *

This {@link FileOperation#WRITE_FILE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return an OutputStream to write the contents of this file * @throws IOException in any of the cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract OutputStream getOutputStream() throws IOException; /** * Returns an OutputStream to write the contents of this file, appending the existing contents, if any. * This file will be created as a zero-byte file if it does not yet exist. *

* This method may throw an IOException in any of the following cases, but may never return * null: *

    *
  • this file is a directory
  • *
  • this file cannot be written
  • *
  • an I/O error occurs
  • *
* *

This {@link FileOperation#APPEND_FILE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return an OutputStream to write the contents of this file * @throws IOException in any of the cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract OutputStream getAppendOutputStream() throws IOException; /** * Returns a {@link RandomAccessInputStream} to read the contents of this file with random access. * Throws an IOException in any of the following cases: *

    *
  • this file does not exist
  • *
  • this file is a directory
  • *
  • this file cannot be read
  • *
  • an I/O error occurs
  • *
* This method may never return null. * *

This {@link FileOperation#RANDOM_READ_FILE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return a RandomAccessInputStream to read the contents of this file with random access * @throws IOException in any of the cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract RandomAccessInputStream getRandomAccessInputStream() throws IOException; /** * Returns a {@link RandomAccessOutputStream} to write the contents of this file with random access. * This file will be created as a zero-byte file if it does not yet exist. * Throws an IOException in any of the following cases: *

    *
  • this file is a directory
  • *
  • this file cannot be written
  • *
  • an I/O error occurs
  • *
* This method may never return null. * *

This {@link FileOperation#RANDOM_WRITE_FILE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return a RandomAccessOutputStream to write the contents of this file with random access * @throws IOException in any of the cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract RandomAccessOutputStream getRandomAccessOutputStream() throws IOException; /** * Deletes this file and this file only (does not recurse on folders). * Throws an IOException in any of the following cases: *

    *
  • if this file does not exist
  • *
  • if this file is a non-empty directory
  • *
  • if this file could not be deleted, for example because of insufficient permissions
  • *
  • if an I/O error occurred
  • *
* *

This {@link FileOperation#DELETE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @throws IOException if this file does not exist or could not be deleted * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract void delete() throws IOException; /** * Renames this file to a specified destination file, overwriting the destination if it exists. If this file is a * directory, any file or directory it contains will also be moved. * After normal completion, this file will not exist anymore: {@link #exists()} will return false. * *

This method throws an {@link IOException} if the operation failed, for any of the following reasons: *

    *
  • this file and the destination file are the same
  • *
  • this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)
  • *
  • this file cannot be read
  • *
  • this file cannot be written
  • *
  • the destination file can not be written
  • *
  • an I/O error occurred
  • *
* *

This {@link FileOperation#RENAME file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @param destFile file to rename this file to * @throws IOException in any of the error cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract void renameTo(AbstractFile destFile) throws IOException; /** * Remotely copies this file to a specified destination file, overwriting the destination if it exists. * If this file is a directory, any file or directory it contains will also be copied. * *

This method differs from {@link #copyTo(AbstractFile)} in that it performs a server-to-server copy of the * file(s), without having the file's contents go through to the local process. This operation should only be * implemented if it offers a performance advantage over a regular client-driven copy like * {@link #copyTo(AbstractFile)}, or if {@link FileOperation#WRITE_FILE} is not supported (output streams cannot be * retrieved) and thus a regular copy cannot succeed. * *

This method throws an {@link IOException} if the operation failed, for any of the following reasons: *

    *
  • this file and the destination file are the same
  • *
  • this file is a directory and a parent of the destination file (the operation would otherwise loop indefinitely)
  • *
  • this file (or one if its children) cannot be read
  • *
  • the destination file (or one of its children) can not be written
  • *
  • an I/O error occurred
  • *
* *

The behavior in the case of an error occurring in the midst of the transfer is unspecified: files that have * been copied (even partially) may or may not be left in the destination. * * @param destFile the destination file to copy this file to * @throws IOException in any of the error cases listed above * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract void copyRemotelyTo(AbstractFile destFile) throws IOException; /** * Returns the free space (in bytes) on the disk/volume where this file is, -1 if this information is * not available. * *

This {@link FileOperation#GET_FREE_SPACE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return the free space (in bytes) on the disk/volume where this file is, -1 if this information is * not available. * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract long getFreeSpace() throws IOException; /** * Returns the total space (in bytes) of the disk/volume where this file is. * *

This {@link FileOperation#GET_TOTAL_SPACE file operation} may or may not be supported by the underlying filesystem * -- {@link #isFileOperationSupported(FileOperation)} can be called to find out if it is. If the operation isn't * supported, a {@link UnsupportedFileOperation} will be thrown when this method is called. * * @return the total space (in bytes) of the disk/volume where this file is * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ public abstract long getTotalSpace() throws IOException; /** * Returns the file Object of the underlying API providing access to the filesystem. The returned Object may expose * filesystem-specific functionality that are not available in AbstractFile. Note however that the * returned Object type may change over time, if the underlying API used to provide access to the filesystem * changes, so this method should be used only as a last resort. * *

If the implemented filesystem has no such Object, null is returned. * * @return the file Object of the underlying API providing access to the filesystem, null if there * is none */ public abstract Object getUnderlyingFileObject(); /** * Returns the stream which can be re-used for file reading. * All method calls will return the same class until the stream was closed. * If the stream has been created but has insufficient buffer size it will be recreated. * This method used for the file viewer and editor etc. to prevent multiple re-opening of files (and archives). * * @param bufferSize minimum size of buffer for PushbackInputStream. * @return he stream which can be re-used for file reading * @throws IOException if an I/O error occurred */ public synchronized PushbackInputStream getPushBackInputStream(final int bufferSize) throws IOException { if (pushbackInputStream == null) { pushbackInputStream = new MuPushbackInputStream(getInputStream(), bufferSize); } else if (pushbackInputStream.getBufferSize() < bufferSize) { pushbackInputStream.close(); pushbackInputStream = new MuPushbackInputStream(getInputStream(), bufferSize); } return pushbackInputStream; } /** * Closes PushbackStream if it exists * @throws IOException if an I/O error occurred */ public void closePushbackInputStream() throws IOException { if (pushbackInputStream != null) { pushbackInputStream.close(); } } public boolean isLocalFile() { return FileProtocols.FILE.equals(fileURL.getScheme()) && hasAncestor(LocalFile.class); } /** * */ private class MuPushbackInputStream extends PushbackInputStream implements HasProgress { private final InputStream src; // public MuPushbackInputStream(InputStream in) { // super(in); // src = in; // } MuPushbackInputStream(InputStream in, int size) { super(in, size); src = in; } @Override public void close() throws IOException { synchronized (AbstractFile.this) { super.close(); src.close(); pushbackInputStream = null; } } int getBufferSize() { return buf.length; } @Override public int getProgress() { return hasProgress() ? ((HasProgress)in).getProgress() : -1; } @Override public boolean hasProgress() { return in instanceof HasProgress; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/AbstractFileClassLoader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.*; /** * ClassLoader implementation capable of loading classes from instances of {@link AbstractFile}. *

It's possible to modify this loader's classpath at runtime through the {@link #addFile(AbstractFile)} method. * * @author Nicolas Rinaudo */ public class AbstractFileClassLoader extends ClassLoader { private final static org.slf4j.Logger LOGGER = null;//org.slf4j.LoggerFactory.getLogger(AbstractFileClassLoader.class); /** All abstract files in which to look for classes and resources. */ private final Vector files; /** * Creates a new AbstractFileClassLoader. * @param parent parent of the class loader. */ public AbstractFileClassLoader(ClassLoader parent) { super(parent); files = new Vector<>(); } /** * Creates a new AbstractFileClassLoader that uses the system classloader as a parent. */ public AbstractFileClassLoader() { this(ClassLoader.getSystemClassLoader()); } /** * Adds the specified file to the class loader's classpath. *

* Note that the file will not be added if it's already in the classpath. * * @param file file to add the class loader's classpath. * @throws IllegalArgumentException if file is not browsable. */ public void addFile(AbstractFile file) { // Makes sure the specified file is browsable. if (!file.isBrowsable()) { throw new IllegalArgumentException(); } // Only adds the file if it's not already there. if (!contains(file)) { files.add(file); } } /** * Returns an iterator on all files in this loader's classpath. * @return an iterator on all files in this loader's classpath. */ public Iterator files() { return files.iterator(); } /** * Returns true if this loader's classpath already contains the specified file. * @param file file to look for. * @return true if this loader's classpath already contains the specified file. */ public boolean contains(AbstractFile file) { return files.contains(file); } // - Resource access ------------------------------------------------------- // ------------------------------------------------------------------------- /** * Tries to locate the specified resource and returns an AbstractFile instance on it. * @param name name of the resource to locate. * @return an {@link AbstractFile} instance describing the requested resource if found, null otherwise. */ private AbstractFile findResourceAsFile(String name) { for (AbstractFile file : files) { try { // If the requested resource could be found, returns it. final AbstractFile child = file.getChild(name); if (child.exists()) { return child; } } catch(IOException ignore) { if (LOGGER != null) { LOGGER.info(""); } } // Treats error as a simple 'resource not found' case and keeps looking for // one with the correct name that will load. } // The requested resource wasn't found. return null; } /** * Returns an input stream on the requested resource. * @param name name of the resource to open. * @return an input stream on the requested resource, null if not found. */ @Override public InputStream getResourceAsStream(String name) { InputStream in = getParent().getResourceAsStream(name); // Input stream on the resource. // Tries the parent first, to respect the delegation model. if (in != null) { return in; } // Tries to locate the resource in the extended classpath if it wasn't found in the parent. AbstractFile file = findResourceAsFile(name); // File representing the resource. if (file != null) { try { return file.getInputStream(); } catch(Exception e) { if (LOGGER != null) { LOGGER.info("", e); } } } // Couldn't find the resource. return null; } /** * Tries to find the requested resource. * @param name name of the resource to locate. * @return the URL of the requested resource if found, null otherwise. */ @Override protected URL findResource(String name) { AbstractFile file = findResourceAsFile(name); // Path to the requested resource. // Tries to find the resource. if (file == null) { return null; } // Tries to retrieve an URL on the resource. try { return file.getJavaNetURL(); } catch(Exception e) { return null; } } /** * Tries to find all the resources with the specified name. * @param name of the resources to find. * @return an enumeration containing the URLs of all the resources that match name. */ @Override protected Enumeration findResources(String name) { Vector resources = new Vector<>(); // All resources that match 'name'. // Goes through all files in the classpath to find the resource. for (AbstractFile file : files) { try { if (file.getChild(name).exists()) { resources.add(file.getJavaNetURL()); } } catch(IOException e) { if (LOGGER != null) { LOGGER.info("", e); } } } return resources.elements(); } /** * Returns the absolute path of the requested library. * @param name name of the library to load. * @return the absolute path of the requested library if found, null otherwise. */ @Override protected String findLibrary(String name) { AbstractFile file = findResourceAsFile(name); // Path of the requested library. return file == null ? null : file.getAbsolutePath(); } // - Class loading --------------------------------------------------------- // ------------------------------------------------------------------------- /** * Loads and returns the class defined by the specified name and path. * @param name name of the class to load. * @param file file containing the class' bytecode. * @return the class defined by the specified name and path. * @throws IOException if an error occurs. */ private Class loadClass(String name, AbstractFile file) throws IOException { byte[] buffer = new byte[(int)file.getSize()]; // Buffer for the class' bytecode. int offset = 0; // Current offset in buffer. try (InputStream in = file.getInputStream()) { // Loads the content of file in buffer. while (offset != buffer.length) { offset += in.read(buffer, offset, buffer.length - offset); } // Loads the class. return defineClass(name, buffer, 0, buffer.length); } } /** * Tries to find and load the specified class. * @param name fully qualified name of the class to load. * @return the requested Class if found, null otherwise. * @throws ClassNotFoundException if the requested class was not found. */ @Override protected Class findClass(String name) throws ClassNotFoundException { AbstractFile file = findResourceAsFile(name.replace('.', '/') + ".class"); // File containing the class' bytecode. // Tries to locate the specified class and, if found, load it. if (file != null) { try { return loadClass(name, file); } catch(IOException e) { if (LOGGER != null) { LOGGER.info("", e); } } } throw new ClassNotFoundException(name); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/AbstractROArchiveFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; /** * AbstractROArchiveFile represents a read-only archive file. This class is abstract and implemented * by read-only archive files. * *

* AbstractROArchiveFile implementations only have to provide two methods: *

    *
  • {@link #getEntryIterator()} to list the entries contained by the archive in a flat, non-hierarchical way *
  • {@link AbstractArchiveFile#getEntryInputStream(ArchiveEntry, ArchiveEntryIterator)} to retrieve a particular entry's content. *
* The {@link #isWritable()} method is implemented to always returns false. * * @author Maxence Bernard */ public abstract class AbstractROArchiveFile extends AbstractArchiveFile { /** * Creates an AbstractROArchiveFile on top of the given file. * * @param file the file on top of which to create the archive */ protected AbstractROArchiveFile(AbstractFile file) { super(file); } /** * Returns false: AbstractROArchiveFile implementations are not capable of adding or * deleting entries. * * @return false */ @Override public final boolean isWritable() { return false; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/AbstractRWArchiveFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.io.IOException; import java.io.OutputStream; /** * AbstractRWArchiveFile represents a read-write archive file. This class is abstract and implemented by * all read-write archive files. * In addition to the read-only operations defined by {@link com.mucommander.commons.file.AbstractArchiveFile}, it provides * abstract methods for adding and deleting entries from the archive. * * The {@link #isWritable()} method implemented by this class always returns true. However, * write operations may not always be available depending on the underlying file (e.g. if random file access is * required). In that case, {@link #isWritable ()} should be overridden to return true only when * write operations are available. * * @author Maxence Bernard */ public abstract class AbstractRWArchiveFile extends AbstractArchiveFile { /** * Creates an AbstractRWArchiveFile on top of the given file. * * @param file the file on top of which to create the archive */ protected AbstractRWArchiveFile(AbstractFile file) { super(file); } /** * Returns true: AbstractRWArchiveFile implementations are by definition capable of adding * or deleting entries. This method should be overridden if the implementation is capable of providing write access * only under certain conditions, for example if it requires random access to the proxied archive file which may not * always be available depending on the underlying file. If that is the case, this method should return * true only when all conditions for providing write operations are met. * * @return true, always */ @Override public boolean isWritable() { return true; } /** * Adds the given entry to the archive and returns an OutputStream to write the entry's contents * if the entry is a regular file, null if the entry is a directory. * Throws an IOException if the entry already exists in the archive or if an I/O error occurs. * * @param entry the entry to add to the archive * @return an OutputStream to write the entry's contents if the entry is a regular file, null if the entry is a directory * @throws IOException if the entry already exists in the archive or if an I/O error occurs * @throws UnsupportedFileOperationException if {@link FileOperation#WRITE_FILE} operations are not supported by * the underlying file protocol. */ public abstract OutputStream addEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException; /** * Deletes the specified entry from the archive. Throws an IOException if the entry doesn't exist * in the archive or if an I/O error occurs. * * @param entry the entry to delete from the archive * @throws IOException if the entry doesn't exist in the archive or if an I/O error occurs * @throws UnsupportedFileOperationException if {@link FileOperation#WRITE_FILE} operations are not supported by * the underlying file protocol. */ public abstract void deleteEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException; /** * Updates the specified entry in the archive with the attributes contained in the {@link ArchiveEntry} object. * Throws an IOException if the entry doesn't exist in the archive or if an I/O error occurs. * *

This methods can be used to update the entry's date and permissions for instance. * * @param entry the entry to update in the archive * @throws IOException if the entry doesn't exist in the archive or if an I/O error occurs * @throws UnsupportedFileOperationException if {@link FileOperation#WRITE_FILE} operations are not supported by * the underlying file protocol. */ public abstract void updateEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException; /** * Processes the archive file to leave it in an optimal form. This method should be called after a writable archive * has been modified (entries added or removed). * *

The actual effect of this method on the archive file depends on the kind of archive. It may be implemented * as a no-op if there is no use for it. * To illustrate, in the case of a {@link com.mucommander.commons.file.impl.zip.ZipArchiveFile}, this method removes chunks * of free space that are left when entries are deleted. * * @throws IOException if an I/O error occurs * @throws UnsupportedFileOperationException if {@link FileOperation#WRITE_FILE} operations are not supported by * the underlying file protocol. */ public abstract void optimizeArchive() throws IOException, UnsupportedFileOperationException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/ArchiveEntry.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.file.util.PathUtils; /** * This class represents a generic archive entry. It provides getters and setters for common archive entry attributes * and allows to encapsulate the entry object of a 3rd party library. * *

Important: the path of archive entries must use the '/' character as a path delimiter, and be relative * to the archive's root, i.e. must not start with a leading '/'. * * @author Maxence Bernard */ public class ArchiveEntry extends SimpleFileAttributes { public static final char SEPARATOR_CHAR = '/'; private static final String SEPARATOR_STRING = String.valueOf(SEPARATOR_CHAR); /** Encapsulated entry object */ private Object entryObject; /** Caches the computed hashcode */ private int hashCode; /** * Creates a new ArchiveEntry with all attributes set to their default value. */ public ArchiveEntry() { } /** * Creates a new ArchiveEntry using the values of the supplied attributes. * * @param path the entry's path with {@link #SEPARATOR_CHAR} as path delimiter * @param directory true if the entry is a directory * @param date the entry's date * @param size the entry's size * @param exists true if the entry exists in the archive */ public ArchiveEntry(String path, boolean directory, long date, long size, boolean exists) { setPath(path); setDate(date); setSize(size); setDirectory(directory); setExists(exists); } /** * Returns the depth of this entry based on the number of path delimiters ({@link #SEPARATOR_CHAR}) its path * contains. Top-level entries have a depth of 1. * * @return the depth of this entry */ public int getDepth() { return getDepth(getPath()); } /** * Returns the depth of the specified entry path, based on the number of path delimiters ({@link #SEPARATOR_CHAR}) * it contains. Top-level entries have a depth of 1. * * @param entryPath the path for which to calculate the depth * @return the depth of the given entry path */ static int getDepth(String entryPath) { return PathUtils.getDepth(entryPath, SEPARATOR_STRING); } /** * Extracts this entry's filename from its path and returns it. * * @return this entry's filename */ public String getName() { String path = getPath(); int len = path.length(); // Remove trailing '/' if any if (path.charAt(len-1) == SEPARATOR_CHAR) { path = path.substring(0, --len); } int lastSlash = path.lastIndexOf(SEPARATOR_CHAR); return lastSlash == -1 ? path : path.substring(lastSlash+1, len); } /** * Returns an archive format-dependent object providing extra information about this entry, typically an object from * a 3rd party library ; null if this entry has none. * * @return an object providing extra information about this entry, null if this entry has none */ public Object getEntryObject() { return entryObject; } /** * Sets an archive format-dependent object providing extra information about this entry, typically an object from * a 3rd party library ; null for none. * * @param entryObject an object providing extra information about this entry, null for none */ public void setEntryObject(Object entryObject) { this.entryObject = entryObject; } /** * Returns the file permissions of this entry. This method is overridden to return default permissions * ({@link FilePermissions#DEFAULT_DIRECTORY_PERMISSIONS} for directories, {@link FilePermissions#DEFAULT_FILE_PERMISSIONS} * for regular files), when none have been set. * * @return the file permissions of this entry */ @Override public FilePermissions getPermissions() { FilePermissions permissions = super.getPermissions(); if (permissions == null) { return isDirectory() ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS; } return permissions; } /** * Overridden to invalidates any previously computed hash code. * * @param path new path to set */ @Override public void setPath(String path) { super.setPath(path); // Invalidate any previously hashCode = 0; } /** * Returns true if the given object is an ArchiveEntry whose path is equal to this one, * according to {@link PathUtils#pathEquals(String, String, String)} (trailing slash-insensitive comparison). * * @param o the object to test * @return true if the given object is an ArchiveEntry whose path is equal to this one * @see PathUtils#pathEquals(String, String, String) */ public boolean equals(Object o) { return o instanceof ArchiveEntry && PathUtils.pathEquals(getPath(), ((ArchiveEntry) o).getPath(), SEPARATOR_STRING); } /** * This method is overridden to return a hash code that is consistent with {@link #equals(Object)}, * so that url1.equals(url2) implies url1.hashCode()==url2.hashCode(). */ public int hashCode() { if (hashCode != 0) { // Return any previously computed hashCode. Note that setPath invalidates the hashCode. return hashCode; } String path = getPath(); // #equals(Object) is trailing separator insensitive, so the hashCode must be trailing separator invariant hashCode = path.endsWith(SEPARATOR_STRING) ? path.substring(0, path.length()-1).hashCode() : path.hashCode(); return hashCode; } @Override public String toString() { return " ArchiveEntry(" + entryObject + ")"; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/ArchiveEntryIterator.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.io.IOException; /** * This class allows to iterate the entries of an archive. It mimics the behavior of an Iterator, with * several differences: * *

    *
  • its methods are allowed to throw IOException
  • *
  • there is no hasNext method, because it wouldn't map very well onto certain formats that don't know * if there is a next entry until the current entry has been consumed.
  • *
  • {@link #close()} needs to be called when the Iterator is not needed anymore, allowing implementations to release * any resources that they hold.
  • *
* * @see com.mucommander.commons.file.SingleArchiveEntryIterator * @see com.mucommander.commons.file.WrapperArchiveEntryIterator * @author Maxence Bernard */ public interface ArchiveEntryIterator { /** * Returns the next entry in this iterator, null if this iterator has no more entries. * * @return true if this iterator has a next entry * @throws IOException if an error occurred while reading the archive, either because the archive is corrupt or * because of an I/O error */ ArchiveEntry nextEntry() throws IOException; /** * Closes this iterator and releases all the resources it holds. This method must be called when this iterator * is not needed anymore. * * @throws IOException if an error occurred while closing the resources */ void close() throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/ArchiveEntryTree.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.file.util.PathUtils; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.tree.DefaultMutableTreeNode; /** * Stores archive entries and organizes them in a tree structure that maps entries in the way they are organized * inside the archive. An instance of ArchiveEntryTree also acts as the root node: all entry nodes * are children of it (direct or indirect). * * @author Maxence Bernard */ class ArchiveEntryTree extends DefaultMutableTreeNode { private static Logger logger; /** * Creates a new empty tree. */ ArchiveEntryTree() { } /** * Adds the given entry to the archive tree, creating parent nodes as necessary. * * @param entry the entry to add to the tree */ void addArchiveEntry(ArchiveEntry entry) { String entryPath = entry.getPath(); int entryDepth = entry.getDepth(); int slashPos = 0; DefaultMutableTreeNode node = this; for (int d = 1; d <= entryDepth; d++) { if (d == entryDepth && !entry.isDirectory()) { // create a leaf node for the entry entry.setExists(true); // the entry has to exist node.add(new DefaultMutableTreeNode(entry, true)); break; } String subPath = d==entryDepth?entryPath:entryPath.substring(0, (slashPos=entryPath.indexOf('/', slashPos)+1)); int nbChildren = node.getChildCount(); DefaultMutableTreeNode childNode = null; boolean matchFound = false; for (int c = 0; c < nbChildren; c++) { childNode = (DefaultMutableTreeNode)node.getChildAt(c); // Path comparison is 'trailing slash insensitive' if (PathUtils.pathEquals(((ArchiveEntry)childNode.getUserObject()).getPath(), subPath, "/")) { // Found a match matchFound = true; break; } } if (matchFound) { if (d == entryDepth) { getLogger().trace("Replacing entry for node {}", childNode); childNode.setUserObject(entry); // Replace existing entry } else { node = childNode; } } else { if (d == entryDepth) { // create a leaf node for the entry entry.setExists(true); // the entry has to exist node.add(new DefaultMutableTreeNode(entry, true)); } else { getLogger().trace("Creating node for {}", subPath); childNode = new DefaultMutableTreeNode(new ArchiveEntry(subPath, true, entry.getLastModifiedDate(), 0, true), true); node.add(childNode); node = childNode; } } } } /** * Finds and returns the node that corresponds to the specified entry path, null if no entry matching * the path could be found. * *

Important note: the given path's separator character must be '/' and the path must be relative to the * archive's root, i.e. not start with a leading '/', otherwise the entry will not be found. Trailing separators * are ignored when paths are compared, for example the path 'temp' will match the entry 'temp/'. * * @param entryPath the path to the entry to look up in this tree * @return the node that corresponds to the specified entry path */ DefaultMutableTreeNode findEntryNode(String entryPath) { int entryDepth = ArchiveEntry.getDepth(entryPath); int slashPos = 0; DefaultMutableTreeNode currentNode = this; for (int d = 1; d <= entryDepth; d++) { String subPath = d == entryDepth ? entryPath : entryPath.substring(0, (slashPos = entryPath.indexOf('/', slashPos)+1)); DefaultMutableTreeNode matchNode = getDefaultMutableTreeNode(currentNode, subPath); if (matchNode == null) { return null; // No node matching the provided path, return null } currentNode = matchNode; } return currentNode; } @Nullable private DefaultMutableTreeNode getDefaultMutableTreeNode(DefaultMutableTreeNode currentNode, String subPath) { int nbChildren = currentNode.getChildCount(); for (int c = 0; c < nbChildren; c++) { DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)currentNode.getChildAt(c); // Path comparison is 'trailing slash insensitive' if (PathUtils.pathEquals(((ArchiveEntry)childNode.getUserObject()).getPath(), subPath, "/")) { // Found the node, let's return it return childNode; } } return null; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ArchiveEntryTree.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/ArchiveFormatProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.file.filter.FilenameFilter; import java.io.IOException; /** * This interface allows {@link FileFactory} to instantiate {@link AbstractArchiveFile} implementations and associate * them with the filenames matched by a {@link FilenameFilter}. *

* For {@link AbstractArchiveFile} implementations to be automatically instantiated by {@link FileFactory}, * this interface needs to be implemented and an instance registered with {@link FileFactory}. * * @author Nicolas Rinaudo, Maxence Bernard * @see AbstractArchiveFile * @see FileFactory */ public interface ArchiveFormatProvider { /** * Creates a new instance of AbstractArchiveFile . * * @param file file to map as an AbstractArchiveFile. * @return a new instance of AbstractArchiveFile that matches the specified URL. * @throws IOException if an error occurs. */ AbstractArchiveFile getFile(AbstractFile file) throws IOException; /** * Returns the FilenameFilter that matches filenames to be associated with this archive format. * * @return the FilenameFilter that matches filenames to be associated with this archive format */ FilenameFilter getFilenameFilter(); String[] getFileExtensions(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/AuthException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.io.IOException; /** * AuthException is an IOException that is thrown whenever an operation failed due to the lack of, * invalid or insufficient credentials. A URL associated with the exception gives the location where the error * occurred, and the set of credentials that were used (if any). * * @author Maxence Bernard */ public class AuthException extends IOException { protected FileURL fileURL; protected String msg; /** * Creates a new AuthException instance, without any associated exception. * * @param fileURL the location where the error occurred, with the set of credentials that were used (if any). */ public AuthException(FileURL fileURL) { this(fileURL, null); } /** * Creates a new AuthException instance that was caused by the given exception. * * @param fileURL the location where the error occurred, with the set of credentials that were used (if any) * @param msg a message describing the error, null if there is none */ public AuthException(FileURL fileURL, String msg) { super(msg); this.fileURL = fileURL; if (msg != null) { this.msg = msg.trim(); } } /** * Returns the location where the error occurred, with the set of credentials that were used (if any). * * @return the location where the error occurred, with the set of credentials that were used (if any) */ public FileURL getURL() { return fileURL; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/AuthenticationType.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; /** * Defines the different types of authentication a file protocol may use. * * @see FileURL#getAuthenticationType() * @author Maxence Bernard */ public enum AuthenticationType { /** Indicates that the file protocol does not use any kind of authentication. */ NO_AUTHENTICATION, /** Indicates that the file protocol can use authentication but does not require it. */ AUTHENTICATION_OPTIONAL, /** Indicates that the file protocol requires authentication. */ AUTHENTICATION_REQUIRED } ================================================ FILE: src/main/java/com/mucommander/commons/file/Authenticator.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; /** * This interface is used by{@link FileFactory} to authenticate {@link FileURL} instances prior to resolving * corresponding {@link AbstractFile} instances. This interface provides the necessary hooks for interacting with an * application or system keystore. *

* A typical implementation of {@link #authenticate(FileURL)} will look for {@link Credentials} matching the * specified URL and, if one set (or more) is found, call {@link FileURL#setCredentials(Credentials)} to set them. * Likewise, this method may also look for and set {@link FileURL#setProperty(String, String) URL properties}, * that will be used by the corresponding {@link AbstractFile} during or after resolution. *

* {@link #authenticate(FileURL)} should normally be called only for {@link FileURL} schemes that * {@link FileURL#getAuthenticationType() support authentication}. Implementations should however not rely on that and * handle non-authenticated URLs as a no-op. *

* A default authenticator can be registered at {@link FileFactory#setDefaultAuthenticator(Authenticator)}. * * @see FileURL#getAuthenticationType() * @see FileFactory#setDefaultAuthenticator(Authenticator) * @see FileFactory#getFile(FileURL, AbstractFile, Authenticator, Object...) * @author Maxence Bernard */ public interface Authenticator { /** * Authenticates the specified {@link FileURL} instance. * * @param fileURL the file URL to authenticate */ void authenticate(FileURL fileURL); } ================================================ FILE: src/main/java/com/mucommander/commons/file/Credentials.java ================================================ package com.mucommander.commons.file; /** * This class is a container for a login and password pair, used to authenticate a location on a filesystem. * * @see com.mucommander.commons.file.FileURL * @author Maxence Bernard */ public final class Credentials { private final String login; private final String password; /** * Creates a new instance with the supplied login and password. * Any provided null values will be replaced by empty strings. * * @param login the login part as a string * @param password the password part as a string */ public Credentials(String login, String password) { this.login = login == null ? "" : login; this.password = password == null ? "" : password; } /** * Returns the login part. The returned login may be an empty string but never null. * * @return the login part. */ public String getLogin() { return login; } /** * Returns the password part. The returned password may be an empty string but never null. * * @return the password part. */ public String getPassword() { return password; } /** * Returns the password as a masked string, each of the characters replaced by '*' characters. * * @return the password as a masked string. */ public String getMaskedPassword() { int passwordLength = password.length(); return "*".repeat(passwordLength); } /** * Returns true if these credentials are empty. *

* Credentials are said to be empty if both login and password are empty strings. * * @return true if these credentials are empty, false otherwise. */ public boolean isEmpty() { return "".equals(login) && "".equals(password); } /** * This method is equivalent to calling {@link #equals(Object, boolean)} with false: * two Credentials instances with the same login but a different password are considered equal. * * @param o the Object to test for equality * @return true if this and the specified instance are equal * @see #equals(Object, boolean) */ @Override public boolean equals(Object o) { return equals(o, false); } /** * Returns true if these Credentials and the specified instance are equal. For credentials to be equal, * their login (as returned by {@link #getLogin()} must be equal. If the password-sensitive parameter is enabled, * their passwords (as returned by {@link #getPassword()} must also match. * *

* Empty Credentials and null are considered equal: if a null instance is specified, * true is returned if these Credentials are {@link #isEmpty() empty}). * * @param o the Object to test for equality * @param passwordSensitive true if passwords need to be equal for credentials instances to match * @return true if this and the specified instance are equal */ public boolean equals(Object o, boolean passwordSensitive) { // Empty Credentials and null are equivalent if (o == null) { return isEmpty(); } if (!(o instanceof Credentials)) { // Note: this class is declared final so we don't need to worry about subclasses return false; } Credentials credentials = (Credentials)o; return credentials.login.equals(this.login) && (!passwordSensitive || credentials.password.equals(this.password)); } /** * Returns a cloned instance of these Credentials. * * @return a cloned instance of these Credentials */ @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { // Should never happen return null; } } public String toString() { return login; } public int hashCode() { // Do not take into account the password, as #equals(Object) is password-insensitive return login.hashCode(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/DefaultPathCanonizer.java ================================================ package com.mucommander.commons.file; import java.util.Vector; /** * @author Maxence Bernard */ public class DefaultPathCanonizer implements PathCanonizer { /** Path separator */ private final String separator; /** The string replacement for '~' path fragments, null for no tilde replacement */ private final String tildeReplacement; /** * Creates a new path canonizer using the specified path separator and no tilde replacement. * * @param separator the path separator that delimits path fragments */ public DefaultPathCanonizer(String separator) { this(separator, null); } /** * Creates a new path canonizer using the specified path separator and tilde replacement string * (if not null). * * @param separator the path separator that delimits path fragments * @param tildeReplacement if not null, path fragments equal to '~' will be replaced by this string. */ public DefaultPathCanonizer(String separator, String tildeReplacement) { this.separator = separator; this.tildeReplacement = tildeReplacement; } /** * Returns a canonical value of the given path, where '.' and '..' path fragments are factored out, * and '~' fragments replaced by the string specified in the constructor (if not null). * * @param path the path to canonize * @return the canonized path */ @Override public String canonize(String path) { // Todo: use PathTokenizer? if (!path.equals("/")) { int pos; // position of current path separator int pos2 = 0; // position of next path separator int separatorLen = separator.length(); String dir; // Current directory String dirWS; // Current directory without trailing separator Vector pathV = new Vector<>(); // Will contain directory hierachy while ((pos = pos2) != -1) { // Get the index of the next path separator occurrence pos2 = path.indexOf(separator, pos); if (pos2 == -1) { // Last dir (or empty string) dir = path.substring(pos); dirWS = dir; } else { pos2 += separatorLen; dir = path.substring(pos, pos2); // Dir name includes trailing separator dirWS = dir.substring(0, dir.length()-separatorLen); } // Discard '.' and empty directories if ((dirWS.isEmpty() && !pathV.isEmpty()) || dirWS.equals(".")) { continue; } // Remove last directory else if(dirWS.equals("..")) { if (!pathV.isEmpty()) { pathV.removeElementAt(pathV.size()-1); } continue; } // Replace '~' by the provided replacement string, only if one was specified else if(tildeReplacement!=null && dirWS.equals("~")) { path = path.substring(0, pos) + tildeReplacement + path.substring(pos+1); // Will perform another pass at the same position pos2 = pos; continue; } // Add directory to the end of the list pathV.add(dir); } // Reconstruct path from directory list StringBuilder result = new StringBuilder(); int nbDirs = pathV.size(); for (int i = 0; i < nbDirs; i++) { result.append(pathV.elementAt(i)); } return result.toString(); // We now have a path free of "." and ".." (and "~" if tilde replacement is enabled) } return path; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/DefaultSchemeHandler.java ================================================ package com.mucommander.commons.file; /** * This class provides a default {@link SchemeHandler} implementation. The no-arg constructor creates an * instance with default values that suit most schemes. This is how the default URL handler returned by * {@link FileURL#getDefaultHandler()} is created.
* The multi-arg constructor allows to create a scheme handler with specific values. *

* The {@link #getRealm(FileURL)} implementation returns a URL with the same scheme and host (if any) as the specified * URL, and a path set to "/". This behavior can be modified by overriding getRealm. * * @see com.mucommander.commons.file.FileURL#getDefaultHandler() * @see com.mucommander.commons.file.SchemeHandler * @author Maxence Bernard */ public class DefaultSchemeHandler implements SchemeHandler { protected SchemeParser parser; protected int standardPort; protected String pathSeparator; protected AuthenticationType authenticationType; protected Credentials guestCredentials; /** * Creates a DefaultSchemeHandler with default values that suit schemes in which the scheme name is not included * in the URL (local and unc locations): *

    *
  • the parser is a DefaultSchemeParser instance created with the no-arg constructor
  • *
  • the scheme's standard port is -1
  • *
  • the scheme's path separator is operating system's path separator
  • *
  • authentication type is {@link AuthenticationType#NO_AUTHENTICATION}
  • *
  • guest credentials are null
  • *
*/ public DefaultSchemeHandler() { this(new DefaultSchemeParser(), -1, System.getProperty("file.separator"), AuthenticationType.NO_AUTHENTICATION, null); } /** * Creates a DefaultSchemeHandler with the specified values. * * @param parser the parser that takes care of parsing URL strings and turning them into FileURL * @param standardPort the scheme's standard port, -1 for none * @param pathSeparator the scheme's path separator, cannot be null * @param authenticationType the type of authentication used by the scheme's file protocol * @param guestCredentials the scheme's guest credentials, null for none */ public DefaultSchemeHandler(SchemeParser parser, int standardPort, String pathSeparator, AuthenticationType authenticationType, Credentials guestCredentials) { this.parser = parser; this.standardPort = standardPort; this.pathSeparator = pathSeparator; this.authenticationType = authenticationType; this.guestCredentials = guestCredentials; } ////////////////////////////////// // SchemeHandler implementation // ////////////////////////////////// /** * Returns the parser that was passed to the constructor. * * @return the parser that was passed to the constructor */ public SchemeParser getParser() { return parser; } /** * Returns the authentication type that was passed to the constructor. * * @return the authentication type that was passed to the constructor */ public AuthenticationType getAuthenticationType() { return authenticationType; } /** * Returns the set of guest credentials that was passed to the constructor. * * @return the set of guest credentials that was passed to the constructor */ public Credentials getGuestCredentials() { return guestCredentials; } /** * Returns the path separator that was passed to the constructor. * * @return the path separator that was passed to the constructor */ public String getPathSeparator() { return pathSeparator; } /** * Returns the standard port that was passed to the constructor. * * @return the standard port that was passed to the constructor */ public int getStandardPort() { return standardPort; } /** * Returns a URL with the same scheme, host and port (if any) as the specified URL, and a path set to * "/" or "\" depending on the URL format. * The login, password, query and fragment parts of the returned URL are always null. * For example, when called with {@code http://www.mucommander.com:8080/path/to/file?query¶m=value}, * this method returns http://www.mucommander.com:8080/. * * @param location the location for which to return the authentication realm * @return the authentication realm of the specified location */ public FileURL getRealm(FileURL location) { // Start by cloning the given URL and then modify the parts that need it FileURL realm = (FileURL)location.clone(); realm.setPath(location.getPathSeparator()); realm.setCredentials(null); realm.setQuery(null); // Todo // realm.setFragment(null) return realm; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/DefaultSchemeParser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; import lombok.extern.slf4j.Slf4j; import java.net.MalformedURLException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; /** * This class provides a default {@link SchemeParser} implementation. Certain scheme-specific features of the parser * can be turned on or off in the constructor, allowing this parser to be used with most schemes. * *

This parser can not only parse URLs but also local absolute paths and UNC paths. Upon parsing, these paths are * turned into equivalent, fully qualified URLs. * *

Local paths

*

* Local absolute paths are turned into corresponding 'file' URLs. Local paths are system-dependent, their form and * path separator vary from one OS to the other. Only native paths are supported, i.e. Windows-style paths are supported * only when running on Windows (or OS/2), Unix-style paths only on an OS that uses them natively... * Here are a couple example of how local paths are parsed and turned into FileURL instances: *

    *
  • Under Windows or OS/2, C:\Windows\System32\ will be parsed and turned into a FileURL whose path * separator is "\" and representation file://localhost/C:\Windows\System32\
  • *
  • Under a Unix-style OS (Linux, Mac OS X, Solaris...), C:\Windows\System32\ will be parsed and turned * into a FileURL whose path separator is "\" and representation file://localhost/C:\Windows\System32\
  • *
* *

UNC paths

*

* Windows-style UNC paths such as \\Server\Volume\File are supported on all OSes but the FileURL * resulting from the parsing varies will not be the same whether they are created on a Windows environment or * on another: *

    *
  • On Windows (any version), \\Server\Volume\File will be turned into a FileURL whose string * representation is file://Server/\Volume\File
  • *
  • On any other kind of OS, \\Server\Volume\File will be turned into a FileURL whose string * representation is smb://Server/Volume/File
  • *
* * @see PathCanonizer * @author Maxence Bernard */ @Slf4j public class DefaultSchemeParser implements SchemeParser { /** True if query should be parsed and not considered as part of the path */ private final boolean parseQuery; /** PathCanonizer instance to be used for canonizing the path part */ private final PathCanonizer pathCanonizer; /** * Creates a DefaultSchemeParser with a {@link DefaultPathCanonizer} that uses the operating system's default * path separator as the path separator and no tilde replacement, and query parsing disabled. */ DefaultSchemeParser() { this(false); } /** * Creates a DefaultSchemeParser with a {@link DefaultPathCanonizer} that uses the operating system's * default path separator as the path separator and no tilde replacement. * If parseQuery is true, any query part (delimited by '?') will be parsed as such, * or considered as part of the path otherwise. * * @param parseQuery true, any query part (delimited by '?') will be parsed as such, or considered * as part of the path otherwise */ DefaultSchemeParser(boolean parseQuery) { this(new DefaultPathCanonizer(FileSystems.getDefault().getSeparator(), null), parseQuery); } /** * Creates a DefaultSchemeParser using the specified {@link PathCanonizer} for canonizing the path part. * If parseQuery is true, any query part (delimited by '?') will be parsed as such, * or considered as part of the path otherwise. * * @param pathCanonizer PathCanonizer instance to be used for canonizing the path part * @param parseQuery true, any query part (delimited by '?') will be parsed as such, or considered * as part of the path otherwise */ DefaultSchemeParser(PathCanonizer pathCanonizer, boolean parseQuery) { this.parseQuery = parseQuery; this.pathCanonizer = pathCanonizer; } /** * Handles the parsing of the given local file URL. * * @param url the URL to parse * @param fileURL the FileURL instance in which to set the different parsed parts */ private void handleLocalFilePath(String url, FileURL fileURL) { SchemeHandler handler = FileURL.getRegisteredHandler(FileProtocols.FILE); SchemeParser parser = handler.getParser(); fileURL.setHandler(handler); fileURL.setScheme(FileProtocols.FILE); fileURL.setHost(FileURL.LOCALHOST); fileURL.setPath((parser instanceof DefaultSchemeParser?((DefaultSchemeParser)parser).getPathCanonizer():pathCanonizer).canonize(url)); } /** * Returns the {@link PathCanonizer} instance that is used by this {@link DefaultSchemeParser}. * * @return the {@link PathCanonizer} instance that is used by this {@link DefaultSchemeParser} */ private PathCanonizer getPathCanonizer() { return pathCanonizer; } @Override public void parse(String url, FileURL fileURL) throws MalformedURLException { // The general form of a URI is: // foo://example.com:8042/over/there?name=ferret#nose // \_/ \______________/\_________/ \_________/ \__/ // | | | | | // scheme authority path query fragment // | _____________________|__ // / \ / \ // urn:example:animal:ferret:nose // See http://labs.apache.org/webarch/uri/rfc/rfc3986.html for full specs try { int pos; int schemeDelimPos = url.indexOf("://"); int urlLen = url.length(); // If the given url contains no scheme, consider that it is a local path and transform it into a file:// URL if (schemeDelimPos == -1) { // Treat the URL as local file path if it starts with: // - '/' and OS doesn't use root drives (Unix-style path) // - a drive letter and OS uses root drives (Windows-style) [support both C:\ and C:/ style] // - a ~ character (refers to the user home folder) if ((!LocalFile.USES_ROOT_DRIVES && url.startsWith("/")) || url.startsWith("~/") || url.equals("~")) { handleLocalFilePath(url, fileURL); // All done, return return; } else if (LocalFile.USES_ROOT_DRIVES && (url.indexOf(":\\") == 1 || url.indexOf(":/") == 1)) { // Turn forward slash-separated paths into their backslash-separated counterparts. if (url.charAt(2) == '/') { url = url.replace('/', '\\'); } handleLocalFilePath(url, fileURL); // All done, return return; } // Handle Windows-style UNC network paths ( \\hostname\path ): // - under Windows, transform it into a URL in the file://hostname/path form, // LocalProtocolProvider will translate it back into a UNC network path // - under other OS, conveniently transform it into smb://hostname/path to be nice with folks // who've spent too much time using Windows else if (url.startsWith("\\\\") && urlLen > 2) { if (OsFamily.WINDOWS.isCurrent()) { pos = url.indexOf('\\', 2); url = FileProtocols.FILE+"://"+ (pos==-1?url.substring(2):url.substring(2, pos)+"/"+(pos==urlLen-1?"":url.substring(pos+1))); // Update scheme delimiter position schemeDelimPos = FileProtocols.FILE.length(); } else { url = FileProtocols.SMB+"://"+url.substring(2).replace('\\', '/'); // Update scheme delimiter position schemeDelimPos = FileProtocols.SMB.length(); } // Update URL's length urlLen = url.length(); } else { // This doesn't look like a valid path, throw an MalformedURLException throw new MalformedURLException("Path not absolute or malformed: "+url); } } // Start URL parsing String scheme = url.substring(0, schemeDelimPos); fileURL.setScheme(scheme); // Advance string index pos = schemeDelimPos+3; int separatorPos = url.indexOf('/', pos); // The question mark character (if any) marks the beginning of the query part, only if it should be parsed. int questionMarkPos = parseQuery?url.indexOf('?', pos):-1; int hostEndPos; // Contains the position of the beginning of the path/query part if (separatorPos != -1) { // Separator is necessarily before question mark hostEndPos = separatorPos; } else if (questionMarkPos != -1) { hostEndPos = questionMarkPos; } else { hostEndPos = urlLen; } // The authority part is the one between scheme:// and the path/query. It includes the user information // (login/password), host and port. String authority = url.substring(pos, hostEndPos); pos = 0; // Parse login and password (if specified). // They may contain non-URL safe characters that are decoded here, and re-encoded by FileURL#toString. int atPos = authority.lastIndexOf('@'); int colonPos; // Filenames may contain @ chars, so atPos must be lower than next separator's position (if any) if (atPos != -1 && (separatorPos == -1 || atPos < separatorPos)) { colonPos = authority.indexOf(':'); String login = URLDecoder.decode(authority.substring(0, colonPos == -1 ? atPos : colonPos), StandardCharsets.UTF_8); String password; if (colonPos != -1) { password = URLDecoder.decode(authority.substring(colonPos+1, atPos), StandardCharsets.UTF_8); } else { password = null; } if (!login.isEmpty() || !(password == null || password.isEmpty())) { fileURL.setCredentials(new Credentials(login, password)); } // Advance string index pos = atPos+1; } // Parse host and port (if specified) colonPos = authority.indexOf(':', pos); String host; if (colonPos != -1) { host = authority.substring(pos, colonPos); String portString = authority.substring(colonPos+1); if (!portString.isEmpty()) { // Tolerate an empty port part (e.g. http://mucommander.com:/) try { fileURL.setPort(Integer.parseInt(portString)); } catch(NumberFormatException e) { throw new MalformedURLException("URL contains an invalid port"); } } } else { host = authority.substring(pos); } if (host.isEmpty()) { host = null; } fileURL.setHost(host); // Parse path part excluding query part pos = hostEndPos; String path = url.substring(pos, questionMarkPos==-1?urlLen:questionMarkPos); // Empty path means '/' if (path.isEmpty()) { path = "/"; } // Canonize path: factor out '.' and '..' and replace '~' by the replacement string (if any) fileURL.setPath(pathCanonizer.canonize(path)); // log.debug("Warning: path should not be empty, url={}", url); // Parse query part (if any) if (questionMarkPos != -1) { fileURL.setQuery(url.substring(questionMarkPos+1)); // Do not include the question mark } } catch (MalformedURLException e) { throw e; } catch (Exception e2) { log.info("Unexpected exception in FileURL() with {}", url, e2); throw new MalformedURLException(); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/DummyFile.java ================================================ package com.mucommander.commons.file; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * This class is an implementation of AbstractFile which implements all methods as no-op (that do nothing) * that return default values. It makes it easy to quickly create a AbstractFile implementation by simply * overriding the methods that are needed, for example as an anonymous class inside a method. * *

This class should NOT be subclassed for proper AbstractFile implementations. It should only be used in certain * circumstances that require creating a quick AbstractFile implementation where only a few methods will be used. * * @author Maxence Bernard */ public class DummyFile extends AbstractFile { public DummyFile(FileURL url) { super(url); } /** * Implementation notes: always returns 0. */ @Override public long getLastModifiedDate() { return 0; } /** * Implementation notes: always throws {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always. */ @Override @UnsupportedFileOperation public void setLastModifiedDate(long lastModified) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } /** * Implementation notes: always returns -1. */ @Override public long getSize() { return -1; } /** * Implementation notes: always returns null. */ @Override public AbstractFile getParent() { return null; } /** * Implementation notes: no-op, does nothing with the specified parent. */ @Override public void setParent(AbstractFile parent) { } /** * Implementation notes: always returns false. */ @Override public boolean exists() { return false; } /** * Implementation notes: always returns {@link FilePermissions#EMPTY_FILE_PERMISSIONS}. */ @Override public FilePermissions getPermissions() { return FilePermissions.EMPTY_FILE_PERMISSIONS; } /** * Implementation notes: returns {@link PermissionBits#EMPTY_PERMISSION_BITS}, none of the permission bits can be * changed. */ @Override public PermissionBits getChangeablePermissions() { return PermissionBits.EMPTY_PERMISSION_BITS; } /** * Implementation notes: always returns false. */ @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } /** * Implementation notes: always returns null. */ @Override public String getOwner() { return null; } /** * Implementation notes: always returns false. */ @Override public boolean canGetOwner() { return false; } /** * Implementation notes: always returns null. */ @Override public String getGroup() { return null; } /** * Implementation notes: always returns false. */ @Override public boolean canGetGroup() { return false; } /** * Implementation notes: always returns false. */ @Override public boolean isDirectory() { return false; } /** * Implementation notes: always returns false. */ @Override public boolean isArchive() { return false; } /** * Implementation notes: always returns false. */ @Override public boolean isSymlink() { return false; } /** * Implementation notes: always returns false. */ @Override public boolean isSystem() { return false; } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public AbstractFile[] ls() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.LIST_CHILDREN); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void mkdir() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CREATE_DIRECTORY); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public InputStream getInputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.READ_FILE); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void delete() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.DELETE); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void renameTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RENAME); } /** * Implementation notes: always throws {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } /** * Implementation notes: always throws {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } /** * Implementation notes: always returns null. */ @Override public Object getUnderlyingFileObject() { return null; } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/FileAccessDeniedException.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.io.IOException; /** * @author Oleg Trifonov * Created on 28/10/16. */ public class FileAccessDeniedException extends IOException { } ================================================ FILE: src/main/java/com/mucommander/commons/file/FileAttributes.java ================================================ package com.mucommander.commons.file; /** * This interface defines getters for the following file attributes: *

*
path
*
the file's path, null by default. The type of path (relative or absolute) separator character * are unspecified and context-dependant.
* *
exists
*
specifies whether the file exists physically on the underlying filesystem, false by default
* *
date
*
the file's date in milliseconds since the epoch (00:00:00 GMT, January 1, 1970), * 0 (00:00:00 GMT, January 1, 1970) by default
* *
size
*
the file's size in bytes, 0 by default
* *
isDirectory
*
specifies whether the file is a directory or a regular file, false by default
* *
permissions
*
represents the file permissions as a {@link com.mucommander.commons.file.FilePermissions} object, null if * undefined
* *
owner
*
the file's owner, null by default
* *
group
*
the file's group, null by default
*
* *

See the {@link MutableFileAttributes} for an extended interface that include file attribute setters. * * @see MutableFileAttributes * @see SimpleFileAttributes * @author Maxence Bernard */ public interface FileAttributes { /** * Returns the file's path, null by default. * *

The format and separator character of the path are filesystem-dependent. * * @return the file's path, null by default */ String getPath(); /** * Returns true if the file exists physically on the underlying filesystem, false * by default. * * @return true if the file exists physically on the underlying filesystem, false by default */ boolean exists(); /** * Returns the file's date in milliseconds since the epoch (00:00:00 GMT, January 1, 1970), 0 by default * * @return the file's date in milliseconds since the epoch (00:00:00 GMT, January 1, 1970), 0 by default */ long getLastModifiedDate(); /** * Returns the file's size in bytes. * * @return the file's size in bytes */ long getSize(); /** * Returns true if the file is a directory, false if it is a regular file * (defaults to false). * * @return true if the file is a directory, false if it is a regular file or undefined */ boolean isDirectory(); /** * Returns the file's permissions, null by default. * * @return the file's permissions, null by default */ FilePermissions getPermissions(); /** * Returns the file's owner, null by default. * * @return the file's owner, null by default */ String getOwner(); /** * Returns the file's group, null by default. * * @return the file's group, null by default */ String getGroup(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/FileFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import com.mucommander.commons.file.impl.avrdude.AvrdudeProtocolProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.icon.FileIconProvider; import com.mucommander.commons.file.icon.impl.SwingFileIconProvider; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.impl.local.LocalProtocolProvider; import com.mucommander.commons.file.util.FilePool; import com.mucommander.commons.file.util.PathTokenizer; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.runtime.OsFamily; /** * FileFactory is an abstract class that provides static methods to get a {@link AbstractFile} instance for * a specified path or {@link FileURL} location. *

Protocols

*

* In order to allow the com.mucommander.commons.file API to access new file protocols, developers must create * an implementation of {@link AbstractFile} that handles that protocol and register it to FileFactory. * This registration requires an implementation of {@link ProtocolProvider}, an instance of which will be passed to * {@link #registerProtocol(String,ProtocolProvider) registerProtocol}. * *

* Built-in file protocols are: *

    *
  • {@link FileProtocols#FILE Local} files.
  • *
  • {@link FileProtocols#FTP FTP}.
  • *
  • {@link FileProtocols#SFTP SFTP}.
  • *
  • {@link FileProtocols#HTTP HTTP}.
  • *
  • {@link FileProtocols#HTTPS HTTPS}.
  • *
  • {@link FileProtocols#NFS NFS}.
  • *
  • {@link FileProtocols#SMB SMB}.
  • *
* *

Archive formats

*

* In order to allow the com.mucommander.commons.file API to access new archive formats, developers must create * an implementation of {@link AbstractArchiveFile} that handles that format and register it to FileFactory. * This registration requires an implementation of {@link ArchiveFormatProvider}, an instance of which will be passed to * {@link #registerArchiveFormat(ArchiveFormatProvider)}. * *

* Built-in file formats are: *

    *
  • ZIP, registered to zip, jar, war, wal, wmz, xpi, ear, odt, ods and odp files.
  • *
  • TAR, registered to tar, tar.gz, tgz, tar.bz2 and tbz2 files.
  • *
  • GZIP, registered to gz files.
  • *
  • BZip2, registered to bz2 files.
  • *
  • ISO, registered to iso and nrg files.
  • *
  • AR, registered to ar, a and deb files.
  • *
  • LST, registered to lst files.
  • *
  • RAR, registered to rar files.
  • *
  • SEVENZIP, registered to 7z files.
  • *
* * @author Maxence Bernard, Nicolas Rinaudo */ public class FileFactory { private static Logger logger; /** All registered protocol providers. */ private static final Map protocolProviders = new ConcurrentHashMap<>(); /** Local file provider to avoid hashtable lookups (faster). */ private static ProtocolProvider localFileProvider; /** List of registered ArchiveFormatMapping instances */ private static final List archiveFormatProvidersV = new CopyOnWriteArrayList<>(); /** Array of registered FileProtocolMapping instances, for quicker access */ private static ArchiveFormatProvider[] archiveFormatProviders; /** Contains a FilePool instance for each registered scheme */ private static final Map FILE_POOL_MAP = new HashMap<>(); /** System temp directory */ private static final AbstractFile TEMP_DIRECTORY; /** Default file icon provider, initialized in static block */ private static FileIconProvider defaultFileIconProvider; /** Default authenticator, used when none is specified */ private static Authenticator defaultAuthenticator; private static Set archiveExtensions; public static void registerProtocolNetworks() { ProtocolProvider protocolProvider; registerProtocol(FileProtocols.SMB, new com.mucommander.commons.file.impl.smb.SMBProtocolProvider()); registerProtocol(FileProtocols.HTTP, protocolProvider = new com.mucommander.commons.file.impl.http.HTTPProtocolProvider()); // !!! очень долго грузится registerProtocol(FileProtocols.HTTPS, protocolProvider); registerProtocol(FileProtocols.FTP, new com.mucommander.commons.file.impl.ftp.FTPProtocolProvider()); registerProtocol(FileProtocols.NFS, new com.mucommander.commons.file.impl.nfs.NFSProtocolProvider()); registerProtocol(FileProtocols.SFTP, new com.mucommander.commons.file.impl.sftp.SFTPProtocolProvider()); registerProtocol(FileProtocols.HDFS, new com.mucommander.commons.file.impl.hadoop.HDFSProtocolProvider()); //registerProtocol(FileProtocols.S3, new com.mucommander.commons.file.impl.hadoop.S3ProtocolProvider()); registerProtocol(FileProtocols.S3, new com.mucommander.commons.file.impl.s3.S3ProtocolProvider()); registerProtocol(FileProtocols.WEBDAV, new com.mucommander.commons.file.impl.webdav.WebDAVProvider()); registerProtocol(FileProtocols.VSPHERE, new com.mucommander.commons.file.impl.vsphere.VSphereProtocolProvider()); } public static void registerProtocolArchives() { // Register built-in archive file formats, order for TarArchiveFile and GzipArchiveFile/Bzip2ArchiveFile is important: // TarArchiveFile must match 'tar.gz'/'tar.bz2' files before GzipArchiveFile/Bzip2ArchiveFile does. registerArchiveFormat(new com.mucommander.commons.file.impl.zip.ZipFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.tar.TarFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.gzip.GzipFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.bzip2.Bzip2FormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.iso.IsoFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.ar.ArFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.lst.LstFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.rar.RarFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.sevenzip.SevenZipFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.rpm.RpmFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.arj.ArjFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.cab.CabFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.cpio.CpioFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.deb.DebFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.lzh.LzhFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.lzma.LzmaFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.udf.UdfFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.wim.WimFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.xar.XarFormatProvider()); registerArchiveFormat(new com.mucommander.commons.file.impl.z.ZFormatProvider()); /*SevenZipJBindings RPM support lacks RPM Metadata - only payload would be available - there are better java * libs available for handling RPM */ // registerArchiveFormat(new com.mucommander.commons.file.impl.rpm.RpmFormatProvider()); } public static void registerProtocolOthers() { // Hadoop requires Java 1.6 registerProtocol(FileProtocols.HDFS, new com.mucommander.commons.file.impl.hadoop.HDFSProtocolProvider()); // registerProtocol(FileProtocols.S3, new com.mucommander.commons.file.impl.hadoop.S3ProtocolProvider()); registerProtocol(FileProtocols.S3, new com.mucommander.commons.file.impl.s3.S3ProtocolProvider()); registerProtocol(FileProtocols.VSPHERE, new com.mucommander.commons.file.impl.vsphere.VSphereProtocolProvider()); // TODO !!! check that adb installed registerProtocol(FileProtocols.ADB, new com.mucommander.commons.file.impl.adb.AdbProtocolProvider()); registerProtocol(FileProtocols.AVR, new AvrdudeProtocolProvider()); } static { registerProtocol(FileProtocols.FILE, new LocalProtocolProvider()); // Set the default FileIconProvider instance defaultFileIconProvider = new SwingFileIconProvider(); // create the temp directory folder TEMP_DIRECTORY = getFile(System.getProperty("java.io.tmpdir")); } /** * Makes sure no instance of FileFactory is created. */ private FileFactory() { } /** * Registers a new file protocol. *

* If a {@link ProtocolProvider} was already registered to the specified protocol, it will automatically be * unregistered. * *

* The protocol argument is expected to be the protocol identifier, without the trailing ://. * For example, the identifier of the HTTP protocol would be http. This parameter's case is irrelevant, * as it will be stored in all lower-case. * *

* After this call, the various {@link #getFile(String) getFile} methods will be able to resolve files using the * specified protocol. * *

* Built-in file protocols are listed in {@link FileProtocols}. * * @param protocol identifier of the protocol to register. * @param provider object used to create instances of files using the specified protocol. * @return the previously registered protocol provider if any, null otherwise. */ public static ProtocolProvider registerProtocol(String protocol, ProtocolProvider provider) { protocol = protocol.toLowerCase(); // create raw and archive file pools synchronized (FILE_POOL_MAP) { FILE_POOL_MAP.put(protocol, new FilePool()); } // Special case for local file provider. // Note that the local file provider is also added to the provider hashtable. if (protocol.equals(FileProtocols.FILE)) { localFileProvider = provider; } return protocolProviders.put(protocol, provider); } /** * Unregisters the provider associated with the specified protocol. * * @param protocol identifier of the protocol whose provider should be unregistered. * @return the provider that has been unregistered, or null if none. */ public static ProtocolProvider unregisterProtocol(String protocol) { protocol = protocol.toLowerCase(); // Remove raw and archive file pools synchronized (FILE_POOL_MAP) { FILE_POOL_MAP.remove(protocol); } // Special case for local file provider if (protocol.equals(FileProtocols.FILE)) { localFileProvider = null; } return protocolProviders.remove(protocol); } /** * Returns the protocol provider associated with the specified protocol identifier, or null if there * is none. * * @param protocol identifier of the protocol whose provider should be retrieved. * @return the protocol provider registered to the specified protocol identifier, or null if none. */ public static ProtocolProvider getProtocolProvider(String protocol) { return protocolProviders.get(protocol.toLowerCase()); } /** * Returns true if the given protocol has a registered {@link ProtocolProvider}. * * @param protocol identifier of the protocol to test * @return true if the given protocol has a registered {@link ProtocolProvider}. */ public static boolean isRegisteredProtocol(String protocol) { return getProtocolProvider(protocol)!=null; } /** * Returns an iterator on all known protocol names. * *

All objects returned by the iterator's nextElement() method will be string instances. These can * then be passed to {@link #getProtocolProvider(String) getProtocolProvider} to retrieve the associated * {@link ProtocolProvider}. * * @return an iterator on all known protocol names. */ public static Iterator protocols() { return protocolProviders.keySet().iterator(); } /** * Registers a new ArchiveFormatProvider. * * @param provider the ArchiveFormatProvider to register. */ public static void registerArchiveFormat(ArchiveFormatProvider provider) { archiveFormatProvidersV.add(provider); updateArchiveFormatProviderArray(); } /** * Removes a previously-registered ArchiveFormatProvider. *

* To unregister the provider of a particular archive format without knowing the associated provider instance, use * {@link #getArchiveFormatProvider(String)} with a known archive filename to retrieve the provider instance. * For example, FileFactory.unregisterArchiveFormat(FileFactory.getArchiveFormatProvider("file.zip")) * will unregister the (first, if any) Zip provider. * * @param provider the ArchiveFormatProvider to unregister. * @see #getArchiveFormatProvider(String) */ public static void unregisterArchiveFormat(ArchiveFormatProvider provider) { int index = archiveFormatProvidersV.indexOf(provider); if (index != -1) { archiveFormatProvidersV.remove(index); updateArchiveFormatProviderArray(); } } /** * Updates the ArchiveFormatProvider array to reflect the contents of the Vector. */ private static void updateArchiveFormatProviderArray() { archiveFormatProviders = new ArchiveFormatProvider[archiveFormatProvidersV.size()]; archiveFormatProvidersV.toArray(archiveFormatProviders); } /** * Returns the first ArchiveFormatProvider that matches the specified filename, null * if there is none. Note that if a filename matches the {@link java.io.FilenameFilter} of several registered * providers, the first provider matching the filename will be returned. * * @param filename an archive filename that potentially matches one of the registered ArchiveFormatProvider * @return the first ArchiveFormatProvider that matches the specified filename, null if there is none */ private static ArchiveFormatProvider getArchiveFormatProvider(String filename) { if (filename == null || archiveFormatProviders == null) { return null; } for (ArchiveFormatProvider provider : archiveFormatProviders) { if (provider != null && provider.getFilenameFilter() != null && provider.getFilenameFilter().accept(filename)) { return provider; } } return null; } /** * Returns an iterator on all known archive formats. * * @return an iterator on all known archive formats. */ public static Iterator archiveFormats() { return archiveFormatProvidersV.iterator(); } /** * Returns an instance of AbstractFile for the given absolute path. * *

This method does not throw any IOException but returns null if the file could not be created. * * @param absPath the absolute path to the file * @return null if the given path is not absolute or incorrect (doesn't correspond to any file) or * if something went wrong during file creation. */ public static AbstractFile getFile(String absPath) { try { return getFile(absPath, null); } catch(IOException e) { getLogger().info("Caught an exception (file {})", absPath, e); return null; } } /** * Returns an instance of AbstractFile for the given absolute path. * *

This method does not throw any IOException but returns null if the file could not be created. * * @param absPath the absolute path to the file * @param throwException if set to true, an IOException will be thrown if something went wrong during file creation * @return null if the given path is not absolute or incorrect (doesn't correspond to any file) * @throws java.io.IOException and throwException param was set to true. * @throws AuthException if additional authentication information is required to create the file */ public static AbstractFile getFile(String absPath, boolean throwException) throws AuthException, IOException { try { return getFile(absPath, null); } catch(IOException e) { getLogger().info("Caught an exception", e); if (throwException) { throw e; } return null; } } /** * Returns an instance of AbstractFile for the given absolute path and use the given parent for the new file if * not null. AbstractFile subclasses should as much as possible call this method rather than {@link #getFile(String)} * because it is more efficient. * * @param absPath the absolute path to the file * @param parent the returned file's parent * @return an instance of AbstractFile for the specified absolute path. * @throws java.io.IOException if something went wrong during file or file url creation. * @throws AuthException if additional authentication information is required to create the file */ public static AbstractFile getFile(String absPath, AbstractFile parent) throws AuthException, IOException { return getFile(FileURL.getFileURL(absPath), parent); } /** * Returns an instance of AbstractFile for the given FileURL instance. * * @param fileURL the file URL * @return the created file or null if something went wrong during file creation */ public static AbstractFile getFile(FileURL fileURL) { try { return getFile(fileURL, null); } catch(IOException e) { getLogger().info("Caught an exception", e); return null; } } /** * Returns an instance of AbstractFile for the given FileURL instance. * * @param fileURL the file URL * @param throwException if set to true, an IOException will be thrown if something went wrong during file creation * @return the created file * @throws java.io.IOException if something went wrong during file creation */ public static AbstractFile getFile(FileURL fileURL, boolean throwException) throws IOException { try { return getFile(fileURL, null); } catch(IOException e) { getLogger().info("Caught an exception", e); if (throwException) { throw e; } return null; } } /** * Shorthand for {@link #getFile(FileURL, AbstractFile, Authenticator, Object...)} called with the * {@link #getDefaultAuthenticator() default authenticator}. * * @param fileURL the file URL representing the file to be created * @param parent the parent AbstractFile to use as the created file's parent, can be null * @return an instance of {@link AbstractFile} for the given {@link FileURL}. * @throws java.io.IOException if something went wrong during file creation. */ public static AbstractFile getFile(FileURL fileURL, AbstractFile parent, Object... instantiationParams) throws IOException { return getFile(fileURL, parent, defaultAuthenticator, instantiationParams); } /** * Creates and returns an instance of AbstractFile for the given FileURL and uses the specified parent file (if any) * as the created file's parent. * *

Specifying the file parent if an instance already exists allows to recycle the AbstractFile instance * instead of creating a new one when the parent file is requested. * * @param fileURL the file URL representing the file to be created * @param authenticator used to authenticate the specified location if its protocol * {@link FileURL#getAuthenticationType() is authenticated} and the location contains no credentials already. * If the value is null, no {@link Authenticator} will be used, not even the default one. * @param parent the parent AbstractFile to use as the created file's parent, can be null * @return an instance of {@link AbstractFile} for the given {@link FileURL}. * @throws java.io.IOException if something went wrong during file creation. */ public static AbstractFile getFile(FileURL fileURL, AbstractFile parent, Authenticator authenticator, Object... instantiationParams) throws IOException { String protocol = fileURL.getScheme(); if (!isRegisteredProtocol(protocol)) { throw new IOException("Unsupported file protocol: " + protocol); } // Lookup the pool for an existing AbstractFile instance, only if there are no instantiationParams. // If there are instantiationParams (the file was created by the AbstractFile implementation directly, that is // by ls()), any existing file in the pool must be replaced with a new, more up-to-date one. FilePool filePool = FILE_POOL_MAP.get(fileURL.getScheme().toLowerCase()); if (instantiationParams.length == 0) { // Note: FileURL#equals(Object) and #hashCode() take into account credentials and properties and are // trailing slash insensitive (e.g. '/root' and '/root/' URLS are one and the same) AbstractFile file = filePool.get(fileURL); if (file != null) { return file; } } String filePath = fileURL.getPath(); // For local paths under Windows (e.g. "/C:\temp"), remove the leading '/' character if (OsFamily.WINDOWS.isCurrent() && FileProtocols.FILE.equals(protocol)) { filePath = PathUtils.removeLeadingSeparator(filePath, "/"); } String pathSeparator = fileURL.getPathSeparator(); PathTokenizer pt = new PathTokenizer(filePath, pathSeparator, false); AbstractFile currentFile = null; boolean lastFileResolved = false; // Extract every filename from the path from left to right and for each of them, see if it looks like an archive. // If it does, create the appropriate protocol file and wrap it with an archive file. while (pt.hasMoreFilenames()) { // Test if the filename's extension looks like a supported archive format... // Note that the archive can also be a directory with an archive extension. if (isArchiveFilename(pt.nextFilename())) { // Remove trailing separator of file, some file protocols such as SFTP don't like trailing separators. // On the contrary, directories without a trailing slash are fine. String currentPath = PathUtils.removeTrailingSeparator(pt.getCurrentPath(), pathSeparator); // Test if current file is an archive and if it is, create an archive entry file instead of a raw // protocol file if (currentFile == null || !currentFile.isArchive()) { // create a fresh FileURL with the current path FileURL clonedURL = (FileURL)fileURL.clone(); clonedURL.setPath(currentPath); // Look for a cached file instance before creating a new one currentFile = filePool.get(clonedURL); if (currentFile == null) { currentFile = wrapArchive(createRawFile(clonedURL, authenticator, instantiationParams)); // Add the intermediate file instance to the cache filePool.put(clonedURL, currentFile); } lastFileResolved = true; } else { // currentFile is an AbstractArchiveFile // Note: wrapArchive() is already called by AbstractArchiveFile#createArchiveEntryFile() AbstractFile tempEntryFile = ((AbstractArchiveFile)currentFile).getArchiveEntryFile(PathUtils.removeLeadingSeparator(currentPath.substring(currentFile.getURL().getPath().length()), pathSeparator)); if (tempEntryFile.isArchive()) { currentFile = tempEntryFile; lastFileResolved = true; } else { lastFileResolved = false; } // Note: don't cache the entry file } } else { lastFileResolved = false; } } // create last file if it hasn't been already (if the last filename was not an archive), same routine as above // except that it doesn't wrap the file with an archive file if (!lastFileResolved) { // Note: DON'T strip out the trailing separator, as this would cause problems with root resources String currentPath = pt.getCurrentPath(); if (currentFile == null || !currentFile.isArchive()) { FileURL clonedURL = (FileURL)fileURL.clone(); clonedURL.setPath(currentPath); // Note: no need to look a cached file instance, we have already looked for it at the very beginning. currentFile = createRawFile(clonedURL, authenticator, instantiationParams); // Add the final file instance to the cache filePool.put(currentFile.getURL(), currentFile); } else { // currentFile is an AbstractArchiveFile currentFile = ((AbstractArchiveFile)currentFile).getArchiveEntryFile(PathUtils.removeLeadingSeparator(currentPath.substring(currentFile.getURL().getPath().length()), pathSeparator)); // Note: don't cache the entry file } } // Reuse existing parent file instance if one was specified if (parent != null) currentFile.setParent(parent); return currentFile; } private static AbstractFile createRawFile(FileURL fileURL, Authenticator authenticator, Object... instantiationParams) throws IOException { String scheme = fileURL.getScheme().toLowerCase(); // Special case for local files to avoid provider hashtable lookup and other unnecessary checks // (for performance reasons) if (scheme.equals(FileProtocols.FILE)) { if (localFileProvider == null) { throw new IOException("Unknown file protocol: " + scheme); } return localFileProvider.getFile(fileURL, instantiationParams); // Uncomment this line and comment the previous one to simulate a slow filesystem //file = new DebugFile(file, 0, 50); } // Use the protocol hashtable for any other file protocol else { // If an Authenticator has been specified and the specified FileURL's protocol is authenticated and the // FileURL doesn't contain any credentials, use it to authenticate the FileURL. if (authenticator != null && fileURL.getAuthenticationType() != AuthenticationType.NO_AUTHENTICATION && !fileURL.containsCredentials()) { authenticator.authenticate(fileURL); } // Finds the right file protocol provider ProtocolProvider provider = getProtocolProvider(scheme); if (provider == null) { throw new IOException("Unknown file protocol: " + scheme); } return provider.getFile(fileURL, instantiationParams); } } /** * Returns a variation of the given filename, appending a pseudo-unique ID to the filename's prefix while keeping * the same filename extension. * * @param filename base filename */ private static String getFilenameVariation(String filename) { int lastDotPos = filename.lastIndexOf('.'); int len = filename.length(); String nameSuffix = "_" + System.currentTimeMillis() + (new Random().nextInt(10000)); if (lastDotPos == -1) { filename += nameSuffix; } else { filename = filename.substring(0, lastDotPos) + nameSuffix + filename.substring(lastDotPos, len); } return filename; } /** * Creates and returns a temporary local file using the desired filename. If a file with this name already exists * in the temp directory, the filename's prefix (name without extension) will be appended an ID. The filename's * extension will however always be preserved. * *

The returned file may be a {@link LocalFile} or a {@link AbstractArchiveFile} if the extension corresponds * to a registered archive format. * * @param desiredFilename the desired filename for the temporary file. If a file with this name already exists * in the temp directory, the filename's prefix (name without extension) will be appended an ID, but the filename's * extension will always be preserved. * @param deleteOnExit if true, the temporary file will be deleted upon normal termination of the JVM * @return the temporary file, may be a LocalFile or an AbstractArchiveFile if the filename's extension corresponds * to a registered archive format. * @throws IOException if an error occurred while instantiating the temporary file. This should not happen under * normal circumstances. */ public static AbstractFile getTemporaryFile(String desiredFilename, boolean deleteOnExit) throws IOException { if (desiredFilename == null || desiredFilename.isEmpty()) { desiredFilename = "temp"; } // Attempt to use the desired name AbstractFile tempFile = TEMP_DIRECTORY.getDirectChild(desiredFilename); if (tempFile.exists()) { tempFile = TEMP_DIRECTORY.getDirectChild(getFilenameVariation(desiredFilename)); } if (deleteOnExit) { ((java.io.File)tempFile.getUnderlyingFileObject()).deleteOnExit(); } return tempFile; } /** * Creates a temporary file with a default filename. This method is a shorthand for * {@link #getTemporaryFile(String, boolean)} called with a null name. * * @param deleteOnExit if true, the temporary file will be deleted upon normal termination of the JVM * @return the temporary file, may be a LocalFile or an AbstractArchiveFile if the filename's extension corresponds * to a registered archive format. * @throws IOException if an error occurred while instantiating the temporary file. This should not happen under * normal circumstances. */ public static AbstractFile getTemporaryFile(boolean deleteOnExit) throws IOException { return getTemporaryFile(null, deleteOnExit); } /** * Returns the temporary folder, i.e. the parent folder of temporary files returned by * {@link #getTemporaryFile(String, boolean)}. * * @return the temporary folder */ public static AbstractFile getTemporaryFolder() { return TEMP_DIRECTORY; } /** * Returns true if the given filename's extension matches one of the registered archive formats. * * @param filename the filename to test * @return true if the specified filename is a known archive file name, false otherwise. */ public static boolean isArchiveFilename(String filename) { if (archiveExtensions == null) { if (archiveFormatProviders == null) { return false; } archiveExtensions = new HashSet<>(); for (ArchiveFormatProvider provider : archiveFormatProviders) { if (provider != null) { String[] extensions = provider.getFileExtensions(); for (String ext : extensions) { String extWithoutDot = ext.startsWith(".") ? ext.substring(1) : ext; archiveExtensions.add(extWithoutDot.toLowerCase()); } } } } String ext = AbstractFile.getExtension(filename); return ext != null && archiveExtensions.contains(ext.toLowerCase()); //return getArchiveFormatProvider(filename) != null; } /** * Tests if the given file's extension matches that of one of the registered archive formats. * If it does, a corresponding {@link AbstractArchiveFile} instance is created on top of the provided file * and returned. If it doesn't, the provided AbstractFile instance is simply returned. *

* Note that return {@link AbstractArchiveFile} instances may not actually be archives according to * {@link AbstractFile#isArchive()}. An {@link AbstractArchiveFile} instance that is not currently an archive * (either non-existent or a directory) will behave as a regular (non-archive) file. This allows file instances to * go from being an archive to not being an archive (and vice-versa), without having to re-resolve the file. */ public static AbstractFile wrapArchive(AbstractFile file) throws IOException { String filename = file.getName(); // Looks for an archive FilenameFilter that matches the given filename. // Comparing the filename against each and every archive extension has a cost, so we only perform the test if // the filename contains a dot '.' character, since most of the time this method is called with a filename that // doesn't match any of the filters. if (filename.indexOf('.') >= 0) { ArchiveFormatProvider provider = getArchiveFormatProvider(filename); if (provider != null) { return provider.getFile(file); } } return file; } /** * Same as wrapArchive(AbstractFile) but using the given extension rather than the file's extension. */ public static AbstractFile wrapArchive(AbstractFile file, String extension) throws IOException { ArchiveFormatProvider provider = getArchiveFormatProvider(file.getBaseName() + extension); return provider != null ? provider.getFile(file) : file; } /** * Returns the default {@link com.mucommander.commons.file.icon.FileIconProvider} instance. The default provider class * (before {@link #setDefaultFileIconProvider(com.mucommander.commons.file.icon.FileIconProvider)} is called) is * platform-dependent and as such may vary across platforms. * *

It is noteworthy that the provider returned by this method is used by {@link com.mucommander.commons.file.AbstractFile#getIcon()} * to create and return the icon. * * @return the default FileIconProvider implementation */ public static FileIconProvider getDefaultFileIconProvider() { return defaultFileIconProvider; } /** * Sets the default {@link com.mucommander.commons.file.icon.FileIconProvider} implementation. * *

It is noteworthy that the provider returned by this method is used by {@link com.mucommander.commons.file.AbstractFile#getIcon()} * to create and return the icon. * * @param fip the new value for the default FileIconProvider */ public static void setDefaultFileIconProvider(FileIconProvider fip) { defaultFileIconProvider = fip; } /** * Returns the default {@link Authenticator} that is used for authenticating {@link FileURL} instances prior * to resolving the corresponding file. * * @return the default {@link Authenticator} that is used for authenticating {@link FileURL} instances prior * to resolving the corresponding file * @see Authenticator */ public static Authenticator getDefaultAuthenticator() { return defaultAuthenticator; } /** * Sets the default {@link Authenticator} that is used for authenticating {@link FileURL} instances prior * to resolving the corresponding file. * * @param authenticator the new default {@link Authenticator} * @see Authenticator */ public static void setDefaultAuthenticator(Authenticator authenticator) { defaultAuthenticator = authenticator; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(FileFactory.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/FileOperation.java ================================================ package com.mucommander.commons.file; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; /** * @author Maxence Bernard * @see UnsupportedFileOperationException * @see AbstractFile */ public enum FileOperation { /** * Represents a 'read' operation, as specified by {@link AbstractFile#getInputStream()}. * * @see AbstractFile#getInputStream() **/ READ_FILE, /** * Represents a 'random read' operation, as specified by {@link AbstractFile#getRandomAccessInputStream()}. * * @see AbstractFile#getRandomAccessInputStream() **/ RANDOM_READ_FILE, /** * Represents a 'write' operation, as specified by {@link AbstractFile#getOutputStream()}. * * @see AbstractFile#getOutputStream() **/ WRITE_FILE, /** * Represents an 'append' operation, as specified by {@link AbstractFile#getAppendOutputStream()}. * * @see AbstractFile#getAppendOutputStream() **/ APPEND_FILE, /** * Represents a 'random write' operation, as specified by {@link AbstractFile#getRandomAccessOutputStream()}. * * @see AbstractFile#getRandomAccessOutputStream() **/ RANDOM_WRITE_FILE, /** * Represents an 'mkdir' operation, as specified by {@link AbstractFile#mkdir()}. * * @see AbstractFile#mkdir() **/ CREATE_DIRECTORY, /** * Represents an 'ls' operation, as specified by {@link AbstractFile#ls()}. * * @see AbstractFile#ls() **/ LIST_CHILDREN, /** * Represents a 'delete' operation, as specified by {@link AbstractFile#delete()}. * * @see AbstractFile#delete() **/ DELETE, /** * Represents a 'remove copy' operation, as specified by {@link AbstractFile#copyRemotelyTo(AbstractFile)}. */ COPY_REMOTELY, /** * Represents a 'rename' operation, as specified by {@link AbstractFile#renameTo(AbstractFile)}. */ RENAME, /** * Represents a 'change date' operation, as specified by {@link AbstractFile#setLastModifiedDate(long)}. * * @see AbstractFile#setLastModifiedDate(long) **/ CHANGE_DATE, /** * Represents a 'change permission' operation, as specified by {@link AbstractFile#changePermission(int, int, boolean)}. */ CHANGE_PERMISSION, /** * Represents a 'get free space' operation, as specified by {@link AbstractFile#getFreeSpace()}. */ GET_FREE_SPACE, /** * Represents a 'change replication factor' operation, as specified by {@link AbstractFile#changeReplication(short)}}. */ GET_BLOCKSIZE, /** * Represents a 'change replication factor' operation, as specified by {@link AbstractFile#changeReplication(short)}}. */ GET_REPLICATION, /** * Represents a 'change replication factor' operation, as specified by {@link AbstractFile#changeReplication(short)}}. */ CHANGE_REPLICATION, /** * Represents a 'get total space' operation, as specified by {@link AbstractFile#getTotalSpace()}. */ GET_TOTAL_SPACE; private static final Logger LOGGER = LoggerFactory.getLogger(FileOperation.class); /** * Returns the {@link AbstractFile} method corresponding to this file operation. * * @param c the AbstractFile class for which to return a Method object. * @return the {@link AbstractFile} method corresponding to this file operation. */ public Method getCorrespondingMethod(Class c) { try { switch(this) { case READ_FILE: return c.getMethod("getInputStream"); case RANDOM_READ_FILE: return c.getMethod("getRandomAccessInputStream"); case WRITE_FILE: return c.getMethod("getOutputStream"); case APPEND_FILE: return c.getMethod("getAppendOutputStream"); case RANDOM_WRITE_FILE: return c.getMethod("getRandomAccessOutputStream"); case CREATE_DIRECTORY: return c.getMethod("mkdir"); case LIST_CHILDREN: return c.getMethod("ls"); case CHANGE_DATE: return c.getMethod("setLastModifiedDate", Long.TYPE); case GET_BLOCKSIZE: return c.getMethod("getBlocksize"); case GET_REPLICATION: return c.getMethod("getReplication"); case CHANGE_REPLICATION: return c.getMethod("changeReplication", Short.TYPE); case CHANGE_PERMISSION: return c.getMethod("changePermission", Integer.TYPE, Integer.TYPE, Boolean.TYPE); case DELETE: return c.getMethod("delete"); case RENAME: return c.getMethod("renameTo", AbstractFile.class); case COPY_REMOTELY: return c.getMethod("copyRemotelyTo", AbstractFile.class); case GET_FREE_SPACE: return c.getMethod("getFreeSpace"); case GET_TOTAL_SPACE: return c.getMethod("getTotalSpace"); default: // This should never be reached, unless method signatures have changed and this method hasn't been updated. LOGGER.warn("this line should not have been executed"); return null; } } catch (Exception e) { // Should never happen, unless method signatures have changed and this method hasn't been updated. LOGGER.warn("this line should not have been executed", e); return null; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/FilePermissions.java ================================================ package com.mucommander.commons.file; /** * FilePermissions is an interface that represents the permissions of an {@link com.mucommander.commons.file.AbstractFile}. * The actual permission values can be retrieved by the methods inherited from the * {@link com.mucommander.commons.file.PermissionBits} interface. The permissions mask returned by {@link #getMask()} allows * to determine which permission bits are significant, i.e. should be taken into account. That way, certain * {@link AbstractFile} implementations that have limited permissions support can set those supported permission bits * while making it clear that other bits should be ignored, and not simply be considered as being disabled. * For instance, a file implementation with support for the sole 'user' permissions (read/write/execute) will return a * mask whose int value is 448 (700 octal). * *

This interface also defines constants for commonly used file permissions. * * @see com.mucommander.commons.file.AbstractFile#getPermissions() * @author Maxence Bernard */ public interface FilePermissions extends PermissionBits { /** Empty file permissions: read/write/execute permissions cleared for user/group/other (0), none of the permission * bits are supported (mask is 0) */ FilePermissions EMPTY_FILE_PERMISSIONS = new SimpleFilePermissions(0, 0); /** Default file permissions used by {@link AbstractFile#importPermissions(AbstractFile)} for permission bits that * are not available in the source: rw-r--r-- (644 octal). All the permission bits are marked as supported. */ FilePermissions DEFAULT_FILE_PERMISSIONS = new SimpleFilePermissions(420, FULL_PERMISSION_BITS); /** Default directory permissions used by {@link AbstractFile#importPermissions(AbstractFile)} for permission bits that * are not available in the source: rwxr-xr-x (755 octal). All the permission bits are marked as supported. */ FilePermissions DEFAULT_DIRECTORY_PERMISSIONS = new SimpleFilePermissions(493, FULL_PERMISSION_BITS); FilePermissions DEFAULT_EXECUTABLE_PERMISSIONS = new SimpleFilePermissions(493, FULL_PERMISSION_BITS); /** * Returns the mask that indicates which permission bits are significant and should be taken into account. * Permission bits that are unsupported have no meaning and their value should simply be ignored. * * @return the mask that indicates which permission bits are significant and should be taken into account. */ PermissionBits getMask(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/FileProtocols.java ================================================ package com.mucommander.commons.file; /** * This interface contains a set of known protocol names, that can be found in {@link FileURL}. * * @author Maxence Bernard, Nicolas Rinaudo */ public interface FileProtocols { /** Protocol for local or locally mounted files. */ String FILE = "file"; /** Protocol for files served by an FTP server. */ String FTP = "ftp"; /** Protocol for files served by a web server using HTTP. */ String HTTP = "http"; /** Protocol for files served by an HDFS (Hadoop distributed filesystem) cluster. */ String HDFS = "hdfs"; /** Protocol for files served by a web server using HTTPS. */ String HTTPS = "https"; /** Protocol for files served by an NFS server. */ String NFS = "nfs"; /** Protocol for files served by an Amazon S3 (or protocol-compatible) server. */ String S3 = "s3"; /** Protocol for files served by an SFTP server (not to be confused with FTPS or SCP). */ String SFTP = "sftp"; /** Protocol for files served by an SMB/CIFS server. */ String SMB = "smb"; /** Protocol for files served by a web server using WebDAV/HTTP. */ String WEBDAV = "webdav"; /** Protocol for files served by a web server using WebDAV/HTTPS. */ String WEBDAVS = "webdavs"; /** Protocol for files served by a web server using vSphere. */ String VSPHERE = "vsphere"; /** Protocol for files on android devices. */ String ADB = "adb"; /** Protocol for avrdude programmer. */ String AVR = "avr"; } ================================================ FILE: src/main/java/com/mucommander/commons/file/FileURL.java ================================================ package com.mucommander.commons.file; import com.mucommander.commons.file.compat.CompatURLStreamHandler; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.util.StringUtils; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * This class represents a Uniform Resource Locator (URL). The general format of a URL is as follows: *

 * 	scheme://[login[:password]@]host[:port][/path][?query]
 * 
* *

Instantiation

*

* FileURL cannot be instantiated directly, instances can be created using {@link #getFileURL(String)}. * Unlike the java.net.URL and java.net.URI classes, FileURL instances are mutable -- * all URL parts can be freely modified. FileURL instances can also be cloned using the standard {@link #clone()} method. * *

Handlers and Scheme-specific attributes

*

* In addition to standard URL features, FileURL gives access to scheme-specific attributes: *

*
{@link #getStandardPort() standard port}
the standard port implied when no port is defined in the URL, * e.g. 21 for FTP
*
{@link #getPathSeparator() path separator}
the character(s) that separates path fragments, e.g. '/' for * most schemes, '\' for local paths under certain OSes like Windows.
*
{@link #getGuestCredentials() guest credentials}
credentials to authenticate as a guest, e.g. 'GUEST' * for SMB, 'anonymous' for FTP.
*
{@link #getRealm() authentication realm}
the base URL throughout which a set of credentials can be used. *
*
* These attribute values are provided by the {@link SchemeHandler} registered with the scheme, if any. *

* In addition to providing those attributes, a SchemeHandler provides a {@link SchemeParser} * instance which takes care of the actual parsing of URLs of a particular scheme when {@link #getFileURL(String)} is * invoked. This allows for scheme-specific parsing, like for example for the query part which should only be parsed * and considered as a separate part for certain schemes such as HTTP. *

* This class registers a number of handlers for the schemes/protocols supported by the muCommander file API. * Additional handlers can be registered dynamically using {@link #registerHandler(String, SchemeHandler)}. Likewise, * existing handlers can be unregistered or replaced at runtime using registerHandler and * unregisterHandler. *

* A {@link #getDefaultHandler() default handler} is used for schemes that do not have a specific handler registered. * It provides default values for the above-mentioned attributes and provides a parser that parses those scheme URLs. * The default handler's parser is also used for parsing locations passed to {@link #getFileURL(String)} that do not * contain a scheme (i.e. without the leading scheme://). Those locations can be system-dependent, * local and absolute paths, or UNC paths. These paths are turned by the parser into an equivalent, fully-qualified URL. * *

Properties

*

* This class provides methods to attach properties to a FileURL instance. These properties are not part of the URL * itself and are absent from its string representation. They allow protocol-specific properties like connection * settings to be passed along, to {@link AbstractFile} instances in particular. * *

Limitations

*

* This class has the several limitations that are worth noting: *

    *
  • URL syntax is not strictly enforced: some invalid URLs (as per RFC) will be parsed without throwing an exception
  • *
  • relative URLs are not supported
  • *
  • no proper percent encoding/decoding
  • *
  • no support for the fragment part
  • *
* Some of these limitations will be addressed in upcoming revisions of this class. * * @see SchemeHandler * @see SchemeParser * @author Maxence Bernard */ public class FileURL implements Cloneable { // Todo: add support for the fragment part // Todo: add percent encoding/decoding /** Handler instance that provides the scheme-specific features of this FileURL */ private SchemeHandler handler; /** Scheme part */ private String scheme; /** Port part, -1 if this URL has none */ private int port = -1; /** Host part, null if this URL has none */ private String host; /** Path part */ private String path; /** Filename, extracted from the path, null if the path has none */ private String filename; /** Query part, null if this URL has none */ private String query; /** Properties, null if none have been set thus far */ private Map properties; /** Credentials (login and password parts), null if this URL has none */ private Credentials credentials; /** Caches the value returned by #hashCode() for as long as this instance is not modified */ private int hashCode; /** Default handler for schemes that do not have a specific handler */ private static final SchemeHandler DEFAULT_HANDLER = new DefaultSchemeHandler(); /** Maps schemes (String) onto SchemeHandler instances */ private static final Map handlers = new HashMap<>(); /** String designating the localhost */ public final static String LOCALHOST = "localhost"; static { // Register custom handlers for known schemes registerHandler(FileProtocols.FILE, new DefaultSchemeHandler(new DefaultSchemeParser(new DefaultPathCanonizer(LocalFile.SEPARATOR, System.getProperty("user.home")), false), -1, System.getProperty("file.separator"), AuthenticationType.NO_AUTHENTICATION, null)); registerHandler(FileProtocols.FTP, new DefaultSchemeHandler(new DefaultSchemeParser(), 21, "/", AuthenticationType.AUTHENTICATION_REQUIRED, new Credentials("anonymous", "anonymous_coward@mucommander.com"))); registerHandler(FileProtocols.SFTP, new DefaultSchemeHandler(new DefaultSchemeParser(), 22, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.HDFS, new DefaultSchemeHandler(new DefaultSchemeParser(true), 8020, "/", AuthenticationType.AUTHENTICATION_OPTIONAL, null)); registerHandler(FileProtocols.HTTP, new DefaultSchemeHandler(new DefaultSchemeParser(true), 80, "/", AuthenticationType.AUTHENTICATION_OPTIONAL, null)); registerHandler(FileProtocols.S3, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.WEBDAV, new DefaultSchemeHandler(new DefaultSchemeParser(true), 80, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.HTTPS, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_OPTIONAL, null)); registerHandler(FileProtocols.WEBDAVS, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.NFS, new DefaultSchemeHandler(new DefaultSchemeParser(), 2049, "/", AuthenticationType.NO_AUTHENTICATION, null)); registerHandler(FileProtocols.VSPHERE, new DefaultSchemeHandler(new DefaultSchemeParser(true), 443, "/", AuthenticationType.AUTHENTICATION_REQUIRED, null)); registerHandler(FileProtocols.SMB, new DefaultSchemeHandler(new DefaultSchemeParser(), -1, "/", AuthenticationType.AUTHENTICATION_REQUIRED, new Credentials("GUEST", "")) { @Override public FileURL getRealm(FileURL location) { FileURL realm = new FileURL(this); String newPath = location.getPath(); // Find first path token (share) int pos = newPath.indexOf('/', 1); newPath = newPath.substring(0, pos < 0 ? newPath.length() : pos+1); realm.setPath(newPath); realm.setScheme(location.getScheme()); realm.setHost(location.getHost()); realm.setPort(location.getPort()); // Copy properties (if any) realm.importProperties(location); return realm; } }); } /** * Private constructor. Creates an empty FileURL that uses the given handler, all parts have to be manually set. * * @param handler the handler to have this FileURL use */ private FileURL(SchemeHandler handler) { this.handler = handler; } /** * This method is called whenever this instance is modified to invalidate caches. */ private void urlModified() { hashCode = 0; } /** * Creates and returns a new FileURL instance from the given location, throws a MalformedURLException * if the specified location is not a valid URL or path and cannot be resolved. The {@link SchemeParser parser} * of the {@link SchemeHandler handler} registered for the location's scheme is used to parse the given location. * If the scheme specified in the location does not have a specific handler, or if the location does not contain a * scheme (i.e. is local or UNC path, not a URL) then the default handler's parser is used. * * @param location the URL or path for which to get a FileURL instance * @throws MalformedURLException if the specified string isn't a valid URL, according to the scheme's parser used * @return a FileURL corresponding to the given location */ public static FileURL getFileURL(String location) throws MalformedURLException { if (location == null) { return null; } int schemeDelimPos = location.indexOf("://"); SchemeHandler handler; if (schemeDelimPos == -1) { // No scheme: the location is a local or UNC path, not a URL handler = getDefaultHandler(); } else { handler = getSchemeHandler(location.substring(0, schemeDelimPos)); } FileURL fileURL = new FileURL(handler); try { handler.getParser().parse(location, fileURL); } catch (Exception e) { // Catch any unexpected exception thrown by the SchemeParser and turn it into a MalformedURLException // with a specific error message. if (e instanceof MalformedURLException) { throw (MalformedURLException) e; } throw new MalformedURLException("URL parser error"); } return fileURL; } /** * Returns the handler registered the specified scheme if there is one, the default handler otherwise. * * @param scheme the scheme for which to return a handler * @return a handler for the specified scheme */ private static SchemeHandler getSchemeHandler(String scheme) { SchemeHandler handler = getRegisteredHandler(scheme); if (handler == null) { return getDefaultHandler(); } return handler; } /** * Returns the SchemeHandler instance that provides the scheme-specific features of this FileURL. * * @return the SchemeHandler instance that provides the scheme-specific features of this FileURL */ public SchemeHandler getHandler() { return handler; } /** * Sets the SchemeHandler that provides the scheme-specific features of this FileURL. *

* Important: after calling this method, the scheme should also be changed to match the new handler -- * changing the handler without changing the scheme to an appropriate one will result in inconsistent * scheme-specific attributes to be returned. * * @param handler the SchemeHandler instance that provides the scheme-specific features of this FileURL */ public void setHandler(SchemeHandler handler) { this.handler = handler; } /** * Registers a handler for the specified scheme, replacing any handler previously registered * for the same scheme. * * @param scheme the scheme to associate the handler with (case-insensitive) * @param handler the new handler in charge of the scheme */ static void registerHandler(String scheme, SchemeHandler handler) { handlers.put(scheme.toLowerCase(), handler); } /** * Removes any handler associated with the specified scheme, leaving the default handler in charge of the scheme. * This method has no effect if there is no handler registered for the scheme. * * @param scheme the scheme to remove the handler for */ static void unregisterHandler(String scheme) { handlers.remove(scheme.toLowerCase()); } /** * Returns the handler registered for the specified scheme, null if there isn't any. * * @param scheme the scheme for which to return the handler * @return the handler registered for the specified scheme */ public static SchemeHandler getRegisteredHandler(String scheme) { return handlers.get(scheme.toLowerCase()); } /** * Returns the default handler, which handles schemes which do not have a specific handler. * The returned instance is a {@link DefaultSchemeHandler} created with the no-arg constructor. * * @return the default handler */ static SchemeHandler getDefaultHandler() { return DEFAULT_HANDLER; } /** * Extracts the filename from the given path and returns it, or null if the path does not contain * a filename. * * @param path the path from which to extract a filename * @param separator the path separator * @return the filename extracted from the given path, null if the path doesn't contain any */ public static String getFilenameFromPath(String path, String separator) { if (path.isEmpty() || path.equals("/")) { return null; } // Remove any trailing separator path = PathUtils.removeTrailingSeparator(path, separator); if (!separator.equals("/")) { path = PathUtils.removeLeadingSeparator(path, "/"); } // Extract filename int pos = path.lastIndexOf(separator); if (pos < 0) { return null; } return path.substring(pos+1); } /** * Returns the scheme part of this URL. The returned scheme may never be null. * * @return the scheme part of this FileURL. * @see #setScheme(String) */ public String getScheme() { return scheme; } /** * Sets the scheme part of this URL. An IllegalArgumentException will be thrown if the specified scheme * is null or an empty string. *

* Important: after calling this method, the handler should also be changed to match the new scheme -- * changing the scheme without changing the handler to an appropriate one will result in inconsistent * scheme-specific attributes to be returned. * * @param scheme new scheme part of this URL. * @throws IllegalArgumentException if the specified is null or an empty string * @see #getScheme() */ public void setScheme(String scheme) { if (scheme == null) { throw new IllegalArgumentException(); } this.scheme = scheme; urlModified(); } /** * Returns the host part of this URL, null if it doesn't contain any. * * @return the host part of this URL. * @see #setHost(String) */ public String getHost() { return host; } /** * Sets the host part of this URL, null for no host. * * @param host new host part of this URL. * @see #getHost() */ public void setHost(String host) { this.host = host; urlModified(); } /** * Returns the port part of this URL, -1 if none was specified in the URL. * * @return the port part of this URL, -1 if there isn't any. * @see #setPort(int) * @see #getDefaultHandler() */ public int getPort() { return port; } /** * Sets the port part of this URL, -1 for no specific port. * * @param port new port part of this URL. * @see #getPort() * @see #getDefaultHandler() */ public void setPort(int port) { this.port = port; urlModified(); } /** * Returns this scheme's standard port, -1 if the scheme doesn't have any. * If this URL doesn't have a specific port part, the return value should be considered as being this URL's port. * *

Some file protocols may not have a notion of standard port or even no use for the port part at all, for * example those that are not TCP or UDP based such as the local 'file' scheme. * *

This method is just a shorthand for getHandler().getStandardPort(). * * @return the scheme's standard port * @see #getPort() */ public int getStandardPort() { return handler.getStandardPort(); } /** * Returns the login part of this URL, null if there isn't any. * * @return the login part of this URL, null if there isn't any * @see #getCredentials() */ public String getLogin() { return credentials==null?null:credentials.getLogin(); } /** * Returns the password part of this URL, null if there isn't any. * * @return the password part of this URL, null if there isn't any * @see #getCredentials() */ public String getPassword() { return credentials==null?null:credentials.getPassword(); } /** * Returns the type of authentication used by the scheme's file protocol. The returned value is one of the constants * defined in the {@link AuthenticationType} enum. * *

This method is just a shorthand for getHandler().getAuthenticationType(). * * @return the type of authentication used by the scheme's file protocol */ public AuthenticationType getAuthenticationType() { return handler.getAuthenticationType(); } /** * Returns true if this URL contains credentials, i.e. a login and/or password part. If true is * returned, {@link #getCredentials()} will return a non-null value. * * @return true if this URL contains credentials, false otherwise. */ public boolean containsCredentials() { return credentials!=null; } /** * Returns the credentials (login and password) contained by this URL, wrapped in an {@link Credentials} object. * Returns null if this URL doesn't have a login or password part. * *

The returned credentials may or may be of any use for the scheme's file protocol depending on the value * returned by {@link #getAuthenticationType()}. * * @return the credentials contained by this URL, null if this URL doesn't have a login or password part. * @see #setCredentials(Credentials) * @see #getAuthenticationType() */ public Credentials getCredentials() { return credentials; } /** * Sets the login and password parts of this URL. Any credentials contained by this FileURL will be replaced. * null can be passed to discard existing credentials. * *

Credentials may or may not be of any use for the scheme's file protocol depending on the value * returned by {@link #getAuthenticationType()}. * * @param credentials the new login and password parts, replacing any existing credentials. If null is passed, * existing credentials will be discarded. * @see #getCredentials() */ public void setCredentials(Credentials credentials) { // Empty credentials are equivalent to null credentials this.credentials = credentials == null || credentials.isEmpty() ? null : credentials; urlModified(); } /** * Returns this scheme's guest credentials, null if the scheme doesn't have any. *

* Guest credentials offer a way to authenticate a URL as a 'guest' on file protocols that require a set of * credentials to establish a connection. The returned credentials are provided with no guarantee that the filesystem * will actually accept them and allow the request/connection. The notion of 'guest' credentials may or may not * have a meaning depending on the underlying file protocol. * *

This method is just a shorthand for getHandler().getGuestCredentials(). * * @return the scheme's guest credentials, null if the scheme doesn't have any */ public Credentials getGuestCredentials() { return handler.getGuestCredentials(); } /** * Returns the path part of this URL. The returned value will never be null and always start with a * leading '/' character. * * @return the path part of this URL. * @see #setPath(String) */ public String getPath() { return path; } /** * Sets the path part of this URL. The specified path cannot be null and must start with a leading * '/' character. If the specified path value is null, then the path will be set to "/". * If the path does not start with a leading separator, one will be added. * * @param path new path part of this URL * @see #getPath() */ public void setPath(String path) { if (path == null || path.isEmpty()) { path = "/"; } if (!path.startsWith("/")) { path = "/" + path; } this.path = path; // Extract new filename from path this.filename = getFilenameFromPath(path, getPathSeparator()); urlModified(); } /** * Returns this scheme's path separator, which serves as a delimiter for path fragments. For most schemes, this is * the forward slash character. * *

This method is just a shorthand for getHandler().getPathSeparator(). * * @return this scheme's path separator */ public String getPathSeparator() { return handler.getPathSeparator(); } /** * Returns the parent of this URL according to its path, null if this URL has no parent (its path is "/"). *

* The returned FileURL will have the same handler, scheme, host, port, credentials and properties as this one. * The query part of the returned parent URL will always be null, even if this URL had one. * *

Note: this method returns a new FileURL instance every time it is called, and all mutable fields of this FileURL * are cloned. Therefore, the returned URL can be safely modified without any risk of side effects. * * @return this URL's parent, null if it doesn't have one. */ public FileURL getParent() { // If path equals '/', url has no parent if (!(path.equals("/") || path.isEmpty())) { String separator = getPathSeparator(); // Remove any trailing separator String parentPath = path.endsWith(separator)?path.substring(0, path.length()-separator.length()):path; // Resolve parent folder's path and reconstruct parent URL int lastSeparatorPos = parentPath.lastIndexOf(separator); if (lastSeparatorPos >= 0) { FileURL parentURL = new FileURL(handler); parentURL.scheme = scheme; parentURL.host = host; parentURL.port = port; parentURL.path = parentPath.substring(0, lastSeparatorPos+1); // Keep trailing slash parentURL.filename = getFilenameFromPath(parentURL.path, separator); // Set same credentials for parent, (if any) // Note: Credentials are immutable. parentURL.credentials = credentials; // Copy properties to parent (if any) if (properties != null) parentURL.properties = new HashMap<>(properties); return parentURL; } } return null; // URL has no parent } /** * Returns the authentication realm corresponding to this URL, i.e. the base location throughout which credentials * can be used. Any property contained by the specified FileURL will be carried over in the returned FileURL. * On the contrary, credentials will not be copied, the returned URL always has no credentials. * *

Note: this method returns a new FileURL instance every time it is called. Therefore the returned FileURL can * safely be modified without any risk of side effects. *

This method is just a shorthand for getHandler().getRealm(this). * * @return this url's authentication realm */ public FileURL getRealm() { return handler.getRealm(this); } /** * Returns the filename of this URL , null if doesn't have one (e.g. if the path is "/"). *

* There is no setFilename as the filename is simply extrapolated from the path. * Use {@link #setPath(String)} to change the path and its filename. * * @return the filename of this URL, null if it doesn't have one. * @see #setPath(String) */ public String getFilename() { return filename; } /** * Returns the query part of this URL if it has one, null otherwise. * * @return the query part of this URL if it has one, null otherwise * @see #setQuery(String) */ public String getQuery() { return query; } /** * Sets the query part of this URL, null for no query part. * * @param query new query part of this URL, null for no query part * @see #getQuery() */ public void setQuery(String query) { this.query = query; urlModified(); } /** * Returns the value corresponding to the given property name, null if the property has no value. * * @param name name of the property whose value is to be retrieved * @return the value associated with the specified property name, null if it has no value * @see #setProperty(String,String) */ public String getProperty(String name) { return properties==null?null:properties.get(name); } /** * Sets the given property (name/value pair) in the FileURL instance. A null property value has the * effect of removing the property. * * @param name name of the property to set * @param value value of the property * @see #getProperty(String) */ public void setProperty(String name, String value) { // create the property map only when a property is set for the first time if (properties == null) { properties = new HashMap<>(); } if (value == null) { properties.remove(name); } else { properties.put(name, value); } urlModified(); } /** * Returns an Enumeration of all property names this FileURL contains. * * @return an Enumeration of all property names this FileURL contains */ public Set getPropertyNames() { return properties == null ? new HashSet<>() : properties.keySet(); } /** * Copy the properties of the given FileURL into this FileURL. * * @param url FileURL instance whose properties should be imported into this one. */ public void importProperties(FileURL url) { // Slight optimization to avoid creating an enumeration if the FileURL doesn't have any property if (url.properties == null) { return; } Set propertyKeys = url.getPropertyNames(); for (String key : propertyKeys) { setProperty(key, url.getProperty(key)); } } /** * Returns a String representation of this FileURL, including the login and password parts (credentials) only if * specified, and masking the password as requested. * * @param includeCredentials if true, the login and password parts (if any) will be included in the * returned URL. * @param maskPassword if true and the includeCredentials parameter is also true, the password's * characters (if any) will be replaced by '*' characters. This allows a URL containing credentials to be displayed * to the end user without revealing the actual password. * @return a string representation of this FileURL */ public String toString(boolean includeCredentials, boolean maskPassword) { StringBuilder sb = new StringBuilder(scheme); sb.append("://"); if (includeCredentials && credentials != null) { sb.append(URLEncoder.encode(credentials.getLogin(), StandardCharsets.UTF_8)); String password = credentials.getPassword(); if (!password.isEmpty()) { sb.append(':'); if (maskPassword) { sb.append(credentials.getMaskedPassword()); } else { sb.append(URLEncoder.encode(password, StandardCharsets.UTF_8)); } } sb.append('@'); } if (host != null) { sb.append(host); } // Set the port only if it has a value that is different from the standard port if (port != -1 && port != handler.getStandardPort()) { sb.append(':'); sb.append(port); } if (host != null || !path.equals("/")) { // Test to avoid URLs like 'smb:///' if (path.startsWith("/")) { sb.append(path); } else { // Add a leading '/' if path doesn't already start with one, needed for scheme paths that are not // forward slash-separated sb.append('/'); sb.append(path); } } if (query != null) { sb.append('?'); sb.append(query); } return sb.toString(); } /** * Returns a String representation of this FileURL, including the login and password parts (credentials) only if * requested. * * @param includeCredentials if true, the login and password parts (if any) will be included in the * returned URL. * @return a string representation of this FileURL. */ public String toString(boolean includeCredentials) { return toString(includeCredentials, false); } /** * Creates and returns a java.net.URL referring to the same location as this FileURL. * The java.net.URL is created from the string representation of this FileURL. * Thus, any credentials this FileURL contains are preserved, but properties are lost. * *

The returned URL uses an {@link AbstractFile} to access the associated resource. * An {@link AbstractFile} instance is created by the underlying URLConnection when the URL is * connected. * *

It is important to note that this method is provided for interoperability purposes, for the sole purpose of * connecting to APIs that require a java.net.URL. * * @return a java.net.URL referring to the same location as this FileURL * @throws MalformedURLException if the java.net.URL could not parse the location of this FileURL */ public URL getJavaNetURL() throws MalformedURLException { return new URL(null, toString(true), new CompatURLStreamHandler()); } /** * Returns true if the scheme part of this URL and the given URL are equal. * The comparison is case-sensitive. * * @param url the URL to test for scheme equality * @return true if the scheme part of this URL and the given URL are equal */ public boolean schemeEquals(FileURL url) { return this.scheme.equalsIgnoreCase(url.scheme); } /** * Returns true if the host part of this URL and the given URL are equal. * The comparison is case-insensitive. * * @param url the URL to test for host equality * @return true if the host part of this URL and the given URL are equal */ public boolean hostEquals(FileURL url) { // Note: StringUtils#equals is null-safe return StringUtils.equals(this.host, url.host, false); } /** * Returns true if the port of this URL and the given URL's are equal. Ports are said to be equal if * the values returned by {@link #getPort()} are equal, or if both URLs have the same standard port * (as returned by {@link #getStandardPort()} and one of the port value is -1 (undefined) and the other * is the standard port. * * @param url the URL to test for port equality * @return true if the port of this URL and the given one are equal */ public boolean portEquals(FileURL url) { int port1 = this.port; int port2 = url.port; int standardPort = getStandardPort(); return port1==port2 || (standardPort==url.getStandardPort() && ((port1==-1 && port2==standardPort || (port2==-1 && port1==standardPort)))); } /** * Returns true if the path of this URL and the given URL are equal. The comparison is case-sensitive. * If the sole difference between two paths is a trailing path separator (and both URLs have the same path separator), * they will be considered as equal. * For example, /path and /path/ are considered equal, assuming the path separator is '/'. * *

It is noteworthy that this method uses java.lang.String#equals(Object) to compare URL paths, * which in some rare cases may return false for non-ascii/Unicode paths that have the same written * representation but are not equal according to java.lang.String#equals(Object). Handling such cases * would require a locale-aware String comparison which is not an option here. * * @param url the URL to test for path equality * @return true if the path of this URL and the given URL are equal */ public boolean pathEquals(FileURL url) { boolean isCaseSensitiveOS = !(OsFamily.WINDOWS.isCurrent() || OsFamily.OS_2.isCurrent()); String path1 = isCaseSensitiveOS ? this.getPath() : this.getPath().toLowerCase(); String path2 = isCaseSensitiveOS ? url.getPath() : url.getPath().toLowerCase(); if (path1.equals(path2)) return true; String separator = getPathSeparator(); if (separator.equals(url.getPathSeparator())) { int separatorLen = separator.length(); int len1 = path1.length(); int len2 = path2.length(); // If the difference between the 2 strings is just a trailing path separator, we consider the paths as equal if (Math.abs(len1-len2) == separatorLen && (len1 > len2 ? path1.startsWith(path2) : path2.startsWith(path1))) { String diff = len1>len2 ? path1.substring(len1-separatorLen) : path2.substring(len2-separatorLen); return separator.equals(diff); } } return false; } /** * Returns true if the query part of this URL and the given URL are equal. * The comparison is case-sensitive. * * @param url the URL to test for query equality * @return true if the query part of this URL and the given URL are equal */ public boolean queryEquals(FileURL url) { return StringUtils.equals(this.query, url.query, true); } /** * Returns true if the credentials (login and password) of this URL and the given URL are equal. * The comparison is case-sensitive. * * @param url the URL to test for credentials equality * @return true if the credentials of this URL and the given URL are equal */ public boolean credentialsEquals(FileURL url) { Credentials creds1 = this.credentials; Credentials creds2 = url.credentials; return (creds1 == null && creds2 == null) || (creds1 != null && creds1.equals(creds2, true)) || (creds2 != null && creds2.equals(creds1, true)); } /** * Returns true if the properties contained by this URL and the given URL are equal. * The comparison of each property is case-sensitive. * * @param url the URL to test for properties equality * @return true if the properties contained by this URL and the given URL are equal */ private boolean propertiesEquals(FileURL url) { return (this.properties == null && url.properties == null) || (this.properties != null && this.properties.equals(url.properties)) || (url.properties != null && url.properties.equals(this.properties)); } //////////////////////// // Overridden methods // //////////////////////// /** * Returns a String representation of this FileURL, without including the login and password parts it may have. */ public String toString() { return toString(false); } /** * Returns a clone of this FileURL. The returned instance can safely be modified without any impact on this FileURL * or any previously cloned URL. */ @Override public Object clone() { // create a new FileURL return it, instead of using Object.clone() which is probably way slower; // most FileURL fields are immutable and as such reused in cloned instance FileURL clonedURL = new FileURL(handler); // Immutable fields clonedURL.scheme = scheme; clonedURL.host = host; clonedURL.port = port; clonedURL.path = path; clonedURL.filename = filename; clonedURL.query = query; clonedURL.credentials = credentials; // Note: Credentials are immutable. // Mutable fields if (properties != null) { // Copy properties (if any) clonedURL.properties = new HashMap<>(properties); } // Caches clonedURL.hashCode = hashCode; return clonedURL; } /** * This method is equivalent to calling {@link #equals(Object, boolean, boolean)} with credentials and properties * comparisons enabled. * * @param o object to compare against this FileURL instance. * @return true if both FileURL instances are equal. */ public boolean equals(Object o) { return equals(o, true, true); } /** * Tests the specified FileURL for equality with this FileURL. false is systematically returned if the * specified object is not a FileURL instance or is null. *

* Two FileURL instances are said to be equal if: *

    *
  • schemes are equal (case-insensitive)
  • *
  • hosts are equal (case-insensitive)
  • *
  • ports are equal. The default port is taken into account when comparing ports: a non specified port part (-1) * is equivalent to the scheme's standard port. For instance, http://mucommander.com:80/ * and http://mucommander.com/ are considered equal.
  • *
  • paths are equal (case-sensitive). There can be a trailing separator difference in the two paths, they will * still be considered as equal. For example, /path and /path/ are considered equal * (assuming the path separator is '/').
  • *
  • queries are equal (case-sensitive)
  • *
*

* Credentials (login and password parts) are compared only if requested. The comparison for both the login and * password is case-sensitive.
* Likewise, properties are compared only if requested: the comparison of all properties is case-sensitive. * * @param o object to compare against this FileURL instance * @param compareCredentials if true, the login and password parts of both FileURL need to be * equal (case-sensitive) for the FileURL instances to be equal * @param compareProperties if true, all properties need to be equal (case-sensitive) in both * FileURL for them to be equal * @return true if both FileURL instances are equal */ public boolean equals(Object o, boolean compareCredentials, boolean compareProperties) { if (!(o instanceof FileURL)) { return false; } FileURL url = (FileURL)o; return pathEquals(url) // Compare the path first as it is the most likely to be different && schemeEquals(url) && hostEquals(url) && portEquals(url) && queryEquals(url) && (!compareCredentials || credentialsEquals(url)) && (!compareProperties || propertiesEquals(url)); } /** * This method is overridden to return a hash code that takes into account the behavior of {@link FileURL#equals(Object)}, * so that url1.equals(url2) implies url1.hashCode()==url2.hashCode(). */ public int hashCode() { if (hashCode == 0) { String separator = handler.getPathSeparator(); // #equals(Object) is trailing separator insensitive, so the hashCode must be trailing separator invariant int h = PathUtils.getPathHashCode(path, separator); h = 31* h + scheme.toLowerCase().hashCode(); h = 31* h + (port==-1?handler.getStandardPort():port); if (host != null) h = 31* h + host.toLowerCase().hashCode(); if (query != null) h = 31* h + query.hashCode(); if (credentials != null) h = 31* h + credentials.hashCode(); if (properties != null) h = 31* h + properties.hashCode(); // Cache the value until for as long as this instance is not modified hashCode = h; } return hashCode; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/GroupedPermissionBits.java ================================================ package com.mucommander.commons.file; /** * GroupedPermissionBits is an implementation of {@link com.mucommander.commons.file.PermissionBits} using a given UNIX-style * permission int: {@link #getIntValue()} returns the specified int, and {@link #getBitValue(int, int)} isolates a * specified value. * * @see com.mucommander.commons.file.IndividualPermissionBits * @author Maxence Bernard */ public class GroupedPermissionBits implements PermissionBits { /** UNIX-style permission int */ protected int permissions; /** * Creates a new GroupedPermissionBits using the specified UNIX-style permission int. The int can be created * by combining (binary OR and shift) values defined in {@link com.mucommander.commons.file.PermissionTypes} and * {@link com.mucommander.commons.file.PermissionAccesses}. * * @param permissions a UNIX-style permission int. */ public GroupedPermissionBits(int permissions) { this.permissions = permissions; } @Override public int getIntValue() { return permissions; } @Override public boolean getBitValue(int access, int type) { return (permissions & (type << (access*3))) != 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/IndividualPermissionBits.java ================================================ package com.mucommander.commons.file; /** * IndividualPermissionBits is a partial implementation of {@link com.mucommander.commons.file.PermissionBits} that relies * on {@link #getBitValue(int, int)}: the implementation of {@link #getIntValue()} calls getBitValue() * sequentially for each permission bit, 9 times in total. * * @see com.mucommander.commons.file.GroupedPermissionBits * @author Maxence Bernard */ public abstract class IndividualPermissionBits implements PermissionBits { public IndividualPermissionBits() { } @Override public int getIntValue() { int bitShift = 0; int perms = 0; for(int a=PermissionAccesses.OTHER_ACCESS; a<=PermissionAccesses.USER_ACCESS; a++) { for(int p=PermissionTypes.EXECUTE_PERMISSION; p<=PermissionTypes.READ_PERMISSION; p=p<<1) { if(getBitValue(a, p)) perms |= (1<

See the {@link SimpleFileAttributes} class for an implementation of this interface. * * @author Maxence Bernard * @see SimpleFileAttributes */ public interface MutableFileAttributes extends FileAttributes { /** * Sets the file's path. * *

The format and separator character of the path are filesystem-dependent. * * @param path the file's path */ void setPath(String path); /** * Sets whether the file exists physically on the underlying filesystem. * * @param exists true if the file exists physically on the underlying filesystem */ void setExists(boolean exists); /** * Sets the file's date in milliseconds since the epoch (00:00:00 GMT, January 1, 1970). * * @param date the file's date in milliseconds since the epoch (00:00:00 GMT, January 1, 1970) */ void setDate(long date); /** * Sets the file's size in bytes. * * @param size the file's size in bytes */ void setSize(long size); /** * Specifies whether the file is a directory or a regular file. * * @param directory true for directory, false for regular file */ void setDirectory(boolean directory); /** * Sets the file's permissions. * * @param permissions the file's permissions */ void setPermissions(FilePermissions permissions); /** * Sets the file's owner. * * @param owner the file's owner */ void setOwner(String owner); /** * Sets the file's group. * * @param group the file's owner */ void setGroup(String group); } ================================================ FILE: src/main/java/com/mucommander/commons/file/PathCanonizer.java ================================================ package com.mucommander.commons.file; /** * PathCanonizer is an interface that defines a single {@link #canonize(String)} method that returns the canonical * representation of a given path. This interface is used by {@link SchemeParser} implementations to perform * scheme-specific path canonization, independently of the actual URL parsing. * * @see DefaultSchemeParser * @author Maxence Bernard */ public interface PathCanonizer { /** * Returns a canonical representation of the given path. * * @param path path to canonize * @return a canonical representation of the given path. */ String canonize(String path); } ================================================ FILE: src/main/java/com/mucommander/commons/file/PermissionAccesses.java ================================================ package com.mucommander.commons.file; /** * This interface defines constants fields used for designating the three different permission accesses: * {@link #USER_ACCESS}, {@link #GROUP_ACCESS} and {@link #OTHER_ACCESS}. Their actual int values represent the number * of 3-bit left shifts (<< operator) needed to represent a particular * {@link com.mucommander.commons.file.PermissionTypes permission type} in a UNIX-style permission int. To illustrate, * the 'read' permission (value = 4) for the 'user' access (value = 2) is represented in a UNIX-style permission int as: * 4 << 3*2 = 256 (400 octal). * * @see com.mucommander.commons.file.PermissionTypes * @author Maxence Bernard */ public interface PermissionAccesses { /** Designates the 'other' permission access. */ int OTHER_ACCESS = 0; /** Designates the 'group' permission access. */ int GROUP_ACCESS = 1; /** Designates the 'user' permission access. */ int USER_ACCESS = 2; } ================================================ FILE: src/main/java/com/mucommander/commons/file/PermissionBits.java ================================================ package com.mucommander.commons.file; /** * This interface provides methods to access file permissions, for every combination of types and accesses defined * in {@link com.mucommander.commons.file.PermissionTypes} and {@link com.mucommander.commons.file.PermissionAccesses} respectively. * This interface also defines constants for commonly used permission values. * *

Permission bits can be queried individually using {@link #getBitValue(int, int)} or be represented altogether * as a UNIX-style permission int, using {@link #getIntValue()}. * *

Two implementations of this interface are provided: *

    *
  • {@link com.mucommander.commons.file.GroupedPermissionBits} (full implementation): implements this interface using a * given permission int.
  • *
  • {@link com.mucommander.commons.file.IndividualPermissionBits} (partial implementation): implements the * getIntValue() method by relying on getBitValue() and querying it sequentially for every * permission bit.
  • *
* * @see com.mucommander.commons.file.GroupedPermissionBits * @see com.mucommander.commons.file.IndividualPermissionBits * @author Maxence Bernard */ public interface PermissionBits { /** read/write/execute permissions set for user/group/other (777 octal) */ int FULL_PERMISSION_INT = 511; /** read/write/execute permissions set for user/group/other (777 octal) */ PermissionBits FULL_PERMISSION_BITS = new GroupedPermissionBits(FULL_PERMISSION_INT); /** read/write/execute permissions cleared for user/group/other (0) */ int EMPTY_PERMISSION_INT = 0; /** read/write/execute permissions cleared for user/group/other (0) */ PermissionBits EMPTY_PERMISSION_BITS = new GroupedPermissionBits(EMPTY_PERMISSION_INT); /** * Returns the value of all the permission bits (9 in total) in a UNIX-style permission int. Each of the permission * bits can be isolated by comparing them against the values defined in {@link com.mucommander.commons.file.PermissionTypes} * and {@link com.mucommander.commons.file.PermissionAccesses}. * * @return the value of all the permission bits (9 in total) in a UNIX-style permission int */ int getIntValue(); /** * Returns the value of a specific permission bit: true if the permission is set, false * if it isn't. * * @param access one of the values defined in {@link com.mucommander.commons.file.PermissionAccesses} * @param type one of the values defined in {@link com.mucommander.commons.file.PermissionTypes} * @return true if the permission is set, false if it isn't */ boolean getBitValue(int access, int type); } ================================================ FILE: src/main/java/com/mucommander/commons/file/PermissionTypes.java ================================================ package com.mucommander.commons.file; /** * This interface defines constants fields used for designating the three different permission types: * {@link #READ_PERMISSION}, {@link #WRITE_PERMISSION} and {@link #EXECUTE_PERMISSION}. Their actual value represent * the bit to be set and left-shifted with the desired {@link com.mucommander.commons.file.PermissionAccesses permission access} * in a UNIX-style permission int. * * @see PermissionAccesses * @author Maxence Bernard */ public interface PermissionTypes { /** Designates the 'execute' permission. */ int EXECUTE_PERMISSION = 1; /** Designates the 'write' permission. */ int WRITE_PERMISSION = 2; /** Designates the 'read' permission. */ int READ_PERMISSION = 4; } ================================================ FILE: src/main/java/com/mucommander/commons/file/ProtocolFile.java ================================================ package com.mucommander.commons.file; /** * Super class of all file protocol implementations (by opposition to {@link AbstractArchiveFile archive file} * implementations). * * @see ProtocolProvider * @author Maxence Bernard */ public abstract class ProtocolFile extends AbstractFile { protected ProtocolFile(FileURL url) { super(url); } /** * This implementation always returns false. * * @return false, always */ @Override public boolean isArchive() { return false; } /*@Override public boolean canGetReplication() { return false; } @Override public boolean canGetBlocksize() { return false; } @Override public short getReplication() { return 0; } @Override public long getBlocksize() { return 0; } @Override public void changeReplication(short replication) throws IOException { }*/ } ================================================ FILE: src/main/java/com/mucommander/commons/file/ProtocolProvider.java ================================================ package com.mucommander.commons.file; import java.io.IOException; /** * This interface allows {@link FileFactory} to instantiate {@link AbstractFile} implementations. *

* For {@link AbstractFile} implementations to be automatically instantiated by {@link FileFactory}, this interface * needs to be implemented and an instance registered with {@link FileFactory} and bound to a protocol identifier. * * @author Nicolas Rinaudo, Maxence Bernard * @see FileFactory * @see AbstractFile */ public interface ProtocolProvider { /** * Creates a new instance of AbstractFile that matches the specified URL. * @param url URL to map as an AbstractFile. * @param instantiationParams file implementation-specific parameters used for instantiating the * {@link AbstractFile} implementation. Those parameters are used when creating file instances within * the AbstractFile implementation. * @return a new instance of AbstractFile that matches the specified URL. * @throws IOException if an error occurs. */ AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/ROArchiveEntryFile.java ================================================ package com.mucommander.commons.file; import java.io.OutputStream; /** * Represents a file entry inside a read-only archive. Read-only archives are characterized by * {@link AbstractArchiveFile#isWritable()} returning false. * * @see AbstractArchiveFile * @see RWArchiveEntryFile * @author Maxence Bernard */ public class ROArchiveEntryFile extends AbstractArchiveEntryFile { protected ROArchiveEntryFile(FileURL url, AbstractArchiveFile archiveFile, ArchiveEntry entry) { super(url, archiveFile, entry); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void setLastModifiedDate(long lastModified) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } /** * Always return {@link PermissionBits#EMPTY_PERMISSION_BITS}. */ @Override public PermissionBits getChangeablePermissions() { return PermissionBits.EMPTY_PERMISSION_BITS; } /** * Always throws {@link UnsupportedFileOperationException} when called. */ @Override @UnsupportedFileOperation public void delete() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.DELETE); } /** * Always throws {@link UnsupportedFileOperationException} when called. */ @Override @UnsupportedFileOperation public void mkdir() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CREATE_DIRECTORY); } /** * Always throws {@link UnsupportedFileOperationException} when called. */ @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE); } /** * Always throws {@link UnsupportedFileOperationException} when called. */ @Override @UnsupportedFileOperation public void changePermissions(int permissions) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } /** * Always throws {@link UnsupportedFileOperationException} when called. */ @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } /** * Always returns 0. */ @Override public long getFreeSpace() { return 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/RWArchiveEntryFile.java ================================================ package com.mucommander.commons.file; import com.mucommander.commons.io.ByteCounter; import com.mucommander.commons.io.CounterOutputStream; import javax.swing.tree.DefaultMutableTreeNode; import java.io.IOException; import java.io.OutputStream; /** * Represents a file entry inside a read-write archive. Read-write archives are characterized by * {@link AbstractArchiveFile#isWritable()} returning false. * * @see AbstractArchiveFile * @see ROArchiveEntryFile * @author Maxence Bernard */ public class RWArchiveEntryFile extends AbstractArchiveEntryFile { RWArchiveEntryFile(FileURL url, AbstractArchiveFile archiveFile, ArchiveEntry entry) { super(url, archiveFile, entry); } /** * Updates this entry's attributes in the archive and returns true if the update went OK. * * @return true if the attributes were successfully updated in the archive. */ private boolean updateEntryAttributes() { try { ((AbstractRWArchiveFile)archiveFile).updateEntry(entry); return true; } catch(IOException e) { return false; } } /** * @throws IOException if the entry does not exist within the archive */ @Override public void setLastModifiedDate(long lastModified) throws IOException { if (!entry.exists()) { throw new IOException(); } long oldDate = entry.getLastModifiedDate(); entry.setDate(lastModified); boolean success = updateEntryAttributes(); if (!success) { // restore old date if attributes could not be updated entry.setDate(oldDate); throw new IOException(); } } /** * Always returns {@link PermissionBits#FULL_PERMISSION_BITS}. */ @Override public PermissionBits getChangeablePermissions() { // Todo: some writable archive implementations may not have full 'set' permissions support, or even no notion of permissions return PermissionBits.FULL_PERMISSION_BITS; } /** * Deletes this entry from the associated AbstractArchiveFile. *

* Throws a {@link UnsupportedFileOperationException} if the underlying file does not support the required * read and write {@link FileOperation file operations}. Throws an IOException in any of the following * cases: *

    *
  • if this entry does not exist in the archive
  • *
  • if this entry is a non-empty directory
  • *
  • if an I/O error occurred
  • *
* * @throws IOException in any of the cases listed above. */ @Override public void delete() throws IOException { if (!entry.exists()) { throw new IOException(); } AbstractRWArchiveFile rwArchiveFile = (AbstractRWArchiveFile)archiveFile; // Throw an IOException if this entry is a non-empty directory if (isDirectory()) { ArchiveEntryTree tree = rwArchiveFile.getArchiveEntryTree(); if (tree != null) { DefaultMutableTreeNode node = tree.findEntryNode(entry.getPath()); if (node != null && node.getChildCount() > 0) { throw new IOException(); } } } // Delete the entry in the archive file rwArchiveFile.deleteEntry(entry); // Non-existing entries are considered as zero-length regular files entry.setDirectory(false); entry.setSize(0); entry.setExists(false); } /** * Creates this entry as a directory in the associated AbstractArchiveFile. *

* Throws a {@link UnsupportedFileOperationException} if the underlying file does not support the required * read and write {@link FileOperation file operations}. Throws an IOException if this entry * already exists in the archive or if an I/O error occurred. * * @throws IOException if this entry already exists in the archive or if an I/O error occurred. */ @Override public void mkdir() throws IOException { if (entry.exists()) { throw new IOException(); } AbstractRWArchiveFile rwArchivefile = (AbstractRWArchiveFile)archiveFile; // Update the ArchiveEntry entry.setDirectory(true); entry.setDate(System.currentTimeMillis()); entry.setSize(0); // Add the entry to the archive file rwArchivefile.addEntry(entry); // The entry now exists entry.setExists(true); } /** * Returns an OutputStream that allows to write this entry's contents. *

* This method will create this entry as a regular file in the archive if it doesn't already exist, or replace * it if it already does. * * @throws IOException if an I/O error occurred */ @Override public OutputStream getOutputStream() throws IOException { if (entry.exists()) { try { delete(); } catch(IOException e) { // Go ahead and try to add the file anyway } } // Update the ArchiveEntry's size as data gets written to the OutputStream OutputStream out = new CounterOutputStream(((AbstractRWArchiveFile)archiveFile).addEntry(entry), new ByteCounter() { @Override public synchronized void add(long nbBytes) { entry.setSize(entry.getSize()+nbBytes); entry.setDate(System.currentTimeMillis()); } }); entry.setExists(true); return out; } @Override public void changePermissions(int permissions) throws IOException { if (!entry.exists()) { throw new IOException(); } FilePermissions oldPermissions = entry.getPermissions(); FilePermissions newPermissions = new SimpleFilePermissions(permissions, oldPermissions.getMask()); entry.setPermissions(newPermissions); boolean success = updateEntryAttributes(); if (!success) { // restore old permissions if attributes could not be updated entry.setPermissions(oldPermissions); } if (!success) { throw new IOException(); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/SchemeHandler.java ================================================ package com.mucommander.commons.file; /** * SchemeHandler is an interface that allows {@link FileURL} to be specialized for a particular scheme. * It provides a number of scheme-specific features: *

*
{@link #getStandardPort() standard port}
the standard port implied when no port is defined in the URL, * e.g. 21 for FTP
*
{@link #getPathSeparator() path separator}
the character(s) that separates path fragments, e.g. '/' for * most schemes, '\' for local paths under certain OSes like Windows.
*
{@link #getGuestCredentials() guest credentials}
credentials to authenticate as a guest, e.g. 'GUEST' * for SMB, 'anonymous' for FTP.
*
{@link #getRealm(FileURL) authentication realm}
the base URL throughout which a set of credentials can * be used.
*
*

* In addition to providing those attributes, a SchemeHandler provides a {@link SchemeParser} instance which takes care * of the actual parsing of URLs of a particular scheme when {@link FileURL#getFileURL(String)} is invoked. This allows * for scheme-specific parsing, like for example for the query part which should only be parsed and considered as a * separate part for certain schemes such as HTTP. * *

Handler registration

*

* FileURL registers a number of handlers for the schemes/protocols supported by the muCommander file API. * Additional handlers can be registered dynamically using {@link FileURL#registerHandler(String, SchemeHandler)}. * Likewise, existing handlers can be unregistered or replaced at runtime using * {@link FileURL#registerHandler(String, SchemeHandler)} and {@link FileURL#unregisterHandler(String)}.
*
* FileURL uses a default handler for schemes that do not have a specific handler registered. * * @see DefaultSchemeHandler * @see com.mucommander.commons.file.FileURL#registerHandler(String, SchemeHandler) * @see SchemeParser * @author Maxence Bernard */ public interface SchemeHandler { /** * Returns the SchemeParser that turns URL strings of a particular scheme into {@link FileURL} objects. * * @return the SchemeParser that turns URL strings of a particular scheme into {@link FileURL} objects */ SchemeParser getParser(); /** * Returns the authentication realm of the given location, i.e. the base location throughout which a set of * credentials can be used. Any property contained by the specified FileURL will be carried over in the returned * FileURL. On the contrary, credentials will not be copied, the returned URL always has no credentials. * *

This method returns a new FileURL instance every time it is called. Therefore, the returned URL can * safely be modified without any risk of side effects. * * @param location the location for which to return the authentication realm * @return the authentication realm of the specified url */ FileURL getRealm(FileURL location); /** * Returns the scheme's guest credentials, null if the scheme doesn't have any. *

* Guest credentials offer a way to authenticate a URL as a 'guest' on file protocols that require a set of * credentials to establish a connection. The returned credentials are provided with no guarantee that the filesystem * will actually accept them and allow the request/connection. The notion of 'guest' credentials may or may not * have a meaning depending on the underlying file protocol. * * @return the scheme's guest credentials, null if the scheme doesn't have any */ Credentials getGuestCredentials(); /** * Returns the type of authentication used by the scheme's file protocol. The returned value is one of the constants * defined in {@link AuthenticationType}. * * @return the type of authentication used by the scheme's file protocol */ AuthenticationType getAuthenticationType(); /** * Returns the scheme's path separator, which serves as a delimiter for path fragments. For most schemes, this is * the forward slash character. * * @return this scheme's path separator */ String getPathSeparator(); /** * Returns the scheme's standard port, -1 if the scheme doesn't have any. Some file protocols may not * have a notion of standard port or even no use for the port part at all, for example those that are not TCP * or UDP based such as the local 'file' scheme. * * @return the scheme's standard port */ int getStandardPort(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/SchemeParser.java ================================================ package com.mucommander.commons.file; import java.net.MalformedURLException; /** * SchemeParser is an interface that provides a single {@link #parse(String, FileURL)} method used by * {@link FileURL#getFileURL(String)} to turn a URL string into a corresponding FileURL instance. * * @see FileURL#getFileURL(String) * @see com.mucommander.commons.file.SchemeHandler * @author Maxence Bernard */ public interface SchemeParser { /** * Extracts the different parts from the given URL string and sets them in the specified FileURL instance. * The FileURL is empty when it is passed, with just the handler set. The scheme, host, port, login, password, path, * ... parts must all be set, using the corresponding setter methods. * *

Some parts such as the query and fragment have a meaning only for certain schemes such as HTTP, other schemes * may simply ignore the corresponding query/fragment delimiters ('?' and '#' resp.) and include them in the * path part. * * @param url the URL to parse * @param fileURL the FileURL instance in which to set the different parsed parts * @throws MalformedURLException if the specified string is not a valid URL and cannot be parsed */ void parse(String url, FileURL fileURL) throws MalformedURLException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/SevenZipArchiveFormatDetector.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.StreamUtils; import lombok.extern.slf4j.Slf4j; import net.sf.sevenzipjbinding.ArchiveFormat; import java.io.IOException; import java.io.PushbackInputStream; @Slf4j public abstract class SevenZipArchiveFormatDetector { private final int maxLen; public SevenZipArchiveFormatDetector(int maxLen) { this.maxLen = maxLen; } protected abstract ArchiveFormat detect(byte[] bytes); public ArchiveFormat detect(AbstractFile file) { byte[] bytes = readFirst(file); if (bytes == null) { return null; } ArchiveFormat format = detect(bytes); BufferPool.releaseByteArray(bytes); return format; } private byte[] readFirst(AbstractFile file) { byte[] bytes = BufferPool.getByteArray(maxLen); try { PushbackInputStream is = file.getPushBackInputStream(maxLen); int readBytes = StreamUtils.readUpTo(is, bytes); is.unread(bytes, 0, readBytes); } catch (IOException e) { log.error("Exception on file read", e); BufferPool.releaseByteArray(bytes); try { file.closePushbackInputStream(); } catch (IOException e1) { log.error("Exception on stream close", e1); } return null; } return bytes; } protected static boolean checkSignature(byte[] data, byte[] signature) { if (data.length < signature.length) { return false; } for (int i = 0; i < signature.length; i++) { if (data[i] != signature[i]) { return false; } } return true; } protected static boolean checkSignature(byte[] data, int[] signature) { if (data.length < signature.length) { return false; } for (int i = 0; i < signature.length; i++) { if ((data[i] & 0xff) != signature[i] && signature[i] != -1) { return false; } } return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/SimpleFileAttributes.java ================================================ package com.mucommander.commons.file; /** * This class is a simple implementation of the {@link com.mucommander.commons.file.FileAttributes} interface, where all * the attributes are stored as protected members of the class. * * @author Maxence Bernard */ public class SimpleFileAttributes implements MutableFileAttributes { /** Path attribute */ private String path; /** Exists attribute */ private boolean exists; /** Date attribute */ private long date; /** Size attribute */ private long size; /** Directory attribute */ private boolean directory; /** Permissions attribute */ private FilePermissions permissions; /** Owner attribute */ private String owner; /** Group attribute */ private String group; /** Replication attribute */ private short replication = 1; /** BlockSize attribute */ private long blockSize = 0; /** * Creates a new SimpleFileAttributes instance with unspecified/null attribute values. */ public SimpleFileAttributes() { } /** * Creates a new SimpleFileAttributes instance whose attributes are set to those of the given AbstractFile. * Note that the path attribute is set to the file's {@link com.mucommander.commons.file.AbstractFile#getAbsolutePath() absolute path}. * * @param file the file from which to fetch the attribute values */ public SimpleFileAttributes(AbstractFile file) { setPath(file.getAbsolutePath()); setExists(file.exists()); setDate(file.getLastModifiedDate()); setSize(file.getSize()); setDirectory(file.isDirectory()); setPermissions(file.getPermissions()); setOwner(file.getOwner()); setGroup(file.getGroup()); } @Override public String getPath() { return path; } @Override public void setPath(String path) { this.path = path; } @Override public boolean exists() { return exists; } @Override public void setExists(boolean exists) { this.exists = exists; } @Override public long getLastModifiedDate() { return date; } @Override public void setDate(long date) { this.date = date; } @Override public long getSize() { return size; } @Override public void setSize(long size) { this.size = size; } @Override public boolean isDirectory() { return directory; } @Override public void setDirectory(boolean directory) { this.directory = directory; } @Override public FilePermissions getPermissions() { return permissions; } @Override public void setPermissions(FilePermissions permissions) { this.permissions = permissions; } @Override public String getOwner() { return owner; } @Override public void setOwner(String owner) { this.owner = owner; } @Override public String getGroup() { return group; } @Override public void setGroup(String group) { this.group = group; } public short getReplication() { return replication; } public void setReplication(short replication) { this.replication = replication; } public long getBlockSize() { return blockSize; } public void setBlockSize(long blockSize) { this.blockSize = blockSize; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/SimpleFilePermissions.java ================================================ package com.mucommander.commons.file; /** * SimpleFilePermissions is a FilePermissions implementation that takes int values for the permission values and mask. * Additionally, this class defines padPermission static methods that allows to pad unsupported permission * bits with default values. * * @author Maxence Bernard */ public class SimpleFilePermissions extends GroupedPermissionBits implements FilePermissions { /** The permissions mask */ protected PermissionBits mask; /** * Creates a new SimpleFilePermissions using the specified UNIX-style permission int for permission values and * {@link #FULL_PERMISSION_BITS full permissions mask}. * * @param permissions a UNIX-style permission int that holds permission values. */ public SimpleFilePermissions(int permissions) { this(permissions, FULL_PERMISSION_BITS); } /** * Creates a new SimpleFilePermissions using the specified UNIX-style permission int values for permission values * and mask. * * @param permissions a UNIX-style permission int that holds permission values. * @param mask a UNIX-style permission int which defines which permission bits are supported. */ public SimpleFilePermissions(int permissions, int mask) { this(permissions, new GroupedPermissionBits(mask)); } /** * Creates a new SimpleFilePermissions using the specified UNIX-style permission int and permission mask. * * @param permissions a UNIX-style permission int that holds permission values. * @param mask a permission mask which defines which permission bits are supported. */ public SimpleFilePermissions(int permissions, PermissionBits mask) { super(permissions); this.mask = mask; } /** * Pads the given permissions with the specified ones: the permission bits that are not supported * (as reported by the supplied permissions mask) are replaced by those of the default permissions. * That means:
* - if the mask indicates that all permission bits are supported (mask = 777 octal), the supplied permissions will * simply be returned, without using any of the default permissions
* - if the mask indicates that none of the permission bits are supported (mask = 0), the default permissions will * be returned, without using any of the supplied permissions
* * @param permissions the permissions to pad with default permissions for the bits that are not supported * @param defaultPermissions permissions to use for the bits that are not supported * @return the permissions padded with the default permissions */ public static FilePermissions padPermissions(FilePermissions permissions, FilePermissions defaultPermissions) { int permissionMask = permissions.getMask().getIntValue(); return new SimpleFilePermissions(( permissions.getIntValue() & permissionMask) | (~permissionMask & defaultPermissions.getIntValue()), defaultPermissions.getMask()); } /** * Pads the given permissions with the specified ones: the permission bits that are not supported * (as reported by the supplied permissions mask) are replaced by those of the default permissions. * That means:
* - if the mask indicates that all permission bits are supported (mask = 777 octal), the supplied permissions will * simply be returned, without using any of the default permissions
* - if the mask indicates that none of the permission bits are supported (mask = 0), the default permissions will * be returned, without using any of the supplied permissions
* * @param permissions the permissions to pad with default permissions for the bits that are not supported * @param supportedPermissionsMask the bit mask that indicates which bits of the given permissions are supported * @param defaultPermissions permissions to use for the bits that are not supported * @return the given permissions padded with the default permissions */ public static int padPermissions(int permissions, int supportedPermissionsMask, int defaultPermissions) { return (permissions & supportedPermissionsMask) | (~supportedPermissionsMask & defaultPermissions); } @Override public PermissionBits getMask() { return mask; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/SingleArchiveEntryIterator.java ================================================ package com.mucommander.commons.file; /** * This class is an implementation of {@link ArchiveEntryIterator} that iterates through a single archive entry * specified at creation time. The entry passed to the constructor may be null -- the iterator will * act as an empty one. {@link #close()} is implemented as a no-op. * * @author Maxence Bernard */ public class SingleArchiveEntryIterator implements ArchiveEntryIterator { /** The single entry to iterate through */ protected ArchiveEntry entry; public SingleArchiveEntryIterator(ArchiveEntry entry) { this.entry = entry; } @Override public ArchiveEntry nextEntry() { if (entry == null) { return null; } ArchiveEntry nextEntry = entry; entry = null; return nextEntry; } /** * Implemented as a no-op (nothing to close). */ @Override public void close() { } } ================================================ FILE: src/main/java/com/mucommander/commons/file/SyncedFileAttributes.java ================================================ package com.mucommander.commons.file; /** * SyncedFileAttributes is a FileAttributes implementation which allows attribute values to be automatically * updated when accessed after a certain amount of time (the 'time to live') since their last update. The update * is performed by the abstract {@link #updateAttributes()} method and is triggered by any of the attribute getters. * A typical usage for this class is for remote file systems that need to keep file attributes in sync with a server, * {@link #updateAttributes()} can retrieve a fresh copy of the attributes on the server. * *

Attributes can also be manually updated using attribute setters. The {@link #updateExpirationDate()} method allows * to reset the expiration date and consider the attributes as 'fresh'. * *

An initial value for the attributes 'time to live' is specified in the constructor and can later be changed using * {@link #setTtl(long)}. If the 'time to live' is set to -1, attributes are no longer automatically updated, this class * then simply acts as {@link com.mucommander.commons.file.SimpleFileAttributes}. * * @author Maxence Bernard */ public abstract class SyncedFileAttributes extends SimpleFileAttributes { /** The attributes' 'time to live', negative values disable automatic attributes updates */ private long ttl; /** The attributes' expiration timestamp/date */ private long expirationDate; /** True when attributes are being updated */ private boolean isUpdating; /** * Creates a new SyncedFileAttributes using the specifies 'time to live' value. * * @param ttl the attributes' 'time to live', in milliseconds * @param updateAttributesNow if true, attributes are automatically updated */ public SyncedFileAttributes(long ttl, boolean updateAttributesNow) { setTtl(ttl); // also sets the expiration date if (updateAttributesNow) { checkForExpiration(true); // force attributes update } } /** * Returns the attributes' 'time to live', i.e. the amount of time since the last update after which attributes will * be automatically updated when any of the getter method is called. * * @return the attributes' 'time to live', in milliseconds */ public long getTtl() { return ttl; } /** * Sets the attributes' 'time to live', i.e. the amount of time since the last update after which attributes will * be automatically updated when any of the getter method is called. * Note that setting the 'time to live' causes the expiration date to be updated with {@link #updateExpirationDate()}. * * @param ttl the attributes' 'time to live', in milliseconds */ public void setTtl(long ttl) { this.ttl = ttl; // update the expiration date updateExpirationDate(); } /** * Returns the attributes' expiration timestamp/date, the date after which attributes values will be automatically * updated when any of the getter method is called. * * @return the attributes' expiration timestamp/date */ public long getExpirationDate() { return expirationDate; } /** * Sets the attributes' expiration timestamp/date, the date after which attributes values will be automatically * updated when they are accessed using any of the getter methods. * * @param expirationDate the attributes expiration timestamp/date */ public void setExpirationDate(long expirationDate) { this.expirationDate = expirationDate; } /** * Updates the attributes' expiration date to 'now' + 'ttl' (as returned by {@link #getTtl()}). * This method is called after attributes have been automatically updated. It can also be called after attribute * values have been manually updated using the setter methods. */ public void updateExpirationDate() { setExpirationDate(ttl < 0 ? Long.MAX_VALUE : System.currentTimeMillis() + getTtl()); } /** * Returns true if attributes have expired, i.e. the {@link #getExpirationDate()} expiration date has * passed, false if attributes are still 'fresh'. This method also returns false if * automatic attributes' update has been disabled ('time to live' set to a negative value), or if attributes are * currently being updated. * * @return true if attributes have expired */ public boolean hasExpired() { return ttl>=0 // prevents automatic updates if ttl is set to a negative value && !isUpdating() // causes getters to return the current value while attributes are being updated && System.currentTimeMillis()>expirationDate; } /** * Returns true if attributes are currently being updated. * * @return true if attributes are currently being updated */ private synchronized boolean isUpdating() { return isUpdating; } /** * Sets whether attributes are currently being updated. * * @param isUpdating true if attributes are currently being updated */ private synchronized void setUpdating(boolean isUpdating) { this.isUpdating = isUpdating; } /** * Checks if the attributes have expired and if they have, calls {@link #updateAttributes()} to refresh their * values. * * @param forceUpdate if true, attributes will systematically be updated, without checking the expiration date */ protected void checkForExpiration(boolean forceUpdate) { if (forceUpdate || hasExpired()) { // After this method is called, hasExpired() returns false so that implementations of updateAttributes() // can query attribute getters without entering a loop of death. setUpdating(true); // Updates attribute values updateAttributes(); // Update expiration date after the attribute have actually been updated, note that it may take a while // for remote file protocols to retrieve attributes. updateExpirationDate(); // OK we're done setUpdating(false); } } //////////////////////// // Overridden methods // //////////////////////// /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public String getPath() { checkForExpiration(false); return super.getPath(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public boolean exists() { checkForExpiration(false); return super.exists(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public long getLastModifiedDate() { checkForExpiration(false); return super.getLastModifiedDate(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public long getSize() { checkForExpiration(false); return super.getSize(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public boolean isDirectory() { checkForExpiration(false); return super.isDirectory(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public FilePermissions getPermissions() { checkForExpiration(false); return super.getPermissions(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public String getOwner() { checkForExpiration(false); return super.getOwner(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public String getGroup() { checkForExpiration(false); return super.getGroup(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public short getReplication() { checkForExpiration(false); return super.getReplication(); } /** * Overridden to trigger attributes update if the expiration date has been reached. */ @Override public long getBlockSize() { checkForExpiration(false); return super.getBlockSize(); } ////////////////////// // Abstract methods // ////////////////////// /** * Updates the attribute values. This method is automatically called when attributes are expired and one of the * attribute getters is called. The implementation may choose to update only certain attributes, or skip updates * under certain conditions. */ public abstract void updateAttributes(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/UnsupportedFileOperation.java ================================================ package com.mucommander.commons.file; import java.lang.annotation.*; /** * @author Maxence Bernard */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface UnsupportedFileOperation { } ================================================ FILE: src/main/java/com/mucommander/commons/file/UnsupportedFileOperationException.java ================================================ package com.mucommander.commons.file; import java.io.IOException; /** * This exception can be thrown by certain {@link AbstractFile} methods, when the corresponding operation * is not available, either because the underlying file protocol does not support it, or because it is not * implemented. This exception may also be thrown by file operations that depend on another file operation that is * not supported. *
* Unlike java.lang.UnsupportedOperationException, this exception is not a * RuntimeException and must therefore be caught explicitly. * *

* This exception is to be thrown in a way that is independent of the actual file instance, and of I/O or * network conditions: an AbstractFile method that throws this exception once must throw it * always, for any file instance. * * @author Maxence Bernard * @see UnsupportedFileOperation * @see AbstractFile */ public class UnsupportedFileOperationException extends IOException { /** The {@link FileOperation} this exception refers to */ private final FileOperation op; /** * Creates a new UnsupportedFileOperationException corresponding to the specified {@link FileOperation}. * * @param op the {@link FileOperation} this exception refers to. */ public UnsupportedFileOperationException(FileOperation op) { super(); this.op = op; } /** * Creates a new UnsupportedFileOperationException corresponding to the specified {@link FileOperation} * with a custom message. * * @param op the {@link FileOperation} this exception refers to. * @param message a message describing the exception cause. */ public UnsupportedFileOperationException(FileOperation op, String message) { super(message); this.op = op; } /** * Returns the {@link FileOperation} this exception refers to. * * @return the {@link FileOperation} this exception refers to. */ public FileOperation getFileOperation() { return op; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/WrapperArchiveEntryIterator.java ================================================ package com.mucommander.commons.file; import java.io.IOException; import java.util.Iterator; /** * This class wraps a java.util.Iterator and implements ArchiveEntryIterator by * delegating methods to their java.util.Iterator equivalent. {@link #close()} is implemented as a no-op. * * @author Maxence Bernard */ public class WrapperArchiveEntryIterator implements ArchiveEntryIterator { /** Wrapped iterator */ protected Iterator iterator; /** * Creates a new WrapperArchiveEntryIterator that iterates through the given * java.util.Iterator's elements. * * @param iterator the wrapped iterator */ public WrapperArchiveEntryIterator(Iterator iterator) { this.iterator = iterator; } @Override public ArchiveEntry nextEntry() { return iterator.hasNext() ? iterator.next() : null; } /** * Implemented as a no-op (nothing to close). */ @Override public void close() throws IOException { } } ================================================ FILE: src/main/java/com/mucommander/commons/file/archiver/ArchiveFormat.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.archiver; /** * @author Oleg Trifonov * Created on 15/03/17. */ public enum ArchiveFormat { ZIP("Zip", "zip", true), GZ("Gzip", "gz", false), BZ2("Bzip2", "bz2", false), TAR("Tar", "tar", true), TAR_GZ("Tar/Gzip", "tar.gz", true), TAR_BZ2("Tar/Bzip2", "tar.bz2", true); // ISO("ISO", "iso", true); /** * The name of the given archive format, can be used for display in a GUI. */ public final String name; /** * The default archive format extension. Note: some formats such as Tar/Gzip have several common * extensions (e.g. tar.gz or tgz), the most common one will be returned */ public final String ext; /** * true if the format used by this Archiver can store more than one entry */ public final boolean supportManyEntries; ArchiveFormat(String name, String ext, boolean supportManyEntries) { this.name = name; this.ext = ext; this.supportManyEntries = supportManyEntries; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/archiver/Archiver.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.archiver; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileAttributes; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.io.BufferedRandomOutputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import org.apache.hadoop.io.compress.bzip2.CBZip2OutputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.GZIPOutputStream; /** * Archiver is an abstract class that represents a generic file archiver and abstracts the underlying * compression method and specifics of the format. * *

Subclasses implement specific archive formats (Zip, Tar...) but cannot be instantiated directly. * Instead, the getArchiver methods can be used to retrieve an Archiver * instance for a specified archive format. A list of available archive formats can be dynamically retrieved * using {@link #getFormats(boolean) getFormats}. * *

Archive formats fall into 2 categories: *

    *
  • Single entry formats: Formats that can only store one entry without any directory structure, e.g. Gzip or Bzip2. *
  • Many entries formats: Formats that can store multiple entries along with a directory structure, e.g. Zip or Tar. *
* * @author Maxence Bernard */ public abstract class Archiver { /** The underlying stream this archiver is writing to */ protected OutputStream out; /** Archive format of this Archiver */ protected ArchiveFormat format; /** Support output stream for archiving files */ boolean supportStream; /** * Creates a new Archiver. * * @param out the OutputStream this Archiver will write to */ Archiver(OutputStream out) { this.out = out; this.supportStream = true; } /** * Returns the OutputStream this Archiver is writing to. * * @return the OutputStream this Archiver is writing to */ public OutputStream getOutputStream() { return out; } /** * Returns the archiver format used by this Archiver. See format constants. * @return archiver format code */ public ArchiveFormat getFormat() { return this.format; } /** * Sets the archiver format used by this Archiver, for internal use only. */ private void setFormat(ArchiveFormat format) { this.format = format; } /** * Checks if the format used by this Archiver can store an optional comment. * @return true if the format used by this Archiver can store an optional comment. */ public boolean supportsComment() { return formatSupportsComment(this.format); } /** * @return true if the archiver supports writing with streams */ public boolean supportsStream() { return supportStream; } /** * Sets an optional comment in the archive, the {@link #supportsComment()} or * {@link #formatSupportsComment(ArchiveFormat)} must first be called to make sure * the archive format supports comment, otherwise calling this method will have no effect. * *

Implementation note: Archiver implementations must override this method to handle comments * * @param comment the comment to be stored in the archive */ public void setComment(String comment) { // No-op } /** * Normalizes the entry path, that is : *

    *
  • replace any \ character occurrence by / as this usually is the default separator for archive files *
  • if the entry is a directory, add a trailing slash to the path if it doesn't have one already *
* * @param entryPath * @param isDirectory * * @return normalized path */ String normalizePath(String entryPath, boolean isDirectory) { // Replace any \ character by / entryPath = entryPath.replace('\\', '/'); // If entry is a directory, make sure the path contains a trailing / if (isDirectory && !entryPath.endsWith("/")) entryPath += "/"; return entryPath; } //////////////////// // Static methods // //////////////////// /** * Returns an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to. * null is returned if the specified format is not valid. * *

This method will first attempt to get a {@link RandomAccessOutputStream} if the given file is able to supply * one, and if not, fall back to a regular OutputStream. Note that if the file exists, its contents * will be overwritten. Write bufferring is used under the hood to improve performance. * * @param file the AbstractFile which the returned Archiver will write entries to * @param format an archive format * @return an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to ; * null if the specified format is not valid. * @throws IOException if the file cannot be opened for write, or if an error occurred while intializing the archiver * @throws UnsupportedFileOperationException if the underlying filesystem does not support write operations */ public static Archiver getArchiver(AbstractFile file, ArchiveFormat format) throws IOException, UnsupportedFileOperationException { // switch(format) { // case ISO: // return new ISOArchiver(file); // } OutputStream out = null; if (file.isFileOperationSupported(FileOperation.RANDOM_WRITE_FILE)) { try { // Important: if the file exists, it has to be overwritten as AbstractFile#getRandomAccessOutputStream() // does NOT overwrite the file. This fixes bug #30. if (file.exists()) { file.delete(); } out = new BufferedRandomOutputStream(file.getRandomAccessOutputStream()); } catch (IOException e) { // Fall back to a regular OutputStream } } if (out == null) { out = new BufferedOutputStream(file.getOutputStream()); } return getArchiver(out, format); } /** * Returns an Archiver for the specified format and that uses the given OutputStream to write entries to. * null is returned if the specified format is not valid. Whenever possible, a * {@link RandomAccessOutputStream} should be supplied as some formats take advantage of having a random write access. * * @param out the OutputStream which the returned Archiver will write entries to * @param format an archive format * @return an Archiver for the specified format and that uses the given {@link AbstractFile} to write entries to ; * null if the specified format is not valid. * @throws IOException if an error occurred while initializing the archiver */ private static Archiver getArchiver(OutputStream out, ArchiveFormat format) throws IOException { Archiver archiver; switch (format) { case ZIP: archiver = new ZipArchiver(out); break; case GZ: archiver = new SingleFileArchiver(new GZIPOutputStream(out)); break; case BZ2: archiver = new SingleFileArchiver(createBzip2OutputStream(out)); break; case TAR: archiver = new TarArchiver(out); break; case TAR_GZ: archiver = new TarArchiver(new GZIPOutputStream(out)); break; case TAR_BZ2: archiver = new TarArchiver(createBzip2OutputStream(out)); break; // case ISO: // throw new IllegalStateException("ISO archiving not supported by stream"); default: return null; } archiver.setFormat(format); return archiver; } /** * Creates and returns a Bzip2 OutputStream using the given OutputStream as the underlying * stream. * * @param out the underlying stream * @return a Bzip2 OutputStream * @throws IOException if an error occurred while initializing the Bzip2 OutputStream */ private static OutputStream createBzip2OutputStream(OutputStream out) throws IOException { // Writes the 2 magic bytes 'BZ', as required by CBZip2OutputStream. A quote from CBZip2OutputStream's Javadoc: // "Attention: The caller is responsible to write the two BZip2 magic bytes "BZ" to the specified stream // prior to calling this constructor." out.write('B'); out.write('Z'); return new CBZip2OutputStream(out); } /** * Returns an array of available archive formats, single entry formats or many entries formats * depending on the value of the specified boolean parameter. * * @param manyEntries if true, a list of many entries formats (a subset of single entry formats) will be returned * @return an array of available archive formats */ public static ArchiveFormat[] getFormats(boolean manyEntries) { if (!manyEntries) { return ArchiveFormat.values(); } int cnt = 0; for (ArchiveFormat af : ArchiveFormat.values()) { if (af.supportManyEntries) { cnt++; } } ArchiveFormat[] result = new ArchiveFormat[cnt]; int i = 0; for (ArchiveFormat af : ArchiveFormat.values()) { if (af.supportManyEntries) { result[i++] = af; } } return result; } /** * Returns true if the specified archive format can store an optional comment. * * @param format an archive format * @return true if the specified archive format can store an optional comment */ public static boolean formatSupportsComment(ArchiveFormat format) { return format == ArchiveFormat.ZIP; } ////////////////////// // Abstract methods // ////////////////////// /** * Creates a new entry in the archive using the given relative path and file attributes, and returns an * OutputStream to write the entry's contents. The specified file attributes are used to determine * whether the entry is a directory or a regular file, and to set the entry's size, permissions and date. * *

If the entry is a regular file (not a directory), an OutputStream which can be used to write the contents * of the entry will be returned, null otherwise. The OutputStream must not be closed once * it has been used (Archiver takes care of this), only the {@link #close() close} method has to be called when * all entries have been created. * *

If this Archiver uses a single entry format, the specified path and file won't be used at all. * Also in this case, this method must be invoked only once (single entry), it will throw an IOException * if invoked more than once. * * @param entryPath the path to be used to create the entry in the archive. This parameter is simply ignored if the * archive is a single entry format. * @param attributes used to determine whether the entry is a directory or regular file, and to retrieve its * date and size * @return OutputStream to write the entry's contents. * @throws IOException if this Archiver failed to write the entry, or in the case of a single entry archiver, if * this method was called more than once. */ public abstract OutputStream createEntry(String entryPath, FileAttributes attributes) throws IOException; /** * @return Name of current file being processed */ public String getProcessingFile() { return null; } /** * Written bytes in total without the current file progress * @return number of bytes written as a long */ public long totalWrittenBytes() { return -1; } /** * Written bytes to the current file being processed, will be the same size as the * file if complete. * @return number of bytes written as a long */ public long writtenBytesCurrentFile() { return -1; } /** * @return Size of the current file being processed in bytes */ public long currentFileLength() { return -1; } /** * Finish the archiving process when all files have been added. */ public abstract void postProcess() throws IOException; /** * Closes the underlying OuputStream and ressources used by this Archiver to write the archive. This method * must be called when all entries have been added to the archive. */ public abstract void close() throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/archiver/ISOArchiver.java ================================================ package com.mucommander.commons.file.archiver; import com.github.stephenc.javaisotools.eltorito.impl.ElToritoConfig; import com.github.stephenc.javaisotools.iso9660.ConfigException; import com.github.stephenc.javaisotools.iso9660.ISO9660Directory; import com.github.stephenc.javaisotools.iso9660.ISO9660File; import com.github.stephenc.javaisotools.iso9660.ISO9660RootDirectory; import com.github.stephenc.javaisotools.iso9660.impl.ISO9660Config; import com.github.stephenc.javaisotools.iso9660.impl.ISOImageFileHandler; import com.github.stephenc.javaisotools.joliet.impl.JolietConfig; import com.github.stephenc.javaisotools.rockridge.impl.RockRidgeConfig; import com.github.stephenc.javaisotools.sabre.HandlerException; import com.github.stephenc.javaisotools.sabre.StreamHandler; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileAttributes; import com.mucommander.commons.file.impl.iso.MuCreateISO; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.util.logging.Level; import java.util.logging.Logger; /** * Archiver implementation using the ISO9660 archive format. * * @author Jeppe Vennekilde */ public class ISOArchiver extends Archiver { private StreamHandler streamHandler; private final ISO9660Config config; private final ISO9660RootDirectory root; //Adds support for longer file names & wider range of characters private final boolean enableJoliet = true; //Adds support for deeper directory hierarchies and even bigger file names (up to 255 bytes) private final boolean enableRockRidge = true; //Adds support for creation of bootable iso files (not implemented) private final boolean enableElTorito = false; private MuCreateISO createISOProcess = null; ISOArchiver(AbstractFile file) { super(null); supportStream = false; config = new ISO9660Config(); try { config.allowASCII(false); config.setInterchangeLevel(1); //The rock ridge extension of ISO9660 allow directory depth to exceed 8 config.restrictDirDepthTo8(!enableRockRidge); config.setPublisher(System.getProperty("user.name")); //Max length of volume is 32 chars config.setVolumeID(file.getName().substring(0, Math.min(file.getName().length(), 31))); config.setDataPreparer(System.getProperty("user.name")); config.forceDotDelimiter(true); } catch (ConfigException ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } root = new ISO9660RootDirectory(); try { streamHandler = new ISOImageFileHandler(new File(file.getPath())); } catch (FileNotFoundException ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } } @Override public OutputStream createEntry(String entryPath, FileAttributes attributes) { try { if (attributes.isDirectory()) { String[] split = entryPath.split("\\\\"); ISO9660Directory dir = new ISO9660Directory(split[split.length-1]); ISO9660Directory parent = getParentDirectory(entryPath); if (parent != null) { parent.addDirectory(dir); } } else { try { ISO9660File file = new ISO9660File(new File(attributes.getPath())); ISO9660Directory parent = getParentDirectory(entryPath); if (parent != null) { parent.addFile(file); } } catch (HandlerException ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } } } catch (RuntimeException e) { e.printStackTrace(); throw e; } return null; // TODO !!!! NPE here !!! } /** * Get the ISO9660Directory parent object the path belongs to * * @param isoPath the sub directory/file of the parent directory it will return * @return an ISO9660Directory that is the parent of the provided path */ private ISO9660Directory getParentDirectory(String isoPath){ String[] directories = isoPath.split("\\\\"); //Initial directory (root) ISO9660Directory parent = root; for (int i = 0; i < directories.length - 1; i++){ ISO9660Directory dir = containsDirectory(parent,directories[i]); if (dir == null) { return null; } parent = dir; } return parent; } /** * Check if an ISO9660Directory contain a provided sub directory * * @param parentDirectory the directory that will be searched * @param isoSubDirPath the ISO path that will be used for reference to see * if the parent directory contains the sub directory * @return an ISO9660Directory that is sub directory of the parent directory * null if it does not contain the sub directory */ private ISO9660Directory containsDirectory(ISO9660Directory parentDirectory, String isoSubDirPath){ for (ISO9660Directory directory : parentDirectory.getDirectories()) { if (directory.getName().equals(isoSubDirPath)){ return directory; } } return null; } @Override public String getProcessingFile() { return createISOProcess != null ? createISOProcess.getProcessingFile() : null; } @Override public long totalWrittenBytes(){ return createISOProcess != null ? createISOProcess.totalWrittenBytes(): 0; } @Override public long writtenBytesCurrentFile(){ return createISOProcess != null ? createISOProcess.writtenBytesCurrentFile(): 0; } @Override public long currentFileLength(){ return createISOProcess != null ? createISOProcess.currentFileLength(): 0; } @Override public void postProcess() throws IOException { if (root.hasSubDirs() || !root.getFiles().isEmpty()) { createISOProcess = new MuCreateISO(streamHandler, root); RockRidgeConfig rrConfig = null; if (enableRockRidge) { // Rock Ridge support rrConfig = new RockRidgeConfig(); rrConfig.setMkisofsCompatibility(false); rrConfig.hideMovedDirectoriesStore(true); rrConfig.forcePortableFilenameCharacterSet(true); } JolietConfig jolietConfig = null; if (enableJoliet) { // Joliet support jolietConfig = new JolietConfig(); try { if (config.getPublisher() instanceof String){ jolietConfig.setPublisher((String) config.getPublisher()); } else { try { jolietConfig.setPublisher((File) config.getPublisher()); } catch (HandlerException ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } } //Max volume id is 16 in the joliet config jolietConfig.setVolumeID(config.getVolumeID().substring(0,Math.min(config.getVolumeID().length(), 15))); if (config.getDataPreparer() != null){ if(config.getDataPreparer() instanceof String){ jolietConfig.setDataPreparer((String) config.getDataPreparer()); } else { try { jolietConfig.setDataPreparer((File) config.getDataPreparer()); } catch (Exception ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } } } jolietConfig.forceDotDelimiter(true); } catch (ConfigException ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } } //ELTorito adds support for bootable ISO files, which is not supported at this time //As this is for archiving, not creation of bootable ISO files (yet) ElToritoConfig elToritoConfig = null; try { createISOProcess.process(config, rrConfig, jolietConfig, elToritoConfig); } catch (HandlerException ex) { Logger.getLogger(ISOArchiver.class.getName()).log(Level.SEVERE, null, ex); } } } @Override public void close() throws IOException { } } ================================================ FILE: src/main/java/com/mucommander/commons/file/archiver/SingleFileArchiver.java ================================================ package com.mucommander.commons.file.archiver; import com.mucommander.commons.file.FileAttributes; import java.io.IOException; import java.io.OutputStream; /** * Generic single file Archiver. * * @author Maxence Bernard */ class SingleFileArchiver extends Archiver { private boolean firstEntry = true; SingleFileArchiver(OutputStream outputStream) { super(outputStream); } /** * This method is a no-op, and does nothing but throw an IOException if it is called more than once, * which should never be the case as this Archiver is only meant to store one file. */ @Override public OutputStream createEntry(String entryPath, FileAttributes attributes) throws IOException { if (firstEntry) { firstEntry = false; } else { throw new IOException(); } return out; } @Override public void close() throws IOException { out.close(); } @Override public void postProcess() {} } ================================================ FILE: src/main/java/com/mucommander/commons/file/archiver/TarArchiver.java ================================================ package com.mucommander.commons.file.archiver; import com.mucommander.commons.file.FileAttributes; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.SimpleFilePermissions; import com.mucommander.commons.file.impl.tar.provider.TarEntry; import com.mucommander.commons.file.impl.tar.provider.TarOutputStream; import java.io.IOException; import java.io.OutputStream; /** * Archiver implementation using the Tar archive format. * * @author Maxence Bernard */ class TarArchiver extends Archiver { private final TarOutputStream tos; private boolean firstEntry = true; TarArchiver(OutputStream outputStream) { super(outputStream); this.tos = new TarOutputStream(outputStream); // Specifies how to handle files which filename is > 100 chars (default is to fail!) this.tos.setLongFileMode(TarOutputStream.LONGFILE_GNU); } @Override public OutputStream createEntry(String entryPath, FileAttributes attributes) throws IOException { // Start by closing current entry if (!firstEntry) { tos.closeEntry(); } boolean isDirectory = attributes.isDirectory(); // create the entry TarEntry entry = new TarEntry(normalizePath(entryPath, isDirectory)); // Use provided file's size (required by TarOutputStream) and date long size = attributes.getSize(); if (!isDirectory && size >= 0) // Do not set size if file is directory or file size is unknown! entry.setSize(size); // Set the entry's date and permissions entry.setModTime(attributes.getLastModifiedDate()); entry.setMode(SimpleFilePermissions.padPermissions(attributes.getPermissions(), isDirectory ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS).getIntValue()); // Add the entry tos.putNextEntry(entry); if (firstEntry) { firstEntry = false; } // Return the OutputStream that allows to write to the entry, only if it isn't a directory return isDirectory ? null : tos; } @Override public void close() throws IOException { // Close current entry if (!firstEntry) { tos.closeEntry(); } tos.close(); } @Override public void postProcess() {} } ================================================ FILE: src/main/java/com/mucommander/commons/file/archiver/ZipArchiver.java ================================================ package com.mucommander.commons.file.archiver; import com.mucommander.commons.file.FileAttributes; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.SimpleFilePermissions; import com.mucommander.commons.file.impl.zip.provider.ZipEntry; import com.mucommander.commons.file.impl.zip.provider.ZipOutputStream; import java.io.IOException; import java.io.OutputStream; /** * Archiver implementation using the Zip archive format. * * @author Maxence Bernard */ class ZipArchiver extends Archiver { private final ZipOutputStream zos; private boolean firstEntry = true; ZipArchiver(OutputStream outputStream) { super(outputStream); this.zos = new ZipOutputStream(outputStream); } /** * Overrides Archiver's no-op setComment method as Zip supports archive comment. */ @Override public void setComment(String comment) { zos.setComment(comment); } @Override public OutputStream createEntry(String entryPath, FileAttributes attributes) throws IOException { // Start by closing current entry if (!firstEntry) { zos.closeEntry(); } boolean isDirectory = attributes.isDirectory(); // Create the entry and use the provided file's date ZipEntry entry = new ZipEntry(normalizePath(entryPath, isDirectory)); // Use provided file's size and date long size = attributes.getSize(); if (!isDirectory && size >= 0) { // Do not set size if file is directory or file size is unknown! entry.setSize(size); } entry.setTime(attributes.getLastModifiedDate()); entry.setUnixMode(SimpleFilePermissions.padPermissions(attributes.getPermissions(), isDirectory ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS).getIntValue()); // Add the entry zos.putNextEntry(entry); if (firstEntry) { firstEntry = false; } // Return the OutputStream that allows to write to the entry, only if it isn't a directory return isDirectory ? null : zos; } @Override public void close() throws IOException { zos.close(); } @Override public void postProcess() {} } ================================================ FILE: src/main/java/com/mucommander/commons/file/compat/CompatURLConnection.java ================================================ package com.mucommander.commons.file.compat; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; /** * @author Maxence Bernard */ class CompatURLConnection extends URLConnection { protected AbstractFile file; public CompatURLConnection(URL url) { super(url); // Not connected yet } public CompatURLConnection(URL url, AbstractFile file) { super(url); if(file!=null) { this.file = file; connected = true; } } /** * Checks if this URLConnection is connected and if it isn't, calls {@link #connect()} to connect it. * * @throws IOException if an error occurred while connecting this URLConnection */ private void checkConnected() throws IOException { if(!connected) connect(); } /** * Creates the {@link AbstractFile} instance corresponding to the URL location, only if no AbstractFile * has been specified when this CompatURLConnection was created. * * @throws IOException if an error occurred while instanciating the AbstractFile */ @Override public void connect() throws IOException { if (!connected) { file = FileFactory.getFile(url.toString(), true); connected = true; } } @Override public InputStream getInputStream() throws IOException { checkConnected(); return file.getInputStream(); } @Override public OutputStream getOutputStream() throws IOException { checkConnected(); return file.getOutputStream(); } @Override public long getLastModified() { try { checkConnected(); return file.getLastModifiedDate(); } catch(IOException e) { return 0; } } @Override public long getDate() { return getLastModified(); } @Override public int getContentLength() { try { checkConnected(); return (int)file.getSize(); } catch(IOException e) { return -1; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/compat/CompatURLStreamHandler.java ================================================ package com.mucommander.commons.file.compat; import com.mucommander.commons.file.AbstractFile; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; /** * @author Maxence Bernard */ public class CompatURLStreamHandler extends URLStreamHandler { protected AbstractFile file; public CompatURLStreamHandler() { } public CompatURLStreamHandler(AbstractFile file) { this.file = file; } @Override protected URLConnection openConnection(URL url) throws IOException { return new CompatURLConnection(url, file); // Note: file may be null } } ================================================ FILE: src/main/java/com/mucommander/commons/file/connection/ConnectionHandler.java ================================================ package com.mucommander.commons.file.connection; import com.mucommander.commons.file.AuthException; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; /** * ConnectionHandler is a an abstract class that provides the basic operations for to interact with a server: establish * the connection, keep it alive and close it. * * @see com.mucommander.commons.file.connection.ConnectionPool * @author Maxence Bernard */ public abstract class ConnectionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionHandler.class); /** URL of the server this ConnectionHandler connects to */ protected FileURL realm; /** Credentials that are used to connect to the server */ protected Credentials credentials; /** True if this ConnectionHandler is currently locked */ protected boolean isLocked; /** Time at which the connection managed by this ConnectionHandler was last used */ private long lastActivityTimestamp; /** Time at which the connection managed by this ConnectionHandler was last kept alive */ private long lastKeepAliveTimestamp; /** Number of seconds of inactivity after which this ConnectionHandler's connection will be closed by ConnectionPool */ private long closeOnInactivityPeriod = DEFAULT_CLOSE_ON_INACTIVITY_PERIOD; /** Number of seconds of inactivity after which this ConnectionHandler's connection will be kept alive by ConnectionPool */ private long keepAlivePeriod = DEFAULT_KEEP_ALIVE_PERIOD; /** Default 'close on inactivity' period */ private final static long DEFAULT_CLOSE_ON_INACTIVITY_PERIOD = 300; /** Default keep alive period (-1, keep alive disabled) */ private final static long DEFAULT_KEEP_ALIVE_PERIOD = -1; /** * Creates a new ConnectionHandler for the given server URL using the Credentials included in the URL (potentially * null). * * @param serverURL URL of the server to connect to */ public ConnectionHandler(FileURL serverURL) { realm = serverURL.getRealm(); this.credentials = serverURL.getCredentials(); } /** * Returns the URL of the server this ConnectionHandler connects to. * * @return the URL of the server this ConnectionHandler connects to */ public FileURL getRealm() { return realm; } /** * Returns the Credentials that are used to connect to the server, null if no credentials are used. * * @return the Credentials that are used to connect to the server, null if no credentials are used */ public Credentials getCredentials() { return credentials; } /** * Checks if the connection is currently active (as returned by {@link #isConnected()} and if it isn't, starts it * by calling {@link #startConnection()}. Returns true if the connection was properly started, false if the * connection was already active, or throws an IOException if the connection could not be started. * * @return Returns true if the connection was properly started, false if the connection was already active * @throws IOException if the connection could not be started */ public boolean checkConnection() throws IOException { if (!isConnected()) { LOGGER.info("not connected, starting connection, this="+this); startConnection(); return true; } return false; } /** * Tries to lock this ConnectionHandler and returns true if it could be locked, false if it is already locked. * * @return true if it could be locked, false if it is already locked. */ synchronized boolean acquireLock() { if (isLocked) { LOGGER.info("!!!!! acquireLock() returning false, should not happen !!!!!", new Throwable()); return false; } isLocked = true; return true; } /** * Tries to release the lock on this ConnectionHandler and returns true if it could be locked, false if it * is not locked. * * @return true if it could be locked, false if it is not locked */ public boolean releaseLock() { synchronized(this) { if (!isLocked) { LOGGER.info("!!!!! releaseLock() returning false, should not happen !!!!!", new Throwable()); return false; } isLocked = false; } ConnectionPool.notifyConnectionHandlerLockReleased(); return true; } /** * Returns true if this ConnectionHandler is currently locked. * * @return true if this ConnectionHandler is currently locked */ public synchronized boolean isLocked() { return isLocked; } /** * Updates the time at which the connection managed by this ConnectionHandler was last used to now (current time). */ void updateLastActivityTimestamp() { lastActivityTimestamp = System.currentTimeMillis(); } /** * Returns the time at which the connection managed by this ConnectionHandler was last used. * * @return the time at which the connection managed by this ConnectionHandler was last used */ long getLastActivityTimestamp() { return lastActivityTimestamp; } /** * Updates the time at which the connection managed by this ConnectionHandler was last kept alive to now (current time). */ void updateLastKeepAliveTimestamp() { lastKeepAliveTimestamp = System.currentTimeMillis(); } /** * Returns the time at which the connection managed by this ConnectionHandler was last kept alive. * * @return the time at which the connection managed by this ConnectionHandler was last kept alive */ long getLastKeepAliveTimestamp() { return lastKeepAliveTimestamp; } /** * Returns the number of seconds of inactivity after which {@link ConnectionPool} will close the connection by * calling {@link #closeConnection()}, -1 to indicate that the connection should not be automatically * closed. * *

By default, this value is 300 seconds (5 minutes). * * @return the number of seconds of inactivity after which {@link ConnectionPool} will close the connection, * -1 to indicate that the connection should not be automatically closed */ long getCloseOnInactivityPeriod() { return closeOnInactivityPeriod; } /** * Sets the number of seconds of inactivity after which {@link ConnectionPool} will close the connection by calling * {@link #closeConnection()}, -1 to prevent the connection from being automatically closed. * *

By default, this value is 300 seconds (5 minutes). * * @param nbSeconds the number of seconds of inactivity after which {@link ConnectionPool} will close the connection, * -1 to indicate that the connection should not be automatically closed */ public void setCloseOnInactivityPeriod(long nbSeconds) { closeOnInactivityPeriod = nbSeconds; } /** * Returns the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection alive * by calling {@link #keepAlive()}, -1 to indicate that this connection should not be kept alive. * *

By default, this value is -1 (keep alive disabled). * * @return the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection alive * by calling {@link #keepAlive()}, -1 to indicate that this connection should not be kept alive */ long getKeepAlivePeriod() { return keepAlivePeriod; } /** * Returns the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection alive * by calling {@link #keepAlive()}, -1 to indicate that this connection should not be kept alive. * *

By default, this value is -1 (keep alive disabled). * * @param nbSeconds the number of seconds of inactivity after which {@link ConnectionPool} will keep the connection * alive by calling {@link #keepAlive()}, -1 to indicate that this connection should not be kept alive */ protected void setKeepAlivePeriod(long nbSeconds) { keepAlivePeriod = nbSeconds; } /** * Returns true if the given Object is a ConnectionHandler whose realm and credentials are equal to * those of this ConnectionHandler. The credentials comparison is password-sensitive. * * @param o the Object to compare for equality * @see Credentials#equals(Object, boolean) */ public boolean equals(Object o) { if (!(o instanceof ConnectionHandler)) { return false; } ConnectionHandler connHandler = (ConnectionHandler)o; return equals(connHandler.realm, connHandler.credentials); } /** * Returns true if both the given realm and credentials are equal to those of this ConnectionHandler. * The credentials comparison is password-sensitive. * * @param realm the FileURL to compare against this ConnectionHandler's * @param credentials the Credentials to compare against this ConnectionHandler's * @return true if both the given realm and credentials are equal to those of this ConnectionHandler * @see Credentials#equals(Object, boolean) */ public boolean equals(FileURL realm, Credentials credentials) { if (!this.realm.equals(realm, false, true)) { return false; } // Compare credentials. One or both Credentials instances may be null. // Note: Credentials.equals() considers null as equal to empty Credentials (see Credentials#isEmpty()) return (this.credentials == null && credentials == null) || (this.credentials != null && this.credentials.equals(credentials, true)) || (credentials != null && credentials.equals(this.credentials, true)); } /** * Throws an {@link AuthException} using this connection handler's realm, credentials and the message passed as * an argument (can be null). The FileURL instance representing the realm that is used to create * the AuthException is a clone of this realm, making it safe for modification. * * @param message the message to pass to AuthException's constructor, can be null * @throws AuthException always throws the created AuthException */ protected void throwAuthException(String message) throws AuthException { FileURL clonedRealm = (FileURL)realm.clone(); clonedRealm.setCredentials(credentials); throw new AuthException(clonedRealm, message); } ////////////////////// // Abstract methods // ////////////////////// /** * Starts the connection managed by this ConnectionHandler, and throws an IOException if the connection could not * be established. This method may be called several times during the life of this ConnectionHandler, if the * connection dropped and must be re-established. * * @throws IOException if an error occurred while trying to establish the connection * @throws AuthException if an authentication error occurred (incorrect login or password, insufficient privileges...) */ public abstract void startConnection() throws IOException, AuthException; /** * Returns true if the connection managed by this ConnectionHandler is currently active/established, * in a state that makes it possible to serve client requests. * *

Implementation note: This method must not perform any I/O which could block the calling thread. * * @return true if the connection managed by this ConnectionHandler is currently active/established */ public abstract boolean isConnected(); /** * Closes the connection managed by this ConnectionHandler. * *

Implementation note: the implementation must guarantee that any calls to {@link #isConnected()} after this * method has been called return false. */ public abstract void closeConnection(); /** * Keeps this connection alive. * *

Implementation note: if keep alive is not available in the underlying protocol or * simply unnecessary, this method should be implemented as a no-op (do nothing). */ public abstract void keepAlive(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/connection/ConnectionHandlerFactory.java ================================================ package com.mucommander.commons.file.connection; import com.mucommander.commons.file.FileURL; /** * This interface should be implemented by classes that are able to create ConnectionHandler instances for a given * server location, typically {@link com.mucommander.commons.file.AbstractFile} implementations. * *

This interface allows to take advantage of {@link ConnectionPool} to share connections across * {@link com.mucommander.commons.file.AbstractFile} instances. * * @author Maxence Bernard */ public interface ConnectionHandlerFactory { /** * Creates and returns a {@link ConnectionHandler} instance for the given location. */ ConnectionHandler createConnectionHandler(FileURL location); } ================================================ FILE: src/main/java/com/mucommander/commons/file/connection/ConnectionPool.java ================================================ package com.mucommander.commons.file.connection; import java.io.InterruptedIOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; /** * @see com.mucommander.commons.file.connection.ConnectionHandler * @author Maxence Bernard */ public class ConnectionPool implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class); /** Singleton instance */ private static final ConnectionPool instance = new ConnectionPool(); /** List of registered ConnectionHandler */ private final static List connectionHandlers = new ArrayList<>(); /** The thread that monitors connections, null if there currently is no registered ConnectionHandler */ private static Thread monitorThread; /** Controls how of often the thread monitor checks connections */ private final static int MONITOR_SLEEP_PERIOD = 1000; /** Maximum number of simultaneous connections per realm/credentials combo */ private final static int MAX_CONNECTIONS_PER_REALM = 4; public static ConnectionHandler getConnectionHandler(ConnectionHandlerFactory connectionHandlerFactory, FileURL url, boolean acquireLock) throws InterruptedIOException { FileURL realm = url.getRealm(); while(true) { synchronized(connectionHandlers) { // Ensures that monitor thread is not currently changing the list while we access it Credentials urlCredentials = url.getCredentials(); int matchingConnHandlers = 0; // Try and find an appropriate existing ConnectionHandler //for (ConnectionHandler connHandler : connectionHandlers) { for (Iterator it = connectionHandlers.iterator(); it.hasNext(); ) { ConnectionHandler connHandler = it.next(); // ConnectionHandler must match the realm and credentials and must not be locked if (connHandler.equals(realm, urlCredentials)) { matchingConnHandlers++; synchronized(connHandler) { // Ensures that lock remains unchanged while we access/update it if (!connHandler.isLocked()) { // Try to acquire lock if a lock was requested if (!acquireLock || connHandler.acquireLock()) { LOGGER.info("returning ConnectionHandler {}, realm = {}", connHandler, realm); // Update last activity timestamp to now connHandler.updateLastActivityTimestamp(); return connHandler; } } } } if (matchingConnHandlers == MAX_CONNECTIONS_PER_REALM) { LOGGER.info("Maximum number of connection per realm reached, waiting for one to be removed or released..."); try { // Wait for a ConnectionHandler to be released or removed from the pool final int timeout = 5000; long t0 = System.currentTimeMillis(); connectionHandlers.wait(timeout); // relinquishes the lock on connectionHandlers if (System.currentTimeMillis() - t0 > timeout) { connHandler.closeConnection(); it.remove(); throw new InterruptedIOException(); } break; } catch(InterruptedException e) { LOGGER.info("Interrupted while waiting on a connection for {}", url, e); connHandler.closeConnection(); it.remove(); throw new InterruptedIOException(); } } } if (matchingConnHandlers == MAX_CONNECTIONS_PER_REALM) { continue; } // No suitable ConnectionHandler found, create a new one ConnectionHandler connHandler = connectionHandlerFactory.createConnectionHandler(url); // Acquire lock if a lock was requested if (acquireLock) { connHandler.acquireLock(); } LOGGER.info("adding new ConnectionHandler {}, realm = {}", connHandler, connHandler.getRealm()); // Insert new ConnectionHandler at first position as if it has more chances to be accessed again soon connectionHandlers.add(0, connHandler);// insertElementAt(connHandler, 0); // Start monitor thread if it is not currently running (if there previously was no registered ConnectionHandler) if (monitorThread == null) { LOGGER.info("starting monitor thread"); monitorThread = new Thread(instance); monitorThread.start(); } // Update last activity timestamp to now connHandler.updateLastActivityTimestamp(); return connHandler; } } } /** * Returns a list of registered ConnectionHandler instances. As the name of this method implies, the returned * list is only a snapshot and will not reflect the modifications that are made after this method has been called. * The List is a cloned one and thus can be safely modified. * * @return a list of registered ConnectionHandler instances */ public static List getConnectionHandlersSnapshot() { synchronized (connectionHandlers) { connectionHandlers.removeIf(connectionHandler -> !connectionHandler.isConnected()); return new ArrayList<>(connectionHandlers); } } // /** // * Returns the ConnectionHandler instance located at the given position in the list. // */ // private static ConnectionHandler getConnectionHandlerAt(int i) { // return connectionHandlers.get(i); // } /** * Called by {@link ConnectionHandler#releaseLock()} to notify the ConnectionHandler that a * ConnectionHandler has been released. */ static void notifyConnectionHandlerLockReleased() { synchronized (connectionHandlers) { // Notify any thread waiting for a ConnectionHandler to be released connectionHandlers.notify(); } } /** * Monitors connections and periodically: *

    *
  • keeps connections alive *
  • closes and removes connections that have expired *
*/ public void run() { while (monitorThread != null) { // Thread will be interrupted by CloseConnectionThread if there are no more ConnectionHandler long now = System.currentTimeMillis(); synchronized(connectionHandlers) { // Ensures that getConnectionHandler is not currently changing the list while we access it for (Iterator it = connectionHandlers.iterator(); it.hasNext();) { final ConnectionHandler connHandler = it.next(); synchronized(connHandler) { // Ensures that no one is trying to acquire a lock on the connection while we access it // Do not touch ConnectionHandler if it is currently locked if (connHandler.isLocked()) { continue; } // Remove ConnectionHandler instance from the list of registered ConnectionHandler // if it is not connected if (!connHandler.isConnected()) { LOGGER.info("Removing unconnected ConnectionHandler {}", connHandler); it.remove(); // Notify any thread waiting for a ConnectionHandler to be released connectionHandlers.notify(); continue; // Skips close on inactivity and keep alive checks } long lastUsed = connHandler.getLastActivityTimestamp(); // If time-to-live has been reached without any connection activity, remove ConnectionHandler // from the list of registered ConnectionHandler and close the connection in a separate thread long closePeriod = connHandler.getCloseOnInactivityPeriod(); if (closePeriod != -1 && now - lastUsed > closePeriod*1000) { LOGGER.info("Removing timed-out ConnectionHandler {}",connHandler); it.remove(); // Notify any thread waiting for a ConnectionHandler to be released connectionHandlers.notify(); // Close connection in a separate thread as it could lock this thread new CloseConnectionThread(connHandler).start(); continue; // Skips keep alive check } // If keep-alive period has been reached without any connection activity or a keep alive, // keep connection alive in a separate thread long keepAlivePeriod = connHandler.getKeepAlivePeriod(); if (keepAlivePeriod != -1 && now-Math.max(lastUsed, connHandler.getLastKeepAliveTimestamp()) > keepAlivePeriod*1000) { // Update last keep alive timestamp to now connHandler.updateLastKeepAliveTimestamp(); // Keep connection alive in a separate thread as it could lock this thread new KeepAliveConnectionThread(connHandler).start(); } } } // Stop monitor thread if there are no more ConnectionHandler if (connectionHandlers.isEmpty()) { LOGGER.info("No more ConnectionHandler, stopping monitor thread"); monitorThread = null; } } // Sleep for MONITOR_SLEEP_PERIOD milliseconds, minus the processing time of this loop try { Thread.sleep(Math.max(0, MONITOR_SLEEP_PERIOD-(System.currentTimeMillis()-now))); } catch (InterruptedException e) { // Will loop again } } } /** * Closes a specified ConnectionHandler's connection in a separate thread and removes the ConnectionHandler from * the list of registered ConnectionHandler instances. */ private static class CloseConnectionThread extends Thread { private final ConnectionHandler connHandler; private CloseConnectionThread(ConnectionHandler connHandler) { this.connHandler = connHandler; } @Override public void run() { // Try to close connection, only if it is connected if (connHandler.isConnected()) { LOGGER.info("Closing connection held by {}", connHandler); connHandler.closeConnection(); } } } /** * Keeps alive a specified ConnectionHandler's connection in a separate thread. If the connection is not currently * active, {@link com.mucommander.commons.file.connection.ConnectionHandler#keepAlive()} will not be called. */ private static class KeepAliveConnectionThread extends Thread { private final ConnectionHandler connHandler; private KeepAliveConnectionThread(ConnectionHandler connHandler) { this.connHandler = connHandler; } @Override public void run() { LOGGER.info("keeping connection alive: {}", connHandler); synchronized(connHandler) { // Ensures that lock was not grabbed in the meantime if (connHandler.isLocked()) { return; } // Keep alive connection, only if it is connected if (connHandler.isConnected()) { connHandler.keepAlive(); } } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractContainsFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; /** * This filter matches files whose string criterion values contain a specified string. * * @author Maxence Bernard */ public class AbstractContainsFilter extends AbstractStringCriterionFilter { /** The string to look for in criterion values */ private final String s; /** * Creates a new AbstractContainsFilter using the specified generator and string, and operating in the * specified mode. * * @param generator generates criterion values for files as requested * @param s the string to compare criterion values against * @param caseSensitive if true, this filter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractContainsFilter(CriterionValueGenerator generator, String s, boolean caseSensitive, boolean inverted) { super(generator, caseSensitive, inverted); this.s = s; } @Override public boolean accept(String value) { if (isCaseSensitive()) { return value.contains(s); } return value.toLowerCase().contains(s.toLowerCase()); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractCriterionFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; import java.util.ArrayList; import java.util.List; /** * AbstractCriterionFilter implements the bulk of the {@link CriterionFilter} interface, matching * files based on the criteria values generated by a given {@link CriterionValueGenerator}. The only method left for * subclasses to implement is {@link #accept(Object)}. * * @author Maxence Bernard */ public abstract class AbstractCriterionFilter extends AbstractFileFilter implements CriterionFilter { private final CriterionValueGenerator generator; /** * Creates a new AbstractCriterionFilter using the specified {@link CriterionValueGenerator} and operating * in non-inverted mode. * * @param generator generates criterion values for files as requested */ public AbstractCriterionFilter(CriterionValueGenerator generator) { this(generator, false); } /** * Creates a new AbstractCriterionFilter using the specified {@link CriterionValueGenerator} and operating * in the specified mode. * * @param generator generates criterion values for files as requested * @param inverted if true, this filter will operate in inverted mode. */ AbstractCriterionFilter(CriterionValueGenerator generator, boolean inverted) { super(inverted); this.generator = generator; } /** * Returns true if this filter matched the given value, according to the current {@link #isInverted()} * mode: *
    *
  • if this filter currently operates in normal (non-inverted) mode, this method will return the value of * {@link #accept(Object)}
  • *
  • if this filter currently operates in inverted mode, this method will return the value of * {@link #reject(Object)}
  • *
* * @param value the value to test * @return true if this filter matched the given value, according to the current inverted mode */ public boolean match(C value) { return inverted ? reject(value) : accept(value); } /** * Returns true if the given value was rejected by this filter, false if it was accepted. * *

The {@link #isInverted() inverted} mode has no effect on the values returned by this method. * * @param value the value to be tested * @return true if the given value was rejected by this filter */ public boolean reject(C value) { return !accept(value); } /** * Convenience method that filters out files that do not {@link #match(AbstractFile) match} this filter and * returns a file array of matched AbstractFile instances. * * @param values values to be tested * @return an array of accepted AbstractFile instances */ public C[] filter(C[] values) { List filteredValuesList = new ArrayList<>(); for (C value : values) { if (accept(value)) { filteredValuesList.add(value); } } @SuppressWarnings({"unchecked"}) C[] filteredValues = (C[]) new Object[filteredValuesList.size()]; return filteredValuesList.toArray(filteredValues); } /** * Convenience method that returns true if all the values in the specified array were matched by * {@link #match(Object)}, false if one of the values wasn't. * * @param values the values to be tested * @return true if all the values in the specified array were accepted */ public boolean match(C[] values) { for (C value : values) { if (!match(value)) { return false; } } return true; } /** * Convenience method that returns true if all the values in the specified array were accepted by * {@link #accept(Object)}, false if one of the values wasn't. * * @param values the values to be tested * @return true if all the values in the specified array were accepted */ public boolean accept(C[] values) { for (C value : values) { if (!accept(value)) { return false; } } return true; } /** * Convenience method that returns true if all the values in the specified array were rejected by * {@link #reject(Object)}, false if one of the values wasn't. * * @param values the values to be tested * @return true if all the values in the specified array were rejected */ public boolean reject(C[] values) { for (C value : values) { if (!reject(value)) { return false; } } return true; } @Override public boolean accept(AbstractFile file) { return accept(generator.getCriterionValue(file)); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractEndsWithFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.util.StringUtils; /** * This filter matches files whose string criterion values end with a specified string. * * @author Maxence Bernard */ public class AbstractEndsWithFilter extends AbstractStringCriterionFilter { /** The string to compare criterion values against */ private final String s; /** * Creates a new AbstractEndsWithFilter using the specified generator and string, and operating in the * specified mode. * * @param generator generates criterion values for files as requested * @param s the string to compare criterion values against * @param caseSensitive if true, this filter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractEndsWithFilter(CriterionValueGenerator generator, String s, boolean caseSensitive, boolean inverted) { super(generator, caseSensitive, inverted); this.s = s; } @Override public boolean accept(String value) { if (isCaseSensitive()) { return value.endsWith(s); } return StringUtils.endsWithIgnoreCase(value, s); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractEqualsFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; /** * This filter matches files whose string criterion values that are equal to a specified string. * * @author Maxence Bernard */ public class AbstractEqualsFilter extends AbstractStringCriterionFilter { /** The string to compare criterion values against */ private String s; /** * Creates a new AbstractEndsWithFilter using the specified generator and string, and operating in the * specified mode. * * @param generator generates criterion values for files as requested * @param s the string to compare criterion values against * @param caseSensitive if true, this filter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractEqualsFilter(CriterionValueGenerator generator, String s, boolean caseSensitive, boolean inverted) { super(generator, caseSensitive, inverted); this.s = s; } @Override public boolean accept(String value) { if (isCaseSensitive()) { return value.equals(s); } return value.equalsIgnoreCase(s); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractExtensionFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.util.StringUtils; /** * This filter matches files whose criterion values are equal to one of several specified extensions. * *

The extension(s) may be any string, but when used in the traditional sense of a file extension (e.g. zip extension) * the '.' character must be included in the specified extension (e.g. ".zip" must be used, not just "zip"). * * @author Maxence Bernard, Nicolas Rinaudo */ public class AbstractExtensionFilter extends AbstractStringCriterionFilter { /** File extensions to match against criterion values */ private final char[][] extensions; /** * Creates a new AbstractExtensionFilter using the specified generator and string, and operating in the * specified mode. * * @param generator generates criterion values for files as requested * @param extensions the extensions to compare criterion values against * @param caseSensitive if true, this filter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ AbstractExtensionFilter(CriterionValueGenerator generator, String[] extensions, boolean caseSensitive, boolean inverted) { super(generator, caseSensitive, inverted); this.extensions = new char[extensions.length][]; for (int i = 0; i < extensions.length; i++) { this.extensions[i] = extensions[i].toCharArray(); } } @Override public boolean accept(String value) { return isCaseSensitive() ? containsCaseSensitive(value) : containsIgnoreCase(value); } private boolean containsIgnoreCase(String value) { int len = value.length(); for (char[] extension : extensions) { if (StringUtils.matchesIgnoreCase(value, extension, len)) { return true; } } return false; } private boolean containsCaseSensitive(String value) { int len = value.length(); // If case isn't important, a simple String.endsWith is enough. for (char[] extension : extensions) { if (StringUtils.matches(value, extension, len)) { return true; } } return false; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractFileFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import java.util.ArrayList; import java.util.List; /** * AbstractFileFilter implements the bulk of the {@link FileFilter} interface. The only method left for * subclasses to implement is {@link #accept(AbstractFile)}. * * @see AbstractFilenameFilter * @author Maxence Bernard */ public abstract class AbstractFileFilter implements FileFilter { /** True if this filter should operate in inverted mode and invert matches */ protected boolean inverted; /** * Creates a new AbstractFileFilter operating in non-inverted mode. */ public AbstractFileFilter() { this(false); } /** * Creates a new AbstractFileFilter operating in the specified mode. * * @param inverted if true, this filter will operate in inverted mode. */ public AbstractFileFilter(boolean inverted) { setInverted(inverted); } /////////////////////////////// // FileFilter implementation // /////////////////////////////// public boolean isInverted() { return inverted; } public void setInverted(boolean inverted) { this.inverted = inverted; } public boolean match(AbstractFile file) { return inverted ? reject(file) : accept(file); } public boolean reject(AbstractFile file) { return !accept(file); } public AbstractFile[] filter(AbstractFile[] files) { List filteredFilesV = new ArrayList<>(); for (AbstractFile file : files) { if (match(file)) { filteredFilesV.add(file); } } AbstractFile[] filteredFiles = new AbstractFile[filteredFilesV.size()]; filteredFilesV.toArray(filteredFiles); return filteredFiles; } public void filter(FileSet files) { for (int i = 0; i < files.size();) { if (reject(files.elementAt(i))) { files.removeElementAt(i); } else { i++; } } } public boolean match(AbstractFile[] files) { for (AbstractFile file : files) { if (!match(file)) { return false; } } return true; } public boolean match(FileSet files) { int nbFiles = files.size(); for (int i=0; i. */ package com.mucommander.commons.file.filter; /** * AbstractFilenameFilter implements the bulk of the {@link FilenameFilter} interface. The only method left * for subclasses to implement is {@link #accept(Object)}. * * @author Maxence Bernard */ public abstract class AbstractFilenameFilter extends AbstractStringCriterionFilter implements FilenameFilter { /** * Creates a new case-insensitive AbstractFilenameFilter operating in non-inverted mode. */ public AbstractFilenameFilter() { this(false, false); } /** * Creates a new AbstractFilenameFilter operating in non-inverted mode. * * @param caseSensitive if true, this FilenameFilter will be case-sensitive */ public AbstractFilenameFilter(boolean caseSensitive) { this(caseSensitive, false); } /** * Creates a new AbstractFilenameFilter operating in the specified mode. * * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractFilenameFilter(boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractPathFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; /** * AbstractPathFilter implements the bulk of the {@link PathFilter} interface. The only method left * for subclasses to implement is {@link #accept(Object)}. * * @author Maxence Bernard */ public abstract class AbstractPathFilter extends AbstractStringCriterionFilter implements PathFilter { /** * Creates a new case-insensitive AbstractPathFilter operating in non-inverted mode. */ public AbstractPathFilter() { this(false, false); } /** * Creates a new AbstractPathFilter operating in non-inverted mode. * * @param caseSensitive if true, this FilePathFilter will be case-sensitive */ public AbstractPathFilter(boolean caseSensitive) { this(caseSensitive, false); } /** * Creates a new AbstractPathFilter operating in the specified mode. * * @param caseSensitive if true, this FilePathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractPathFilter(boolean caseSensitive, boolean inverted) { super(new PathGenerator(), caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractRegexpFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * This {@link AbstractStringCriterionFilter} accept or reject files whose string criterion values match a specific * regular expression. * * @author Nicolas Rinaudo, Maxence Bernard */ public abstract class AbstractRegexpFilter extends AbstractStringCriterionFilter { /** Pattern against which criteria values will be compared. */ private final Pattern pattern; /** * Creates a new AbstractRegexpFilter matching the specified regexp and operating in the specified * modes. * * @param generator generates criterion values for files as requested * @param regexp regular expression that matches string values. * @param caseSensitive whether the regular expression is case sensitive or not. * @param inverted if true, this filter will operate in inverted mode. * @throws PatternSyntaxException if the syntax of the regular expression is not correct. */ public AbstractRegexpFilter(CriterionValueGenerator generator, String regexp, boolean caseSensitive, boolean inverted) throws PatternSyntaxException { super(generator, caseSensitive, inverted); pattern = Pattern.compile(regexp, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); } /** * Returns the regular expression used by this filter. * * @return the regular expression used by this filter. */ public String getRegularExpression() { return pattern.pattern(); } /** * Returns true if the specified value matches the filter's regular expression. * * @param value value to match against the filter's regular expression. * @return true if the specified value matches the filter's regular expression, * false otherwise. */ @Override public boolean accept(String value) { return pattern.matcher(value).matches(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractStartsWithFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.util.StringUtils; /** * This filter matches files whose criterion values start with a specified string. * * @author Maxence Bernard */ public class AbstractStartsWithFilter extends AbstractStringCriterionFilter { /** The string to match against criterion values */ private final String s; /** * Creates a new AbstractStartsWithFilter using the specified generator and string, and operating in the * specified mode. * * @param generator generates criterion values for files as requested * @param s the string to compare criterion values against * @param caseSensitive if true, this filter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractStartsWithFilter(CriterionValueGenerator generator, String s, boolean caseSensitive, boolean inverted) { super(generator, caseSensitive, inverted); this.s = s; } @Override public boolean accept(String value) { if (isCaseSensitive()) { return value.startsWith(s); } return StringUtils.startsWithIgnoreCase(value, s); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AbstractStringCriterionFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; /** * AbstractCriterionFilter implements the bulk of the {@link StringCriterionFilter} interface, matching * files based on the criteria values generated by a given {@link CriterionValueGenerator}. The only method left for * subclasses to implement is {@link #accept(Object)}. * * @see AbstractPathFilter * @see AbstractFilenameFilter * @author Maxence Bernard */ public abstract class AbstractStringCriterionFilter extends AbstractCriterionFilter implements StringCriterionFilter { /** True if this StringCriterionFilter is case-sensitive. */ private boolean caseSensitive; /** * Creates a new case-insensitive AbstractStringCriterionFilter operating in non-inverted mode. * * @param generator generates criterion values for files as requested */ public AbstractStringCriterionFilter(CriterionValueGenerator generator) { this(generator, false, false); } /** * Creates a new AbstractStringCriterionFilter operating in non-inverted mode. * * @param generator generates criterion values for files as requested * @param caseSensitive if true, this FilePathFilter will be case-sensitive */ public AbstractStringCriterionFilter(CriterionValueGenerator generator, boolean caseSensitive) { this(generator, caseSensitive, false); } /** * Creates a new AbstractStringCriterionFilter that operates in the specified mode. * * @param generator generates criterion values for files as requested * @param caseSensitive if true, this FilePathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public AbstractStringCriterionFilter(CriterionValueGenerator generator, boolean caseSensitive, boolean inverted) { super(generator, inverted); setCaseSensitive(caseSensitive); } @Override public boolean isCaseSensitive() { return caseSensitive; } @Override public void setCaseSensitive(boolean caseSensitive) { this.caseSensitive = caseSensitive; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AndFileFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * AndFileFilter is a {@link ChainedFileFilter} that matches a file if all of its registered filters match it. * * @author Maxence Bernard */ public class AndFileFilter extends ChainedFileFilter { /** * Creates a new AndFileFilter operating in non-inverted mode and containing the specified filters, * if any. * * @param filters filters to add to this chained filter. */ public AndFileFilter(FileFilter... filters) { this(false, filters); } /** * Creates a new AndFileFilter operating in the specified mode and containing the specified filters, * if any. * * @param inverted if true, this filter will operate in inverted mode. * @param filters filters to add to this chained filter. */ public AndFileFilter(boolean inverted, FileFilter... filters) { super(inverted, filters); } /** * Calls {@link #match(com.mucommander.commons.file.AbstractFile)} on each of the registered filters, and returns * true if all of them matched the given file, false if one of them didn't. * *

If this {@link ChainedFileFilter} contains no filter, this method will always return true. * * @param file the file to test against the registered filters * @return if the file was matched by all filters, false if one of them didn't */ @Override public boolean accept(AbstractFile file) { for (FileFilter filter : filters) { if (!filter.match(file)) { return false; } } return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/AttributeFileFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * AttributeFileFilter matches files which have a specific attribute set. * Here's a list of supported file attributes: *

    *
  • {@link FileAttribute#DIRECTORY}
  • *
  • {@link FileAttribute#FILE}
  • *
  • {@link FileAttribute#BROWSABLE}
  • *
  • {@link FileAttribute#ARCHIVE}
  • *
  • {@link FileAttribute#SYMLINK}
  • *
  • {@link FileAttribute#HIDDEN}
  • *
  • {@link FileAttribute#ROOT}
  • *
* *

Only one attribute can be matched at a time. To match several attributes, combine them using a * {@link com.mucommander.commons.file.filter.ChainedFileFilter}. * * @author Maxence Bernard */ public class AttributeFileFilter extends AbstractFileFilter { public enum FileAttribute { /** Tests if the file is a {@link com.mucommander.commons.file.AbstractFile#isDirectory() directory}. */ DIRECTORY, /** Tests if the file is a regular file, i.e. not a directory. This is equivalent to negating {@link #DIRECTORY}. */ FILE, /** Tests if the file is {@link com.mucommander.commons.file.AbstractFile#isBrowsable() browsable}. */ BROWSABLE, /** Tests if the file is an {@link com.mucommander.commons.file.AbstractFile#isArchive() archive}. */ ARCHIVE, /** Tests if the file is a {@link com.mucommander.commons.file.AbstractFile#isSymlink() symlink}. */ SYMLINK, /** Tests if the file is {@link com.mucommander.commons.file.AbstractFile#isHidden() hidden}. */ HIDDEN, /** Tests if the file is a {@link com.mucommander.commons.file.AbstractFile#isRoot() root folder}. */ ROOT, /** Tests if the file is a {@link com.mucommander.commons.file.AbstractFile#isSystem() system file}. */ SYSTEM } /** The attribute to test files against */ private FileAttribute attribute; /** * Creates a new AttributeFileFilter matching files that have the specified attribute set and operating * in non-inverted mode. * * @param attribute the attribute to test files against */ public AttributeFileFilter(FileAttribute attribute) { this(attribute, false); } /** * Creates a new AttributeFileFilter matching files that have the specified attribute set and operating * in the specified mode. * * @param attribute the attribute to test files against * @param inverted if true, this filter will operate in inverted mode. */ public AttributeFileFilter(FileAttribute attribute, boolean inverted) { super(inverted); this.attribute = attribute; } /** * Returns the attribute which files are tested against. * * @return the attribute which files are tested against. */ public FileAttribute getAttribute() { return attribute; } /** * Sets the attribute which files are tested against. * * @param attribute the attribute which files are tested against. */ public void setAttribute(FileAttribute attribute) { this.attribute = attribute; } @Override public boolean accept(AbstractFile file) { switch(attribute) { case DIRECTORY: return file.isDirectory(); case FILE: return !file.isDirectory(); case BROWSABLE: return file.isBrowsable(); case ARCHIVE: return file.isArchive(); case SYMLINK: return file.isSymlink(); case HIDDEN: return file.isHidden(); case ROOT: return file.isRoot(); case SYSTEM: return file.isSystem(); default: return true; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/ChainedFileFilter.java ================================================ package com.mucommander.commons.file.filter; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * ChainedFileFilter combines one or several {@link FileFilter} to act as just one. *{@link #addFileFilter(FileFilter)} and {@link #removeFileFilter(FileFilter)} allow to add or remove a * FileFilter, {@link #getFileFilterIterator()} to iterate through all the registered filters. * *

The {@link AndFileFilter} and {@link OrFileFilter} implementations match files that respectively match all of * the registered filters, or any of them. * * @see AndFileFilter * @see OrFileFilter * @author Maxence Bernard */ public abstract class ChainedFileFilter extends AbstractFileFilter { /** List of registered FileFilter */ protected List filters = new ArrayList<>(); /** * Creates a new ChainedFileFilter operating in non-inverted mode and containing the specified filters, * if any. * * @param filters filters to add to this chained filter. */ public ChainedFileFilter(FileFilter... filters) { this(false, filters); } /** * Creates a new ChainedFileFilter operating in the specified mode and containing the specified filters, * if any. * * @param inverted if true, this filter will operate in inverted mode. * @param filters filters to add to this chained filter. */ public ChainedFileFilter(boolean inverted, FileFilter... filters) { super(inverted); for (FileFilter filter : filters) addFileFilter(filter); } /** * Adds a new {@link FileFilter} to the list of chained filters. * * @param filter the FileFilter to add */ public void addFileFilter(FileFilter filter) { filters.add(filter); } /** * Removes a {@link FileFilter} from the list of chained filters. Does nothing if the given FileFilter * is not contained by this ChainedFileFilter. * * @param filter the FileFilter to remove */ public void removeFileFilter(FileFilter filter) { filters.remove(filter); } /** * Returns an Iterator that traverses all the registered filters. * * @return an Iterator that traverses all the registered filters. */ public Iterator getFileFilterIterator() { return filters.iterator(); } /** * Returns true if this chained filter doesn't contain any file filter. * * @return true if this chained filter doesn't contain any file filter. */ public boolean isEmpty() { return filters.isEmpty(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/ContainsFilenameFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link FilenameFilter} matches filenames that contain a specified string that can be located anywhere in the * filename. * * @author Maxence Bernard */ public class ContainsFilenameFilter extends AbstractContainsFilter implements FilenameFilter { /** * Creates a new case-insensitive ContainsFilenameFilter operating in non-inverted mode. * * @param s the string to compare filenames against */ public ContainsFilenameFilter(String s) { this(s, false, false); } /** * Creates a new ContainsFilenameFilter operating in non-inverted mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive */ public ContainsFilenameFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new ContainsFilenameFilter operating in the specified mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public ContainsFilenameFilter(String s, boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/ContainsPathFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link PathFilter} matches paths that contain a specified string that can be located anywhere in the * path. * * @author Maxence Bernard */ public class ContainsPathFilter extends AbstractContainsFilter implements PathFilter { /** * Creates a new case-insensitive ContainsPathFilter operating in non-inverted mode. * * @param s the string to compare paths against */ public ContainsPathFilter(String s) { this(s, false, false); } /** * Creates a new ContainsPathFilter operating in non-inverted mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive */ public ContainsPathFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new ContainsPathFilter operating in the specified mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public ContainsPathFilter(String s, boolean caseSensitive, boolean inverted) { super(new PathGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/CriterionFilter.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * CriterionFilter is a {@link FileFilter} that operates on a file criterion. It can be used to match * paths without having to deal with {@link AbstractFile} instances. By extending {@link FileFilter}, this class can be * used everywhere a FileFilter is accepted. * *

Several convenience methods are provided to operate this filter on a set of criteria values, and filter out * values that are rejected by this filter. * * @see AbstractPathFilter * @author Maxence Bernard */ public interface CriterionFilter extends FileFilter { /** * Returns true if this filter matched the given value, according to the current {@link #isInverted()} * mode: *

    *
  • if this filter currently operates in normal (non-inverted) mode, this method will return the value of {@link #accept(Object)}
  • *
  • if this filter currently operates in inverted mode, this method will return the value of {@link #reject(Object)}
  • *
* * @param value the value to test * @return true if this filter matched the given value, according to the current inverted mode */ boolean match(C value); /** * Returns true if the given value was rejected by this filter, false if it was accepted. * *

The {@link #isInverted() inverted} mode has no effect on the values returned by this method. * * @param value the value to be tested * @return true if the given value was rejected by this filter */ boolean reject(C value); /** * Convenience method that filters out files that do not {@link #match(AbstractFile) match} this filter and * returns a file array of matched AbstractFile instances. * * @param value values to be tested * @return an array of accepted AbstractFile instances */ C[] filter(C[] value); /** * Convenience method that returns true if all the values in the specified array were matched by * {@link #match(Object)}, false if one of the values wasn't. * * @param value the values to be tested * @return true if all the values in the specified array were accepted */ boolean match(C[] value); /** * Convenience method that returns true if all the values in the specified array were accepted by * {@link #accept(Object)}, false if one of the values wasn't. * * @param value the values to be tested * @return true if all the values in the specified array were accepted */ boolean accept(C[] value); /** * Convenience method that returns true if all the values in the specified array were rejected by * {@link #reject(Object)}, false if one of the values wasn't. * * @param value the values to be tested * @return true if all the values in the specified array were rejected */ boolean reject(C[] value); /** * Returns true if the given value was accepted by this filter, false if it was rejected. * * @param value the value to test * @return true if the given value was accepted by this filter, false if it was rejected */ boolean accept(C value); } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/CriterionValueGenerator.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * This interface defines a {@link #getCriterionValue(AbstractFile)} method that generates a criterion value for * a specified {@link AbstractFile}. It is used by {@link CriterionFilter} to match files based on their criteria * values. * * @see CriterionFilter * @author Maxence Bernard */ public interface CriterionValueGenerator { C getCriterionValue(AbstractFile file); } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/EmptyFileFilter.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; public class EmptyFileFilter extends AbstractFileFilter { @Override public boolean accept(AbstractFile file) { return file.getSize() == 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/EndsWithFilenameFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link FilenameFilter} matches filenames that end with a specified string. * * @author Maxence Bernard */ public class EndsWithFilenameFilter extends AbstractEndsWithFilter implements FilenameFilter { /** * Creates a new case-insensitive EndsWithFilenameFilter operating in non-inverted mode. * * @param s the string to compare filenames against */ public EndsWithFilenameFilter(String s) { this(s, false, false); } /** * Creates a new EndsWithFilenameFilter operating in non-inverted mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive */ public EndsWithFilenameFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new EndsWithFilenameFilter operating in the specified mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public EndsWithFilenameFilter(String s, boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/EndsWithPathFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link PathFilter} matches paths that end with a specified string. * * @author Maxence Bernard */ public class EndsWithPathFilter extends AbstractEndsWithFilter implements PathFilter { /** * Creates a new case-insensitive EndsWithPathFilter operating in non-inverted mode. * * @param s the string to compare paths against */ public EndsWithPathFilter(String s) { this(s, false, false); } /** * Creates a new EndsWithPathFilter operating in non-inverted mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive */ public EndsWithPathFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new EndsWithPathFilter operating in the specified mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public EndsWithPathFilter(String s, boolean caseSensitive, boolean inverted) { super(new PathGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/EqualsFilenameFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link FilenameFilter} matches filenames that are equal to a specified string. * * @author Maxence Bernard */ public class EqualsFilenameFilter extends AbstractEqualsFilter implements FilenameFilter { /** * Creates a new case-insensitive EqualsFilenameFilter operating in non-inverted mode. * * @param s the string to compare filenames against */ public EqualsFilenameFilter(String s) { this(s, false, false); } /** * Creates a new EqualsFilenameFilter operating in non-inverted mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive */ public EqualsFilenameFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new EqualsFilenameFilter operating in the specified mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public EqualsFilenameFilter(String s, boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/EqualsPathFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link PathFilter} matches paths that are equal to a specified string. * * @author Maxence Bernard */ public class EqualsPathFilter extends AbstractEqualsFilter implements PathFilter { /** * Creates a new case-insensitive EqualsPathFilter operating in non-inverted mode. * * @param s the string to compare paths against */ public EqualsPathFilter(String s) { this(s, false, false); } /** * Creates a new EqualsPathFilter operating in non-inverted mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive */ public EqualsPathFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new EqualsPathFilter operating in the specified mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public EqualsPathFilter(String s, boolean caseSensitive, boolean inverted) { super(new PathGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/ExtensionFilenameFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link FilenameFilter} matches files whose path end with one of several specified extensions. * *

The extension(s) may be any string, but when used in the traditional sense of a file extension (e.g. zip extension) * the '.' character must be included in the specified extension (e.g. ".zip" must be used, not just "zip"). * * @author Maxence Bernard, Nicolas Rinaudo */ public class ExtensionFilenameFilter extends AbstractExtensionFilter implements FilenameFilter { /** * Creates a case-insensitive ExtensionFilenameFilter matching filenames ending with the specified * extension and operating in non-inverted mode. * * @param extension the extension to match */ public ExtensionFilenameFilter(String extension) { this(extension, false, false); } /** * Creates a ExtensionFilenameFilter matching filenames ending with the specified extension * and operating in the specified modes. * * @param extension the extension to match * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public ExtensionFilenameFilter(String extension, boolean caseSensitive, boolean inverted) { this(new String[]{extension}, caseSensitive, inverted); } /** * Creates a case-insensitive ExtensionFilenameFilter matching filenames ending with one of the * specified extensions and operating in the specified mode. * * @param ext the extensions to match */ public ExtensionFilenameFilter(String[] ext) { this(ext, false, false); } /** * Creates a new ExtensionFilenameFilter matching filenames ending with one of the specified * extensions and operating in the specified modes. * * @param ext the extensions to match * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public ExtensionFilenameFilter(String[] ext, boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), ext, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/ExtensionPathFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; /** * This {@link FilenameFilter} matches files whose path end with one of several specified extensions. * *

The extension(s) may be any string, but when used in the traditional sense of a file extension (e.g. zip extension) * the '.' character must be included in the specified extension (e.g. ".zip" must be used, not just "zip"). * * @author Maxence Bernard, Nicolas Rinaudo */ public class ExtensionPathFilter extends AbstractExtensionFilter implements PathFilter { /** * Creates a case-insensitive ExtensionPathFilter matching paths ending with the specified * extension and operating in non-inverted mode. * * @param extension the extension to match */ public ExtensionPathFilter(String extension) { this(extension, false, false); } /** * Creates a ExtensionPathFilter matching paths ending with the specified extension * and operating in the specified modes. * * @param extension the extension to match * @param caseSensitive if true, this PathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ private ExtensionPathFilter(String extension, boolean caseSensitive, boolean inverted) { this(new String[]{extension}, caseSensitive, inverted); } /** * Creates a case-insensitive ExtensionPathFilter matching paths ending with one of the * specified extensions and operating in the specified mode. * * @param ext the extensions to match */ public ExtensionPathFilter(String[] ext) { this(ext, false, false); } /** * Creates a new ExtensionPathFilter matching paths ending with one of the specified * extensions and operating in the specified modes. * * @param ext the extensions to match * @param caseSensitive if true, this PathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public ExtensionPathFilter(String[] ext, boolean caseSensitive, boolean inverted) { super(new PathGenerator(), ext, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/FileFilter.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; /** * A FileFilter matches files that meet certain criteria. It can operate in two opposite modes: inverted * and non-inverted. By default, a FileFilter operates in non-inverted mode where * {@link #match(AbstractFile)} returns the value of {@link #accept(AbstractFile)}. On the contrary, when operating in * inverted mode, {@link #match(AbstractFile)} returns the value of {@link #reject(AbstractFile)}. It is important to * understand that {@link #accept(AbstractFile)} and {@link #reject(AbstractFile)} are not affected by the inverted * mode in which a filter operates. * *

Several convenience methods are provided to operate this filter on a set of files, and filter out files that * do not match this filter. * *

A FileFilter instance can be passed to {@link AbstractFile#ls(FileFilter)} to filter out some of the * the files contained by a folder. * * @see AbstractFileFilter * @see FilenameFilter * @see com.mucommander.commons.file.AbstractFile#ls(FileFilter) * @author Maxence Bernard */ public interface FileFilter { /** * Return true if this filter operates in normal mode, false if in inverted mode. * * @return true if this filter operates in normal mode, false if in inverted mode */ boolean isInverted(); /** * Sets the mode in which {@link #match(com.mucommander.commons.file.AbstractFile)} operates. If true, this * filter will operate in inverted mode: files that would be accepted by {@link #match(com.mucommander.commons.file.AbstractFile)} * in normal (non-inverted) mode will be rejected, and vice-versa.
* The inverted mode has no effect on the values returned by {@link #accept(com.mucommander.commons.file.AbstractFile)} and * {@link #reject(com.mucommander.commons.file.AbstractFile)}. * * @param inverted if true, this filter will operate in inverted mode. */ void setInverted(boolean inverted); /** * Returns true if this filter matched the given file, according to the current {@link #isInverted()} * mode: *

    *
  • if this filter currently operates in normal (non-inverted) mode, this method will return the value of {@link #accept(com.mucommander.commons.file.AbstractFile)}
  • *
  • if this filter currently operates in inverted mode, this method will return the value of {@link #reject(com.mucommander.commons.file.AbstractFile)}
  • *
* * @param file the file to test * @return true if this filter matched the given file, according to the current inverted mode */ boolean match(AbstractFile file); /** * Returns true if the given file was rejected by this filter, false if it was accepted. * *

The {@link #isInverted() inverted} mode has no effect on the values returned by this method. * * @param file the file to test * @return true if the given file was rejected by this FileFilter */ boolean reject(AbstractFile file); /** * Convenience method that filters out files that do not {@link #match(AbstractFile) match} this filter and * returns a file array of matched AbstractFile instances. * * @param files files to be tested against {@link #match(com.mucommander.commons.file.AbstractFile)} * @return a file array of files that were matched by this filter */ AbstractFile[] filter(AbstractFile files[]); /** * Convenience method that filters out files that do not {@link #match(AbstractFile) match} this filter * and removes them from the given {@link FileSet}. * * @param files files to be tested against {@link #match(com.mucommander.commons.file.AbstractFile)} */ void filter(FileSet files); /** * Convenience method that returns true if all the files contained in the specified file array * were matched by {@link #match(AbstractFile)}, false if one of the files wasn't. * * @param files the files to test against this FileFilter * @return true if all the files contained in the specified file array were matched by this filter */ boolean match(AbstractFile files[]); /** * Convenience method that returns true if all the files contained in the specified {@link FileSet} * were matched by {@link #match(AbstractFile)}, false if one of the files wasn't. * * @param files the files to test against this FileFilter * @return true if all the files contained in the specified {@link FileSet} were matched by this filter */ boolean match(FileSet files); /** * Convenience method that returns true if all the files contained in the specified file array * were accepted by {@link #accept(AbstractFile)}, false if one of the files wasn't. * * @param files the files to test against this FileFilter * @return true if all the files contained in the specified file array were accepted by this filter */ boolean accept(AbstractFile files[]); /** * Convenience method that returns true if all the files contained in the specified {@link FileSet} * were accepted by {@link #accept(AbstractFile)}, false if one of the files wasn't. * * @param files the files to test against this FileFilter * @return true if all the files contained in the specified {@link FileSet} were accepted by this filter */ boolean accept(FileSet files); /** * Convenience method that returns true if all the files contained in the specified file array * were rejected by {@link #reject(AbstractFile)}, false if one of the files wasn't. * * @param files the files to test against this FileFilter * @return true if all the files contained in the specified file array were rejected by this filter */ boolean reject(AbstractFile files[]); /** * Convenience method that returns true if all the files contained in the specified {@link FileSet} * were rejected by {@link #reject(AbstractFile)}, false if one of the files wasn't. * * @param files the files to test against this FileFilter * @return true if all the files contained in the specified {@link FileSet} were rejected by this filter */ boolean reject(FileSet files); /** * Returns true if the given file was accepted by this filter, false if it was rejected. * *

The {@link #isInverted() inverted} mode has no effect on the values returned by this method. * * @param file the file to test * @return true if the given file was accepted by this FileFilter */ boolean accept(AbstractFile file); } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/FileOperationFilter.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; /** * OperationFileFilter matches files which support a specified {@link FileOperation file operation}. * *

Only one file operation can be matched at a time. To match several file operations, combine them using a * {@link com.mucommander.commons.file.filter.ChainedFileFilter}. * * @see FileOperation * @author Maxence Bernard */ public class FileOperationFilter extends AbstractFileFilter { /** The file operation to match */ private FileOperation op; public FileOperationFilter(FileOperation op) { this(op, false); } public FileOperationFilter(FileOperation op, boolean inverted) { super(inverted); setFileOperation(op); } /** * Returns the file operation this filter matches. * * @return the file operation this filter matches. */ public FileOperation getFileOperation() { return op; } /** * Sets the file operation this filter matches, replacing the previous operation. * * @param op the file operation this filter matches. */ public void setFileOperation(FileOperation op) { this.op = op; } @Override public boolean accept(AbstractFile file) { return file.isFileOperationSupported(op); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/FilenameFilter.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * FilenameFilter is a {@link FileFilter} that operates on filenames. * *

A FilenameFilter can be passed to {@link AbstractFile#ls(FilenameFilter)} to filter out some of the * files contained by a folder without creating the associated AbstractFile instances. * * @see AbstractFilenameFilter * @author Maxence Bernard */ public interface FilenameFilter extends StringCriterionFilter { } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/FilenameGenerator.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * This interface specializes {@link CriterionValueGenerator} to have {@link #getCriterionValue(AbstractFile)} return * the filename of the specified file. * * @author Maxence Bernard */ public class FilenameGenerator implements CriterionValueGenerator { public String getCriterionValue(AbstractFile file) { return file.getName(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/MountedDriveFilter.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; /** * @author Oleg Trifonov * Created on 26/01/16. */ public class MountedDriveFilter extends AbstractFileFilter { @Override public boolean accept(AbstractFile file) { if (file == null || !OsFamily.MAC_OS_X.isCurrent()) { return false; } for (AbstractFile f : LocalFile.getVolumes() ) { if (file.equals(f)) { return !file.isSymlink(); } } return false; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/OrFileFilter.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * OrFileFilter is a {@link ChainedFileFilter} that matches a file if one of its registered filters * matches it. * * @author Maxence Bernard */ public class OrFileFilter extends ChainedFileFilter { /** * Creates a new OrFileFilter operating in non-inverted mode and containing the specified filters, * if any. * * @param filters filters to add to this chained filter. */ public OrFileFilter(FileFilter... filters) { this(false, filters); } /** * Creates a new OrFileFilter operating in the specified mode and containing the specified filters, * if any. * * @param inverted if true, this filter will operate in inverted mode. * @param filters filters to add to this chained filter. */ public OrFileFilter(boolean inverted, FileFilter... filters) { super(inverted, filters); } /** * Calls {@link #match(com.mucommander.commons.file.AbstractFile)} on each of the registered filters, and returns * true if one of them matched the given file, false if none of them did. * *

If this {@link ChainedFileFilter} contains no filter, this method will always return true. * * @param file the file to test against the registered filters * @return if the file was matched by one filter, false if none of them did */ @Override public boolean accept(AbstractFile file) { for (FileFilter filter : filters) { if (filter.match(file)) { return true; } } return filters.isEmpty(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/PassThroughFileFilter.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * PassThroughFileFilter is a filter that {@link #accept(com.mucommander.commons.file.AbstractFile) accepts} all * files. Depending on the {@link #isInverted() inverted} mode, this filter will match all files or no file at all. * * @author Maxence Bernard */ public class PassThroughFileFilter extends AbstractFileFilter { /** * Creates a new PassThroughFileFilter operating in non-inverted mode. */ public PassThroughFileFilter() { this(false); } /** * Creates a new PassThroughFileFilter operating in the specified mode. * * @param inverted if true, this filter will operate in inverted mode. */ public PassThroughFileFilter(boolean inverted) { super(inverted); } @Override public boolean accept(AbstractFile file) { return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/PathFilter.java ================================================ package com.mucommander.commons.file.filter; /** * PathFilter is a {@link FileFilter} that operates on absolute file paths. * * @see AbstractPathFilter * @author Maxence Bernard */ public interface PathFilter extends StringCriterionFilter { } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/PathGenerator.java ================================================ package com.mucommander.commons.file.filter; import com.mucommander.commons.file.AbstractFile; /** * This interface specializes {@link CriterionValueGenerator} to have {@link #getCriterionValue(AbstractFile)} return * the absolute path of the specified file. * * @author Maxence Bernard */ public class PathGenerator implements CriterionValueGenerator { @Override public String getCriterionValue(AbstractFile file) { return file.getPath(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/RegexpFilenameFilter.java ================================================ package com.mucommander.commons.file.filter; import java.util.regex.PatternSyntaxException; /** * This {@link FilenameFilter} that accepts or rejects files whose filename match a specific regular expression. * * @author Maxence Bernard */ public class RegexpFilenameFilter extends AbstractRegexpFilter implements FilenameFilter { /** * Creates a new RegexpFilenameFilter matching the specified regexp and operating in non-inverted * mode. * * @param regexp regular expression that matches string values. * @param caseSensitive whether the regular expression is case-sensitive or not. * @throws PatternSyntaxException if the syntax of the regular expression is not correct. */ public RegexpFilenameFilter(String regexp, boolean caseSensitive) throws PatternSyntaxException { super(new FilenameGenerator(), regexp, caseSensitive, false); } /** * Creates a new RegexpFilenameFilter matching the specified regexp and operating in the specified * modes. * * @param regexp regular expression that matches string values. * @param caseSensitive whether the regular expression is case-sensitive or not. * @param inverted if true, this filter will operate in inverted mode. * @throws PatternSyntaxException if the syntax of the regular expression is not correct. */ public RegexpFilenameFilter(String regexp, boolean caseSensitive, boolean inverted) throws PatternSyntaxException { super(new FilenameGenerator(), regexp, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/RegexpPathFilter.java ================================================ package com.mucommander.commons.file.filter; import java.util.regex.PatternSyntaxException; /** * This {@link PathFilter} that accepts or rejects files whose path match a specific regular expression. * * @author Maxence Bernard */ public class RegexpPathFilter extends AbstractRegexpFilter implements PathFilter { /** * Creates a new RegexpPathFilter matching the specified regexp and operating in non-inverted * mode. * * @param regexp regular expression that matches string values. * @param caseSensitive whether the regular expression is case-sensitive or not. * @throws PatternSyntaxException if the syntax of the regular expression is not correct. */ public RegexpPathFilter(String regexp, boolean caseSensitive) throws PatternSyntaxException { super(new PathGenerator(), regexp, caseSensitive, false); } /** * Creates a new RegexpPathFilter matching the specified regexp and operating in the specified * modes. * * @param regexp regular expression that matches string values. * @param caseSensitive whether the regular expression is case-sensitive or not. * @param inverted if true, this filter will operate in inverted mode. * @throws PatternSyntaxException if the syntax of the regular expression is not correct. */ public RegexpPathFilter(String regexp, boolean caseSensitive, boolean inverted) throws PatternSyntaxException { super(new PathGenerator(), regexp, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/StartsWithFilenameFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link FilenameFilter} matches filenames that start with a specified string. * * @author Maxence Bernard */ public class StartsWithFilenameFilter extends AbstractStartsWithFilter implements FilenameFilter { /** * Creates a new case-insensitive StartsFilenameFilter matching filenames starting with the specified * string and operating in the specified mode. * * @param s the string to compare filenames against */ public StartsWithFilenameFilter(String s) { this(s, false, false); } /** * Creates a new StartsFilenameFilter matching filenames starting with the specified string and * operating in non-inverted mode. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive */ public StartsWithFilenameFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new StartsFilenameFilter matching filenames starting with the specified string and * operating in the specified modes. * * @param s the string to compare filenames against * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public StartsWithFilenameFilter(String s, boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/StartsWithPathFilter.java ================================================ package com.mucommander.commons.file.filter; /** * This {@link PathFilter} matches paths that start with a specified string. * * @author Maxence Bernard */ public class StartsWithPathFilter extends AbstractStartsWithFilter implements PathFilter { /** * Creates a new case-insensitive StartsPathFilter matching paths starting with the specified * string and operating in the specified mode. * * @param s the string to compare paths against */ public StartsWithPathFilter(String s) { this(s, false, false); } /** * Creates a new StartsPathFilter matching paths starting with the specified string and * operating in non-inverted mode. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive */ public StartsWithPathFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new StartsPathFilter matching paths starting with the specified string and * operating in the specified modes. * * @param s the string to compare paths against * @param caseSensitive if true, this PathFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public StartsWithPathFilter(String s, boolean caseSensitive, boolean inverted) { super(new PathGenerator(), s, caseSensitive, inverted); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/StringCriterionFilter.java ================================================ package com.mucommander.commons.file.filter; /** * @author Maxence Bernard */ public interface StringCriterionFilter extends CriterionFilter { /** * Returns true if this CriterionFilter is case-sensitive. * * @return true if this CriterionFilter is case-sensitive. */ boolean isCaseSensitive(); /** * Specifies whether this CriterionFilter should be case-sensitive or not when comparing paths. * * @param caseSensitive if true, this CriterionFilter will be case-sensitive */ void setCaseSensitive(boolean caseSensitive); } ================================================ FILE: src/main/java/com/mucommander/commons/file/filter/WildcardFileFilter.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.filter; import java.io.File; import org.apache.commons.io.IOCase; /** * This filter matches files whose string criterion values correspond a specified wildcard mask (with '*' and/or '?' characters). * * @author Oleg Trifonov */ public class WildcardFileFilter extends AbstractStringCriterionFilter { private org.apache.commons.io.filefilter.AbstractFileFilter fileFilter; /** * Creates a new case-insensitive WildcardFileFilter operating in non-inverted mode. * * @param s the wildcard to match */ public WildcardFileFilter(String s) { this(s, false, false); } /** * Creates a new WildcardFileFilter operating in non-inverted mode. * * @param s the wildcard to match * @param caseSensitive if true, this FilenameFilter will be case-sensitive */ public WildcardFileFilter(String s, boolean caseSensitive) { this(s, caseSensitive, false); } /** * Creates a new WildcardFileFilter operating in the specified mode. * * @param s the wildcard to match * @param caseSensitive if true, this FilenameFilter will be case-sensitive * @param inverted if true, this filter will operate in inverted mode. */ public WildcardFileFilter(String s, boolean caseSensitive, boolean inverted) { super(new FilenameGenerator(), caseSensitive, inverted); this.fileFilter = org.apache.commons.io.filefilter.WildcardFileFilter.builder().setWildcards(s).setIoCase(isCaseSensitive() ? IOCase.SENSITIVE : IOCase.INSENSITIVE).get(); } @Override public boolean accept(String value) { return fileFilter.accept(new File(value)); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/CacheableFileIconProvider.java ================================================ package com.mucommander.commons.file.icon; import com.mucommander.commons.file.AbstractFile; import javax.swing.*; import java.awt.*; /** * CacheableFileIconProvider is an interface to be implemented by file icon providers that wish to use * some icon caching to improve performance. This interface is to be used in conjunction with {@link CachedFileIconProvider} * to form a functional cached provider. * * @author Maxence Bernard */ public interface CacheableFileIconProvider extends FileIconProvider { /** * Returns true if the icon cache can be used for the specified file and preferred resolution. This * method allows the icon cache to be used only for certain types of files and/or preferred resolutions. * *

This method is called by {@link CachedFileIconProvider#getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * each time an icon is requested. If true is returned, the icon cache will be looked up with * {@link #lookupCache(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} and if the cache did not return * an icon, the icon will be added to the cache with {@link #addToCache(com.mucommander.commons.file.AbstractFile, javax.swing.Icon, java.awt.Dimension)}. *
* On the other hand, if false is returned, {@link #getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * will simply be called, without querying or adding to the cache. * * @param file the file for which to retrieve an icon * @param preferredResolution the preferred resolution for the icon * @return true if the icon cache can be used with the specified file */ boolean isCacheable(AbstractFile file, Dimension preferredResolution); /** * This method is called by {@link CachedFileIconProvider#getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * to perform a cache lookup and give implementations a chance to re-use a cached icon. If a non-null value is * returned, the returned icon will be used. *
* On the other hand, if null is returned, the icon will be fetched using {@link #getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * followed by a call to {@link #addToCache(com.mucommander.commons.file.AbstractFile, javax.swing.Icon,java.awt.Dimension)} * to add the freshly-retrieved icon to the cache. * *

This method is called only if the prior call to {@link #isCacheable(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * returned true. * * @param file the file for which to look for a previously cached icon * @param preferredResolution the preferred resolution for the icon * @return a cached icon to re-use, null if there is none */ Icon lookupCache(AbstractFile file, Dimension preferredResolution); /** * This method is called by {@link CachedFileIconProvider#getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * to give implementations a chance to cache an icon fetched with {@link #getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * and have it returned later by {@link #lookupCache(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)}. *
* There is no obligation for this method to cache the given icon, implementations may freely choose whether to * cache certain icons only. * *

This method is called only if the prior call to {@link #isCacheable(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * returned true. * * @param file the file that corresponds to the given icon * @param icon the icon to add to the cache * @param preferredResolution the preferred icon resolution that was originally requested */ void addToCache(AbstractFile file, Icon icon, Dimension preferredResolution); void cleanCache(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/CachedFileIconProvider.java ================================================ package com.mucommander.commons.file.icon; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.ui.icon.FileIcons; import javax.swing.*; import java.awt.*; /** * CachedFileIconProvider is a FileIconProvider with caching capabilities. * *

This class does not actually provide icons nor does it manage the contents of the cache ; it delegates these tasks * to a {@link CacheableFileIconProvider} instance. All this class does is use the cache implementation to harness its * benefits and take all the credit for it.
* When an icon is requested, a cache lookup is performed. If a cached value is found, it is returned. If not, the icon * is fetched from the underlying provider and added to the cache. * * @author Maxence Bernard */ public class CachedFileIconProvider implements FileIconProvider { /** The underlying icon provider and cache manager */ protected CacheableFileIconProvider cacheableFip; /** * Creates a new CachedFileIconProvider that uses the given {@link CacheableFileIconProvider} to access the cache * and retrieve the icons. * * @param cacheableFip the underlying icon provider and cache manager */ public CachedFileIconProvider(CacheableFileIconProvider cacheableFip) { this.cacheableFip = cacheableFip; } /** * Creates and returns a {@link IconCache} instance. * * @return a new {@link IconCache} instance */ public static IconCache createCache() { return new IconCache(); } /** * Implementation notes: this method first calls {@link CacheableFileIconProvider#isCacheable(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * to determine if the icon cache is used. * *

If the file icon is cacheable, {@link CacheableFileIconProvider#lookupCache(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * is called to look for a previously cached icon. If a value is found, it is returned. If not, * {#getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} is called on the CacheableFileIconProvider * to retrieve the icon. This icon is then added to the cache by calling * {@link CacheableFileIconProvider#addToCache(com.mucommander.commons.file.AbstractFile, javax.swing.Icon, java.awt.Dimension)}. * *

If the file icon is not cacheable, {#getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} * is simply called on the CacheableFileIconProvider and its value returned. */ @Override public Icon getFileIcon(AbstractFile file, Dimension preferredResolution) { boolean isCacheable = cacheableFip.isCacheable(file, preferredResolution); if (BookmarkManager.isBookmark(file)) { for (Bookmark bookmark : BookmarkManager.getBookmarks()) { if (file.getName().equals(bookmark.getName())) { // Note: if several bookmarks match current folder, the first one will be used file = FileFactory.getFile(bookmark.getLocation()); //return getFileIcon(file, preferredResolution); return FileIcons.getFileIcon(file, preferredResolution); } } } // Look for the file icon in the provider's cache Icon icon = isCacheable ? cacheableFip.lookupCache(file, preferredResolution) : null; // Icon is not cacheable or isn't present in the cache, retrieve it from the provider if (icon == null) { icon = cacheableFip.getFileIcon(file, preferredResolution); // Cache the icon if (isCacheable && icon != null) { cacheableFip.addToCache(file, icon, preferredResolution); } } return icon; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/FileIconProvider.java ================================================ package com.mucommander.commons.file.icon; import com.mucommander.commons.file.AbstractFile; import javax.swing.*; import java.awt.*; /** * FileIconProvider provides a standard interface for retrieving file icons. Icon providers can fetch icons from any * type of source, whether they be some API or a set of icon images. There is no requirement on the resolution in which * the icons are provided, nor on the set of file criteria used to select the icon (file kind, name, size, ...). * Implementations should however be able to return icons for any type of {@link AbstractFile}. * The specialized {@link com.mucommander.commons.file.icon.LocalFileIconProvider} class can come in handy for icon sources * that can only cope with local files. * * @see com.mucommander.commons.file.FileFactory#getDefaultFileIconProvider() * @author Maxence Bernard */ public interface FileIconProvider { /** * Returns an icon for the given file, or null if it couldn't be retrieved, either because the * given file doesn't exist or for any other reason. * *

The specified Dimension is used as a hint at the preferred icon's resolution; there is * absolutely no guarantee that the returned Icon will indeed have this resolution. This dimension is * only used to choose between different resolutions should more than one resolution be available, and return the * one that most closely matches the specified one.
* The implementation is not expected to perform any rescaling (either up or down), this method should only return * icons in their 'native' resolutions, using the preferred resolution to choose between different native dimensions. * For example, if this provider is able to create icons both in 16x16 and 32x32 resolutions, and a 48x48 resolution * is preferred, the 32x32 resolution should be favored as it is closer to 32x32. * * @param file the AbstractFile instance for which an icon is requested * @param preferredResolution the preferred icon resolution * @return an icon for the requested file */ Icon getFileIcon(AbstractFile file, Dimension preferredResolution); } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/IconCache.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.icon; import org.apache.commons.collections4.map.ReferenceMap; import javax.swing.*; /** * This class provides an icon cache, mapping Object keys onto {@link Icon} instances. * Any kind of Object may be used as the key: a file, a URL, an extension, ... allowing different of icon caching * strategies to be implemented. * *

Icons are stored as {@link java.lang.ref.SoftReference soft references} so they can be garbage collected * when the VM runs low on memory. * *

The implementation uses the {@link ReferenceMap} class part of the Apache Commons Collection library. * All accesses to the underlying map is synchronized, making this cache thread-safe. * * @author Maxence Bernard */ public class IconCache { /** The actual hash map */ private final ReferenceMap hashMap = new ReferenceMap<>(ReferenceMap.ReferenceStrength.HARD, ReferenceMap.ReferenceStrength.SOFT); /** * Creates a new icon cache. */ IconCache() { } /** * Adds a new key/icon mapping to the cache. If a mapping with the same key exists, it is replaced and the previous * value returned. * * @param key the key that will later allow to retrieve the cached icon * @param value the icon instance to cache * @return returns the icon instance previously mapped onto the given key, null if no * such mapping existed */ public synchronized Icon put(Object key, Icon value) { return hashMap.put(key, value); } /** * Returns the {@link Icon} instance mapped onto the given key if there is one, * null otherwise * * @param key key of the icon instance to retrieve * @return the {@link Icon} instance mapped onto the given key if there is one, * null otherwise */ public synchronized Icon get(Object key) { return hashMap.get(key); } /** * Returns true if this cache currently contains a key/icon mapping where the given key is used as * the mapping's key. * * @param key key to lookup * @return true if this cache currently contains a key/icon mapping where the given key is used as * the mapping's key. */ public synchronized boolean containsKey(Object key) { return hashMap.containsKey(key); } /** * Returns true if this cache currently contains a key/icon mapping where the given icon is used as * the mapping's value. * * @param icon icon to lookup * @return true if this cache currently contains a key/icon mapping where the given icon is used as * the mapping's key. */ public synchronized boolean containsValue(Icon icon) { return hashMap.containsValue(icon); } /** * Removes all existing key/icon mapping from this cache, leaving the cache in the same state as it was right after * its creation. */ public synchronized void clear() { hashMap.clear(); } /** * Returns the number of key/icon mapping this cache currently contains. * * @return the number of key/icon mapping this cache currently contains. */ public synchronized int size() { return hashMap.size(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/LocalFileIconProvider.java ================================================ package com.mucommander.commons.file.icon; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.impl.local.LocalFile; import javax.swing.*; import java.awt.*; import java.io.IOException; /** * LocalFileIconProvider is an abstract {@link FileIconProvider} which makes things easier for * implementations that are only able to provide icons for local files. * *

This class implements {@link #getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} and passes on * requests for local file icons to {@link #getLocalFileIcon(com.mucommander.commons.file.impl.local.LocalFile, com.mucommander.commons.file.AbstractFile, java.awt.Dimension)}. * On the other hand, requests for non-local file icons are transformed to local ones, by creating a local temporary * file with the same name (the best effort) and extension (guaranteed) as the non-local file, and passes on the file * to {@link #getLocalFileIcon(com.mucommander.commons.file.impl.local.LocalFile, com.mucommander.commons.file.AbstractFile, java.awt.Dimension)}. * * @author Maxence Bernard */ public abstract class LocalFileIconProvider implements FileIconProvider { /** * Creates a returns a temporary local file/directory with the same extension as the specified file/directory * (guaranteed), and the same filename as much as possible (the best effort). * This method returns null if the temporary file/directory could not be created. * * @param nonLocalFile the non-local file for which to create a temporary file. * @return a temporary local file/directory with the same extension as the specified file/directory */ private LocalFile createTempLocalFile(AbstractFile nonLocalFile) { try { // Note: the returned temporary file may be an AbstractArchiveFile if the filename's extension corresponds // to a registered archive format LocalFile tempFile = FileFactory.getTemporaryFile(nonLocalFile.getName(), false).getAncestor(LocalFile.class); if (tempFile == null) { return null; } // create a directory if (nonLocalFile.isDirectory()) { tempFile.mkdir(); } else { // create a regular file tempFile.getOutputStream().close(); } return tempFile; } catch (IOException e) { return null; } } ///////////////////////////////////// // FileIconProvider implementation // ///////////////////////////////////// public Icon getFileIcon(AbstractFile originalFile, Dimension preferredResolution) { if (originalFile == null) { return null; } // Specified file is a LocalFile or a ProxyFile proxying a LocalFile (e.g. an archive file): let's simply get // the icon using #getLocalFileIcon(LocalFile) AbstractFile topFile = originalFile.getTopAncestor(); if (topFile instanceof LocalFile) { return getLocalFileIcon((LocalFile)topFile, originalFile, preferredResolution); } // File is a remote file: create a temporary local file (or directory) with the same extension to grab the icon // and then delete the file. This operation is I/O bound and thus expensive, so an LRU is used to cache // frequently-accessed file extensions. LocalFile tempFile = createTempLocalFile(topFile); if (tempFile == null) { // No temp file, no icon! return null; } // Get the file icon Icon icon = getLocalFileIcon(tempFile, originalFile, preferredResolution); // Delete the temporary file try { tempFile.delete(); } catch (IOException e) { // Not much to do } return icon; } ////////////////////// // Abstract methods // ////////////////////// /** * Returns an icon for the given local file, null if the icon couldn't be retrieved. This method is * called by {@link #getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} with a {@link LocalFile} * equivalent to the {@link AbstractFile} originally requested. * *

The specified Dimension is used as a hint at the preferred icon's resolution; there is * absolutely no guarantee that the returned Icon will indeed have this resolution. This dimension is * only used to choose between different resolutions should more than one resolution be available, and return the * one that most closely matches the specified one.
* This method is not expected to perform any rescaling (either up or down), returned resolutions should only be * 'native' icon resolutions. For example, if this provider is able to create icons both in 16x16 and 32x32 * resolutions, and a 48x48 resolution is preferred, the 32x32 resolution should be favored for the returned icon. * * @param localFile the LocalFile instance for which an icon is requested * @param originalFile the AbstractFile for which an icon was originally requested * @param preferredResolution the preferred icon resolution * @return an icon for the requested file */ public abstract Icon getLocalFileIcon(LocalFile localFile, AbstractFile originalFile, Dimension preferredResolution); } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/impl/SwingFileIconProvider.java ================================================ package com.mucommander.commons.file.icon.impl; import com.mucommander.commons.file.icon.CachedFileIconProvider; import com.mucommander.commons.file.icon.LocalFileIconProvider; /** * This {@link LocalFileIconProvider} returns system file icons fetched from the Swing API. Those icons are only * available under one resolution, usually 16x16 but this may vary across platforms. * *

Icons are provided by one of the two following Swing classes; the one that provides the best results on the * target platform is used: *

    *
  • javax.swing.filechooser.FileSystemView: used on all platforms but Mac OS X
  • *
  • javax.swing.JFileChooser: used on Mac OS X only
  • *
* Those classes are only capable of returning icons for java.io.File instances, thus only work with local * files. {@link com.mucommander.commons.file.icon.LocalFileIconProvider} provides transparent creation of local temporary file * to handle non-local files.
* It is also noteworthy that those Swing classes maintain an icon cache. Therefore, local file icons are not cached, * only non-local files (remote protocol or archive entries) have their icons cached to avoid excessive temporary file * creation, using their extension as the cache's key. * * @author Maxence Bernard */ public class SwingFileIconProvider extends CachedFileIconProvider { public SwingFileIconProvider() { super(new SwingFileIconProviderImpl()); } /** * This method forces the initialization of the Swing object that is used to retrieve file icons the first time it * is called, does nothing on subsequent calls. *

* The initialization must be triggered by a thread other than the EventDispatchThread, or a deadlock may happen * on platforms where JFileChooser is used. This method provides to control when and where the initialization occurs. */ public static void forceInit() { SwingFileIconProviderImpl.checkInit(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/icon/impl/SwingFileIconProviderImpl.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.icon.impl; import ch.randelshofer.quaqua.osx.OSXFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.icon.CacheableFileIconProvider; import com.mucommander.commons.file.icon.CachedFileIconProvider; import com.mucommander.commons.file.icon.IconCache; import com.mucommander.commons.file.icon.LocalFileIconProvider; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.commons.io.SilenceableOutputStream; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import lombok.extern.slf4j.Slf4j; import ru.trolsoft.macosx.RetinaImageIcon; import ru.trolsoft.utils.FileUtils; import javax.swing.*; import javax.swing.filechooser.FileSystemView; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.net.URL; /** * Package-protected class which provides the {@link com.mucommander.commons.file.icon.LocalFileIconProvider} and * {@link com.mucommander.commons.file.icon.CacheableFileIconProvider} implementations to {@link SwingFileIconProvider}. * * @see SwingFileIconProvider * @author Maxence Bernard */ @Slf4j class SwingFileIconProviderImpl extends LocalFileIconProvider implements CacheableFileIconProvider { /** Swing object used to retrieve file icons, used on all platforms but Mac OS X */ private static FileSystemView fileSystemView; /** Swing object used to retrieve file icons, used under Mac OS X only */ private static JFileChooser fileChooser; /** Caches icons for directories, used only for non-local files */ private static final IconCache directoryIconCache = CachedFileIconProvider.createCache(); /** Caches icons for regular files, used only for non-local files */ private static final IconCache fileIconCache = CachedFileIconProvider.createCache(); /** True if init has been called */ protected static boolean initialized; /** Name of the 'symlink' icon resource located in the same package as this class */ private final static String SYMLINK_ICON_NAME = "link.png"; /** Icon that is painted over a symlink's target file icon to symbolize a symlink to the target file. */ private static ImageIcon SYMLINK_OVERLAY_ICON; /** Allows stderr to be 'silenced' when needed */ private static SilenceableOutputStream errOut; private static void prepareQuaquaLibraries() { String jarPath = FileUtils.getJarPath(); try { FileUtils.copyFromJarFile("libquaqua.jnilib", jarPath); FileUtils.copyFromJarFile("libquaqua64.dylib", jarPath); FileUtils.copyFromJarFile("libquaqua64.jnilib", jarPath); } catch (IOException e) { log.error("Libraries prepare error", e); } OSXFile.setNativePath(FileUtils.getJarPath() + File.separator); } /** * Initializes the Swing object used to retrieve icons the first time this method is called, does nothing * subsequent calls. * Note: instanciating this object is expensive (I/O bound) so we want to do that only if needed, and only once. */ synchronized static void checkInit() { // This method is synchronized to ensure that the initialization happens only once if (initialized) { return; } if (OsFamily.MAC_OS_X.isCurrent()) { // try to use quaqua OSXFile prepareQuaquaLibraries(); // use ugly JFileChooser if error if (!OSXFile.canWorkWithAliases()) { fileChooser = new JFileChooser(); } } else { fileSystemView = FileSystemView.getFileSystemView(); } // Loads the symlink overlay icon URL iconURL = ResourceLoader.getPackageResourceAsURL(SwingFileIconProviderImpl.class.getPackage(), SYMLINK_ICON_NAME); if (iconURL == null) { throw new RuntimeException("Could not locate required symlink icon: "+SwingFileIconProviderImpl.class.getPackage()+"/" + SYMLINK_ICON_NAME); } SYMLINK_OVERLAY_ICON = new ImageIcon(iconURL); // Replace stderr with a SilenceablePrintStream that can be 'silenced' when needed errOut = new SilenceableOutputStream(System.err, false); System.setErr(new PrintStream(errOut, true)); initialized = true; } /** * Returns an icon for the given java.io.File using the underlying Swing provider component, * null in case of an error. * * @param javaIoFile the file for which to return an icon * @return an icon for the specified file, null in case of an unexpected error */ private static Icon getSwingIcon(File javaIoFile, int preferredSize) { try { if (fileSystemView != null) { // FileSystemView.getSystemIcon() will behave in the following way if the specified file doesn't exist // when the icon is requested: // - throw a NullPointerException (caused by a java.io.FileNotFoundException) => OK why not // - dump the stack trace to System.err => bad! bad! bad! // // A way to workaround this odd behavior would be to test if the file exists when it is requested, // but a/ this is an expensive operation (especially under Windows) and b/ it wouldn't guarantee that // the file effectively exists when the icon is requested. // So the workaround here is to catch exceptions and 'silence' System.err output during the call. errOut.setSilenced(true); return fileSystemView.getSystemIcon(javaIoFile); } else { if (fileChooser == null) { if (RetinaImageIcon.IS_RETINA) { Icon icon = OSXFile.getIcon(javaIoFile, preferredSize*2); if (icon instanceof ImageIcon imageIcon) { return new RetinaImageIcon(imageIcon.getImage()); } } return OSXFile.getIcon(javaIoFile, preferredSize); } return fileChooser.getIcon(javaIoFile); } } catch (Exception e) { log.info("Caught exception while retrieving system icon for file {}", javaIoFile.getAbsolutePath(), e); return null; } finally { if (fileSystemView != null) { errOut.setSilenced(false); } } } /** * Returns an icon symbolizing a symlink to the given target icon. The returned icon uses the specified icon as * its background and overlays a 'link' icon on top of it. * * @param targetFileIcon the icon representing the symlink's target * @return an icon symbolizing a symlink to the given target */ private static ImageIcon getSymlinkIcon(Icon targetFileIcon) { BufferedImage bi = new BufferedImage(targetFileIcon.getIconWidth(), targetFileIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics g = bi.getGraphics(); targetFileIcon.paintIcon(null, g, 0, 0); SYMLINK_OVERLAY_ICON.paintIcon(null, g, 0, 0); return new ImageIcon(bi); } /** * Returns the extension of the given file using {@link AbstractFile#getExtension()}. If the extension is * null, the empty string "" is returned, making the returned extension safe for use * in a hash map where null keys are forbidden. * * @param file file on which to call {@link AbstractFile#getExtension} * @return the file's extension, may be the empty string but never null */ private static String getCheckedExtension(AbstractFile file) { String extension = file.getExtension(); return extension == null ? "" : extension; } /** * Implementation notes: returns false (no caching) for: *

    *
  • local files: their icons are cached by the Swing component that provides icons.
  • *
  • symlinks: their icon cannot be cached using the file's extension as a key.
  • *
* true is returned for non-local files that are not symlinks to avoid excessive temporary file * creation. */ @Override public boolean isCacheable(AbstractFile file, Dimension preferredResolution) { return file != null && !((file.getTopAncestor() instanceof LocalFile) || file.isSymlink()); } @Override public Icon lookupCache(AbstractFile file, Dimension preferredResolution) { // Under Mac OS X, return the icon of /Network for the root of remote (non-local) locations. if (OsFamily.MAC_OS_X.isCurrent() && !FileProtocols.FILE.equals(file.getURL().getScheme()) && file.isRoot()) { return getSwingIcon(new File("/Network"), preferredResolution.width); } // Look for an existing icon instance for the file's extension return (file.isDirectory()? directoryIconCache : fileIconCache).get(getCheckedExtension(file)); } @Override public void addToCache(AbstractFile file, Icon icon, Dimension preferredResolution) { // Map the extension onto the given icon (file.isDirectory() ? directoryIconCache : fileIconCache).put(getCheckedExtension(file), icon); } /** * Implementation note: only one resolution is available (usually 16x16) and blindly returned, the * preferredResolution argument is simply ignored. */ @Override public Icon getLocalFileIcon(LocalFile localFile, AbstractFile originalFile, Dimension preferredResolution) { // Initialize the Swing object the first time this method is called checkInit(); // Retrieve the icon using the Swing provider component Icon icon = getSwingIcon((File)localFile.getUnderlyingFileObject(), preferredResolution.width); // Add a symlink indication to the icon if: // - the original file is a symlink AND // - the original file is not a local file OR // - the original file is a local file but the Swing component generates icons which do not have a symlink // indication. That is the case on Mac OS X 10.5 (regression, 10.4 did this just fine). // // Note that the symlink test is performed last because it is the most expensive. // if ((!(originalFile.getTopAncestor() instanceof LocalFile) || (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrent())) && originalFile.isSymlink()) { icon = icon == null ? null : getSymlinkIcon(icon); } return icon; } public void cleanCache() { directoryIconCache.clear(); fileIconCache.clear(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/CachedFile.java ================================================ package com.mucommander.commons.file.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; import lombok.extern.slf4j.Slf4j; import ru.trolsoft.jni.NativeFileUtils; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; /** * CachedFile is a ProxyFile that caches the return values of most {@link AbstractFile} getter methods. This allows * to limit the number of calls to the underlying file methods which can have a cost since they are often I/O bound. * The methods that are cached are those overridden by this class, except for the ls methods, which are * overridden only to allow recursion (see {@link #CachedFile(com.mucommander.commons.file.AbstractFile, boolean)}). * *

The values are retrieved and cached only when the 'cached methods' are called for the first time; they are * not preemptively retrieved in the constructor, so using this class has no negative impact on performance, * except for the small extra CPU cost added by proxying the methods and the extra RAM used to store cached values. * *

Once the values are retrieved and cached, they never change: the same value will always be returned once a method * has been called for the first time. That means if the underlying file changes (e.g. its size or date has changed), * the changes will not be reflected by this CachedFile. Thus, this class should only be used when a 'real-time' view * of the file is not required, or when the file instance is used only for a small amount of time. * * @author Maxence Bernard */ @Slf4j public class CachedFile extends ProxyFile { // Used to access the java.io.FileSystem#getBooleanAttributes method private static final boolean GET_FILE_ATTRIBUTES_AVAILABLE; private static final Method M_GET_BOOLEAN_ATTRIBUTES; private static final int BA_DIRECTORY, BA_EXISTS, BA_HIDDEN; private static final Object FS; private static final boolean NATIVE_FILE_UTILS_AVAILABLE = OsFamily.MAC_OS_X.isCurrent() && NativeFileUtils.init(); // set-flags private static final int SIZE_SET_MASK = 1; private static final int LAST_MODIFICATION_SET_MASK = 1 << 1; private static final int SYMLINK_SET_MASK = 1 << 2; private static final int DIRECTORY_SET_MASK = 1 << 3; private static final int ARCHIVE_SET_MASK = 1 << 4; private static final int EXECUTABLE_SET_MASK = 1 << 5; private static final int HIDDEN_SET_MASK = 1 << 6; private static final int ABSOLUTE_PATH_SET_MASK = 1 << 7; private static final int CANONICAL_PATH_SET_MASK = 1 << 8; private static final int EXTENSION_SET_MASK = 1 << 9; private static final int NAME_SET_MASK = 1 << 10; private static final int FREE_SPACE_SET_MASK = 1 << 11; private static final int TOTAL_SPACE_SET_MASK = 1 << 12; private static final int EXISTS_SET_MASK = 1 << 13; private static final int PERMISSIONS_SET_MASK = 1 << 14; private static final int PERMISSIONS_STRING_SET_MASK = 1 << 15; private static final int OWNER_SET_MASK = 1 << 16; private static final int GROUP_SET_MASK = 1 << 17; private static final int IS_ROOT_SET_MASK = 1 << 18; private static final int PARENT_SET_MASK = 1 << 19; private static final int GET_ROOT_SET_MASK = 1 << 20; private static final int CANONICAL_FILE_SET_MASK = 1 << 21; private static final int CREATION_DATE_SET_MASK = 1 << 22; private static final int LAST_ACCESS_SET_MASK = 1 << 23; // boolean values private static final int SYMLINK_VALUE_MASK = 1 << 24; private static final int DIRECTORY_VALUE_MASK = 1 << 25; private static final int ARCHIVE_VALUE_MASK = 1 << 26; private static final int EXECUTABLE_VALUE_MASK = 1 << 27; private static final int HIDDEN_VALUE_MASK = 1 << 28; private static final int EXISTS_VALUE_MASK = 1 << 29; private static final int IS_ROOT_VALUE_MASK = 1 << 30; /** If true, AbstractFile instances returned by this class will be wrapped into CachedFile instances */ private static final int RECURSE_INSTANCES_MASK = 1 << 29; /** * All boolean values stored here as bits */ private int bitmask; /////////////////// // Cached values // /////////////////// private long getSize; private long getLastModified; private long getCreationDate; private long getLastAccessDate; private String getAbsolutePath; private String getCanonicalPath; private String getExtension; private String getName; private long getFreeSpace; private long getTotalSpace; private FilePermissions getPermissions; private String getPermissionsString; private String getOwner; private String getGroup; private AbstractFile getParent; private AbstractFile getRoot; private AbstractFile getCanonicalFile; /** * Cached values for isFileOperationSupported method */ private int supportedOperationsValuesMask; private int supportedOperationsCachedMask; static { // Exposes the java.io.FileSystem class which by default has package access, in order to use its // 'getBooleanAttributes' method to speed up access to file attributes under Windows. // This method allows to retrieve the values of the 'exists', 'isDirectory' and 'isHidden' attributes in one // pass, resolving the underlying file only once instead of 3 times. Since resolving a file is a particularly // expensive operation under Windows due to improper use of the Win32 API, this helps speed things up a little. // References: // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5036988 // - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6240028 // // This hack was made for Windows, but is now used for other platforms as well as it is necessarily faster than // retrieving file attributes individually. boolean getFileAttributesAvailable; Method mGetBooleanAttributes; int baExists, baDirectory, baHidden; Object fs; if (NATIVE_FILE_UTILS_AVAILABLE) { getFileAttributesAvailable = true; mGetBooleanAttributes = null; baExists = 0; baDirectory = 0; baHidden = 0; fs = null; } else { try { // Resolve FileSystem class, 'getBooleanAttributes' method and fields Class cFile = File.class; Class cFileSystem = Class.forName("java.io.FileSystem"); mGetBooleanAttributes = cFileSystem.getDeclaredMethod("getBooleanAttributes", cFile); Field fBA_EXISTS = cFileSystem.getDeclaredField("BA_EXISTS"); Field fBA_DIRECTORY = cFileSystem.getDeclaredField("BA_DIRECTORY"); Field fBA_HIDDEN = cFileSystem.getDeclaredField("BA_HIDDEN"); Field fFs; try { fFs = cFile.getDeclaredField("FS"); } catch (NoSuchFieldException e) { fFs = cFile.getDeclaredField("fs"); } // Allow access to the 'getBooleanAttributes' method and to the fields we're interested in mGetBooleanAttributes.setAccessible(true); fFs.setAccessible(true); fBA_EXISTS.setAccessible(true); fBA_DIRECTORY.setAccessible(true); fBA_HIDDEN.setAccessible(true); // Retrieve constant field values once for all baExists = (Integer) fBA_EXISTS.get(null); baDirectory = (Integer) fBA_DIRECTORY.get(null); baHidden = (Integer) fBA_HIDDEN.get(null); fs = fFs.get(null); getFileAttributesAvailable = true; log.trace("Access to java.io.FileSystem granted"); } catch (Exception e) { getFileAttributesAvailable = false; mGetBooleanAttributes = null; baExists = 0; baDirectory = 0; baHidden = 0; fs = null; log.info("Error while allowing access to java.io.FileSystem", e); } } GET_FILE_ATTRIBUTES_AVAILABLE = getFileAttributesAvailable; M_GET_BOOLEAN_ATTRIBUTES = mGetBooleanAttributes; BA_EXISTS = baExists; BA_DIRECTORY = baDirectory; BA_HIDDEN = baHidden; FS = fs; } /** * Creates a new CachedFile instance around the specified AbstractFile, caching returned values of cached methods * as they are called. If recursion is enabled, the methods returning AbstractFile will return CachedFile instances, * allowing the cache files recursively. * * @param file the AbstractFile instance for which returned values of getter methods should be cached * @param recursiveInstances if true, AbstractFile instances returned by this class will be wrapped into CachedFile instances */ public CachedFile(AbstractFile file, boolean recursiveInstances) { super(file); if (recursiveInstances) { bitmask |= RECURSE_INSTANCES_MASK; } } /** * Creates a CachedFile instance for each of the AbstractFile instances in the given array. */ private AbstractFile[] createCachedFiles(AbstractFile[] files) { int nbFiles = files.length; for (int i = 0; i < nbFiles; i++) { files[i] = new CachedFile(files[i], true); } return files; } /** * Pre-fetches values of {@link #isDirectory}, {@link #exists} and {@link #isHidden} for the given local file, * using the java.io.FileSystem#getBooleanAttributes(java.io.File) method. * The given {@link AbstractFile} must be a local file or a proxy to a local file ('file' protocol). This method * must only be called if the {@link #GET_FILE_ATTRIBUTES_AVAILABLE} field is true. */ private void getFileAttributes(AbstractFile file) { file = file.getTopAncestor(); if (file instanceof LocalFile) { if (NATIVE_FILE_UTILS_AVAILABLE) { initAttributesNativeCall(file); } else { initAttributesReflectionCall(file); } } } private void initAttributesReflectionCall(AbstractFile file) { try { int ba = (Integer) M_GET_BOOLEAN_ATTRIBUTES.invoke(FS, file.getUnderlyingFileObject()); if ((ba & BA_DIRECTORY) != 0) { bitmask |= DIRECTORY_VALUE_MASK; } else { bitmask &= ~DIRECTORY_VALUE_MASK; } if ((ba & BA_EXISTS) != 0) { bitmask |= EXISTS_VALUE_MASK; } else { bitmask &= ~EXISTS_VALUE_MASK; } if ((ba & BA_HIDDEN) != 0) { bitmask |= HIDDEN_VALUE_MASK; } else { bitmask &= ~HIDDEN_VALUE_MASK; } bitmask |= DIRECTORY_SET_MASK | HIDDEN_SET_MASK | EXISTS_SET_MASK; } catch (Exception e) { log.info("Could not retrieve file attributes for {}", file, e); } } private void initAttributesNativeCall(AbstractFile file) { int attr = NativeFileUtils.getLocalFileAttributes(file.getAbsolutePath()); if ((attr & NativeFileUtils.FA_MASK_DIRECTORY) != 0) { bitmask |= DIRECTORY_VALUE_MASK; } else { bitmask &= ~DIRECTORY_VALUE_MASK; } if ((attr & NativeFileUtils.FA_MASK_EXISTS) != 0) { bitmask |= EXISTS_VALUE_MASK; } else { bitmask &= ~EXISTS_VALUE_MASK; } if ((attr & NativeFileUtils.FA_MASK_HIDDEN) != 0) { bitmask |= HIDDEN_VALUE_MASK; } else { bitmask &= ~HIDDEN_VALUE_MASK; } bitmask |= DIRECTORY_SET_MASK | HIDDEN_SET_MASK | EXISTS_SET_MASK; } //////////////////////////////////////////////////// // Overridden methods to cache their return value // //////////////////////////////////////////////////// @Override public long getSize() { if ((bitmask & SIZE_SET_MASK) == 0) { getSize = file.getSize(); bitmask |= SIZE_SET_MASK; } return getSize; } @Override public long getLastModifiedDate() { if ((bitmask & LAST_MODIFICATION_SET_MASK) == 0) { getLastModified = file.getLastModifiedDate(); bitmask |= LAST_MODIFICATION_SET_MASK; } return getLastModified; } @Override public long getCreationDate() throws IOException { if ((bitmask & CREATION_DATE_SET_MASK) == 0) { getCreationDate = file.getCreationDate(); bitmask |= CREATION_DATE_SET_MASK; } return getCreationDate; } @Override public long getLastAccessDate() throws IOException { if ((bitmask & LAST_ACCESS_SET_MASK) == 0) { getLastAccessDate = file.getLastAccessDate(); bitmask |= LAST_ACCESS_SET_MASK; } return getLastAccessDate; } @Override public boolean isSymlink() { if ((bitmask & SYMLINK_SET_MASK) == 0) { if (file.isSymlink()) { bitmask |= SYMLINK_VALUE_MASK; } else { bitmask &= ~SYMLINK_VALUE_MASK; } bitmask |= SYMLINK_SET_MASK; } return (bitmask & SYMLINK_VALUE_MASK) != 0; } @Override public boolean isDirectory() { if ((bitmask & DIRECTORY_SET_MASK) == 0) { if (isFileAttributesSupported()) { getFileAttributes(file); } // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again if ((bitmask & DIRECTORY_SET_MASK) == 0) { if (file.isDirectory()) { bitmask |= DIRECTORY_VALUE_MASK; } else { bitmask &= ~DIRECTORY_VALUE_MASK; } bitmask |= DIRECTORY_SET_MASK; } } return (bitmask & DIRECTORY_VALUE_MASK) != 0; } private boolean isFileAttributesSupported() { return GET_FILE_ATTRIBUTES_AVAILABLE && FileProtocols.FILE.equals(file.getURL().getScheme()); } @Override public boolean isArchive() { if ((bitmask & ARCHIVE_SET_MASK) == 0) { if (file.isArchive()) { bitmask |= ARCHIVE_VALUE_MASK; } else { bitmask &= ~ARCHIVE_VALUE_MASK; } bitmask |= ARCHIVE_SET_MASK; } return (bitmask & ARCHIVE_VALUE_MASK) != 0; } @Override public boolean isHidden() { if ((bitmask & HIDDEN_SET_MASK) == 0) { if (isFileAttributesSupported()) { getFileAttributes(file); } // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again if ((bitmask & HIDDEN_SET_MASK) == 0) { if (file.isHidden()) { bitmask |= HIDDEN_VALUE_MASK; } else { bitmask &= ~HIDDEN_VALUE_MASK; } bitmask |= HIDDEN_SET_MASK; } } return (bitmask & HIDDEN_VALUE_MASK) != 0; } @Override public boolean isExecutable() { if ((bitmask & EXECUTABLE_SET_MASK) == 0) { if (file.isExecutable()) { bitmask |= EXECUTABLE_VALUE_MASK; } else { bitmask &= ~EXECUTABLE_VALUE_MASK; } bitmask |= EXECUTABLE_SET_MASK; } return (bitmask & EXECUTABLE_VALUE_MASK) != 0; } @Override public String getAbsolutePath() { if ((bitmask & ABSOLUTE_PATH_SET_MASK) == 0) { getAbsolutePath = file.getAbsolutePath(); bitmask |= ABSOLUTE_PATH_SET_MASK; } return getAbsolutePath; } @Override public String getCanonicalPath() { if ((bitmask & CANONICAL_PATH_SET_MASK) == 0) { getCanonicalPath = file.getCanonicalPath(); bitmask |= CANONICAL_PATH_SET_MASK; } return getCanonicalPath; } @Override public String getExtension() { if ((bitmask & EXTENSION_SET_MASK) == 0) { getExtension = file.getExtension(); bitmask |= EXTENSION_SET_MASK; } return getExtension; } @Override public String getName() { if ((bitmask & NAME_SET_MASK) == 0) { getName = file.getName(); bitmask |= NAME_SET_MASK; } return getName; } @Override public long getFreeSpace() throws IOException { if ((bitmask & FREE_SPACE_SET_MASK) == 0) { getFreeSpace = file.getFreeSpace(); bitmask |= FREE_SPACE_SET_MASK; } return getFreeSpace; } @Override public long getTotalSpace() throws IOException { if ((bitmask & TOTAL_SPACE_SET_MASK) == 0) { getTotalSpace = file.getTotalSpace(); bitmask |= TOTAL_SPACE_SET_MASK; } return getTotalSpace; } @Override public boolean exists() { if ((bitmask & EXISTS_SET_MASK) == 0) { if (isFileAttributesSupported()) { getFileAttributes(file); } // Note: getFileAttributes() might fail to retrieve file attributes, so we need to test isDirectorySet again if ((bitmask & EXISTS_SET_MASK) == 0) { if (file.exists()) { bitmask |= EXISTS_VALUE_MASK; } else { bitmask &= ~EXISTS_VALUE_MASK; } bitmask |= EXISTS_SET_MASK; } } return (bitmask & EXISTS_VALUE_MASK) != 0; } @Override public FilePermissions getPermissions() { if ((bitmask & PERMISSIONS_SET_MASK) == 0) { getPermissions = file.getPermissions(); bitmask |= PERMISSIONS_SET_MASK; } return getPermissions; } @Override public String getPermissionsString() { if ((bitmask & PERMISSIONS_STRING_SET_MASK) == 0) { getPermissionsString = file.getPermissionsString(); bitmask |= PERMISSIONS_STRING_SET_MASK; } return getPermissionsString; } @Override public String getOwner() { if ((bitmask & OWNER_SET_MASK) == 0) { getOwner = file.getOwner(); bitmask |= OWNER_SET_MASK; } return getOwner; } @Override public String getGroup() { if ((bitmask & GROUP_SET_MASK) == 0) { getGroup = file.getGroup(); bitmask |= GROUP_SET_MASK; } return getGroup; } @Override public boolean isRoot() { if ((bitmask & IS_ROOT_SET_MASK) == 0) { if (file.isRoot()) { bitmask |= IS_ROOT_VALUE_MASK; } else { bitmask &= ~IS_ROOT_VALUE_MASK; } bitmask |= IS_ROOT_SET_MASK; } return (bitmask & IS_ROOT_VALUE_MASK) != 0; } @Override public AbstractFile getParent() { if ((bitmask & PARENT_SET_MASK) == 0) { getParent = file.getParent(); // create a CachedFile instance around the file if recursion is enabled if ((bitmask & RECURSE_INSTANCES_MASK) != 0 && getParent != null) { getParent = new CachedFile(getParent, true); } bitmask |= PARENT_SET_MASK; } return getParent; } @Override public AbstractFile getRoot() { if ((bitmask & GET_ROOT_SET_MASK) == 0) { getRoot = file.getRoot(); // create a CachedFile instance around the file if recursion is enabled if ((bitmask & RECURSE_INSTANCES_MASK) != 0) { getRoot = new CachedFile(getRoot, true); } bitmask |= GET_ROOT_SET_MASK; } return getRoot; } @Override public AbstractFile getCanonicalFile() { if ((bitmask & CANONICAL_FILE_SET_MASK) == 0) { getCanonicalFile = file.getCanonicalFile(); // create a CachedFile instance around the file if recursion is enabled if ((bitmask & RECURSE_INSTANCES_MASK) != 0) { // AbstractFile#getCanonicalFile() may return 'this' if the file is not a symlink. In that case, // no need to create a new CachedFile, simply use this one. if (getCanonicalFile == file) { getCanonicalFile = this; } else { getCanonicalFile = new CachedFile(getCanonicalFile, true); } } bitmask |= CANONICAL_FILE_SET_MASK; } return getCanonicalFile; } @Override public AbstractFile[] ls() throws IOException { // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled AbstractFile[] files = file.ls(); if ((bitmask & RECURSE_INSTANCES_MASK) != 0) { return createCachedFiles(files); } return files; } @Override public AbstractFile[] ls(FileFilter filter) throws IOException { // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled AbstractFile[] files = file.ls(filter); if ((bitmask & RECURSE_INSTANCES_MASK) != 0) { return createCachedFiles(files); } return files; } @Override public AbstractFile[] ls(FilenameFilter filter) throws IOException { // Don't cache ls() result but create a CachedFile instance around each of the files if recursion is enabled AbstractFile[] files = file.ls(filter); if ((bitmask & RECURSE_INSTANCES_MASK) != 0) { return createCachedFiles(files); } return files; } @Override public boolean isFileOperationSupported(FileOperation op) { int bitMask = 1 << op.ordinal(); if ((supportedOperationsCachedMask & bitMask) != 0) { return (supportedOperationsValuesMask & bitMask) != 0; } boolean result = super.isFileOperationSupported(op); if (result) { supportedOperationsValuesMask |= bitMask; } supportedOperationsCachedMask |= bitMask; return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/DebugFile.java ================================================ package com.mucommander.commons.file.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FilePermissions; import java.io.IOException; import java.util.Random; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * DebugFile is a {@link ProxyFile} to be used for debugging purposes. It allows to track the calls made to * {@link com.mucommander.commons.file.AbstractFile} methods that are commonly I/O-bound, by logging calls to each of those * methods. It also allows to slow those methods down to simulate a slow filesystem. * * @author Maxence Bernard */ public class DebugFile extends ProxyFile { private static final Logger LOGGER = LoggerFactory.getLogger(DebugFile.class); /** Maximum latency in milliseconds */ private int maxLatency; /** Used to randomize the latency for each calls to slowed-down methods */ private static final Random random = new Random(); /** * Creates a DebugFile that proxies the calls made to the given AbstractFile's methods, with no latency. * * @param file the AbstractFile to proxy and debug */ public DebugFile(AbstractFile file) { this(file, 0); } /** * Creates a DebugFile that proxies the calls made to the given AbstractFile and slows those methods down by * simulating latency by making I/O bound methods wait. * * @param file the AbstractFile to proxy and debug * @param maxLatency the maximum amount of latency in milliseconds */ public DebugFile(AbstractFile file, int maxLatency) { super(file); this.maxLatency = maxLatency; } /** * Sets the the maximum amount of latency in milliseconds to add to calls made to IO-bound AbstractFile methods * (i.e. those that are overridden by this class). The latency is randomized for each method call and uniformly * distributed, the specified value serving as the maximum. * * @param maxLatency the maximum amount of latency in milliseconds to add to IO-bound AbstractFile method calls * (those overridden by this class). */ public void setMaxLatency(int maxLatency) { this.maxLatency = maxLatency; } /** * Sleeps a random number of milliseconds, up to {@link #maxLatency}. */ private void lag() { if (maxLatency > 0) { try { Thread.sleep(random.nextInt(maxLatency)); } catch(InterruptedException ignored) {} } } /** * Returns the debug string printed for all calls made to the AbstractFile methods overridden by this class. */ private String getDebugString() { return "called on "+super.getAbsolutePath()+" ("+file.getClass().getName()+")"; } @Override public long getLastModifiedDate() { LOGGER.trace(getDebugString()); lag(); return super.getLastModifiedDate(); } @Override public long getSize() { LOGGER.trace(getDebugString()); lag(); return super.getSize(); } @Override public boolean exists() { LOGGER.trace(getDebugString()); lag(); return super.exists(); } @Override public boolean isDirectory() { LOGGER.trace(getDebugString()); lag(); return super.isDirectory(); } @Override public boolean isSymlink() { LOGGER.trace(getDebugString()); lag(); return super.isSymlink(); } @Override public long getFreeSpace() throws IOException { LOGGER.trace(getDebugString()); lag(); return super.getFreeSpace(); } @Override public long getTotalSpace() throws IOException { LOGGER.trace(getDebugString()); lag(); return super.getTotalSpace(); } @Override public String getName() { LOGGER.trace(getDebugString()); lag(); return super.getName(); } @Override public String getExtension() { LOGGER.trace(getDebugString()); lag(); return super.getExtension(); } @Override public String getAbsolutePath() { LOGGER.trace(getDebugString()); lag(); return super.getAbsolutePath(); } @Override public String getCanonicalPath() { LOGGER.trace(getDebugString()); lag(); return super.getCanonicalPath(); } @Override public AbstractFile getCanonicalFile() { LOGGER.trace(getDebugString()); lag(); return super.getCanonicalFile(); } @Override public boolean isArchive() { LOGGER.trace(getDebugString()); lag(); return super.isArchive(); } @Override public boolean isHidden() { LOGGER.trace(getDebugString()); lag(); return super.isHidden(); } @Override public FilePermissions getPermissions() { LOGGER.trace(getDebugString()); lag(); return super.getPermissions(); } @Override public String getOwner() { LOGGER.trace(getDebugString()); lag(); return super.getOwner(); } @Override public String getGroup() { LOGGER.trace(getDebugString()); lag(); return super.getGroup(); } @Override public AbstractFile getRoot() { LOGGER.trace(getDebugString()); lag(); return super.getRoot(); } @Override public boolean isRoot() { LOGGER.trace(getDebugString()); lag(); return super.isRoot(); } @Override public boolean equalsCanonical(Object f) { LOGGER.trace(getDebugString()); lag(); return super.equals(f); } public String toString() { LOGGER.trace(getDebugString()); lag(); return super.toString(); } @Override public AbstractFile getParent() { LOGGER.trace(getDebugString()); lag(); return super.getParent(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ProxyFile.java ================================================ package com.mucommander.commons.file.impl; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PushbackInputStream; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; /** * ProxyFile is an {@link AbstractFile} that acts as a proxy between the class that extends it * and the proxied AbstractFile instance specified to the constructor. * All AbstractFile public methods (abstract or not) are delegated to the proxied file. * The {@link #getProxiedFile()} method allows to retrieve the proxied file instance. * *

This class is useful for wrapper files, such as {@link com.mucommander.commons.file.AbstractArchiveFile archive files}, * that provide additional functionality over an existing AbstractFile instance (the proxied file). * By implementing/overriding every AbstractFile methods, ProxyFile ensures that * all AbstractFile methods can safely be used, even if they are overridden by the proxied * file instance's class. * *

Implementation note: the java.lang.reflect.Proxy class can unfortunately not be * used as it only works with interfaces (not abstract class). There doesn't seem to be any dynamic way to * proxy method invocations, so any modifications made to {@link com.mucommander.commons.file.AbstractFile} must be also * reflected in ProxyFile. * * @see com.mucommander.commons.file.AbstractArchiveFile * @author Maxence Bernard */ public abstract class ProxyFile extends AbstractFile { private static Logger logger; /** The proxied file instance */ protected AbstractFile file; /** * Creates a new ProxyFile using the given file to delegate AbstractFile method calls to. * * @param file the file to be proxied */ public ProxyFile(AbstractFile file) { super(file.getURL()); this.file = file; } /** * Returns the AbstractFile instance proxied by this ProxyFile. * * @return the AbstractFile instance proxied by this ProxyFile */ public AbstractFile getProxiedFile() { return file; } @Override public long getLastModifiedDate() { return file.getLastModifiedDate(); } @Override public void setLastModifiedDate(long lastModified) throws IOException { file.setLastModifiedDate(lastModified); } @Override public long getSize() { return file.getSize(); } @Override public AbstractFile getParent() { return file.getParent(); } @Override public void setParent(AbstractFile parent) { file.setParent(parent); } @Override public boolean exists() { return file.exists(); } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { file.changePermission(access, permission, enabled); } @Override public String getOwner() { return file.getOwner(); } @Override public boolean canGetOwner() { return file.canGetOwner(); } @Override public String getGroup() { return file.getGroup(); } @Override public boolean canGetGroup() { return file.canGetGroup(); } @Override public boolean isDirectory() { return file.isDirectory(); } @Override public boolean isSymlink() { return file.isSymlink(); } @Override public boolean isSystem() { return file.isSystem(); } @Override public AbstractFile[] ls() throws IOException { return file.ls(); } @Override public void mkdir() throws IOException { file.mkdir(); } @Override public InputStream getInputStream() throws IOException { return file.getInputStream(); } @Override public OutputStream getOutputStream() throws IOException { return file.getOutputStream(); } @Override public OutputStream getAppendOutputStream() throws IOException { return file.getAppendOutputStream(); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return file.getRandomAccessInputStream(); } @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return file.getRandomAccessOutputStream(); } @Override public void delete() throws IOException { file.delete(); } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { file.copyRemotelyTo(destFile); } @Override public void renameTo(AbstractFile destFile) throws IOException { file.renameTo(destFile); } @Override public long getFreeSpace() throws IOException { return file.getFreeSpace(); } @Override public long getTotalSpace() throws IOException { return file.getTotalSpace(); } @Override public Object getUnderlyingFileObject() { return file.getUnderlyingFileObject(); } ///////////////////////////////////// // Overridden AbstractFile methods // ///////////////////////////////////// @Override public boolean isFileOperationSupported(FileOperation op) { Class thisClass = getClass(); Method opMethod = op.getCorrespondingMethod(thisClass); // If the method corresponding to the file operation has been overridden by this class (a ProxyFile subclass), // check the presence of the UnsupportedFileOperation annotation in this class. try { if (!thisClass.getMethod(opMethod.getName(), opMethod.getParameterTypes()).getDeclaringClass().equals(ProxyFile.class)) { return AbstractFile.isFileOperationSupported(op, thisClass); } } catch(Exception e) { // Should never happen, unless AbstractFile method signatures have changed and FileOperation has not been updated getLogger().warn("Exception caught, this should not have happened", e); } // Otherwise, check for the presence of the UnsupportedFileOperation annotation in the wrapped AbstractFile. return file.isFileOperationSupported(op); } @Override public FileURL getURL() { return file.getURL(); } @Override public URL getJavaNetURL() throws MalformedURLException { return file.getJavaNetURL(); } @Override public String getName() { return file.getName(); } @Override public String getExtension() { return file.getExtension(); } @Override public String getBaseName() { return file.getBaseName(); } @Override public String getAbsolutePath() { return file.getAbsolutePath(); } @Override public String getCanonicalPath() { return file.getCanonicalPath(); } @Override public AbstractFile getCanonicalFile() { return file.getCanonicalFile(); } @Override public String getSeparator() { return file.getSeparator(); } @Override public boolean isArchive() { return file.isArchive(); } @Override public boolean isHidden() { return file.isHidden(); } @Override public FilePermissions getPermissions() { return file.getPermissions(); } @Override public void changePermissions(int permissions) throws IOException { file.changePermissions(permissions); } @Override public PermissionBits getChangeablePermissions() { return file.getChangeablePermissions(); } @Override public String getPermissionsString() { return file.getPermissionsString(); } @Override public AbstractFile getRoot() { return file.getRoot(); } @Override public boolean isRoot() { return file.isRoot(); } @Override public AbstractFile getVolume() { return file.getVolume(); } @Override public InputStream getInputStream(long offset) throws IOException { return file.getInputStream(offset); } @Override public void copyStream(InputStream in, boolean append, long length) throws FileTransferException { file.copyStream(in, append, length); } @Override public AbstractFile[] ls(FileFilter filter) throws IOException { return file.ls(filter); } @Override public AbstractFile[] ls(FilenameFilter filter) throws IOException { return file.ls(filter); } @Override public void mkfile() throws IOException { file.mkfile(); } @Override public void deleteRecursively() throws IOException { file.deleteRecursively(); } @Override public boolean equals(Object f) { return file.equals(f); } @Override public boolean equalsCanonical(Object f) { return file.equalsCanonical(f); } public int hashCode() { return file.hashCode(); } public String toString() { return file.toString(); } @Override public short getReplication() throws UnsupportedFileOperationException { return file.getReplication(); } @Override public long getBlocksize() throws UnsupportedFileOperationException { return file.getBlocksize(); } @Override public void changeReplication(short replication) throws IOException { file.changeReplication(replication); } @Override public boolean isExecutable() { return file.isExecutable(); } @Override public PushbackInputStream getPushBackInputStream(int bufferSize) throws IOException { return file.getPushBackInputStream(bufferSize); } @Override public void closePushbackInputStream() throws IOException { file.closePushbackInputStream(); } @Override public boolean isLocalFile() { return super.isLocalFile(); } @Override public long getCreationDate() throws IOException { return super.getCreationDate(); } @Override public long getLastAccessDate() throws IOException { return super.getLastAccessDate(); } @Override public boolean canRead() { return file.canRead(); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ProxyFile.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/SevenZipJBindingROArchiveFile.java ================================================ package com.mucommander.commons.file.impl; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.sevenzip.SevenZipArchiveFile.ExtractCallback; import com.mucommander.commons.file.impl.sevenzip.SignatureCheckedRandomAccessFile; import com.mucommander.commons.file.impl.sevenzip.multivolume.InArchiveWrapper; import com.mucommander.commons.file.impl.sevenzip.multivolume.SevenZipMultiVolumeCallbackHandler; import com.mucommander.commons.file.impl.sevenzip.multivolume.SevenZipRarMultiVolumeCallbackHandler; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.util.CircularByteBuffer; import net.sf.sevenzipjbinding.*; import net.sf.sevenzipjbinding.impl.VolumedArchiveInStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Pattern; public class SevenZipJBindingROArchiveFile extends AbstractROArchiveFile { private static final Logger LOGGER = LoggerFactory.getLogger(SevenZipJBindingROArchiveFile.class); private static final Pattern MULTI_PART_RAR_PATTERN = Pattern.compile("[.]part\\d+[.]rar"); private static final String MULTI_PART_7Z_EXT = ".7z.001"; private static volatile boolean libraryInit = false; protected IInArchive inArchive; private ArchiveFormat sevenZipJBindingFormat; private final SevenZipArchiveFormatDetector formatDetector; private final byte[] formatSignature; /** * Creates an AbstractROArchiveFile on top of the given file. * * @param file the file on top of which to create the archive * * @see * ArchiveFormat */ public SevenZipJBindingROArchiveFile(AbstractFile file, ArchiveFormat sevenZipJBindingFormat, byte[] formatSignature) { super(file); this.sevenZipJBindingFormat = sevenZipJBindingFormat; this.formatSignature = formatSignature; this.formatDetector = null; initSevenZipBindings(); } public SevenZipJBindingROArchiveFile(AbstractFile file, SevenZipArchiveFormatDetector formatDetector) { super(file); this.sevenZipJBindingFormat = null; this.formatSignature = new byte[] {}; this.formatDetector = formatDetector; initSevenZipBindings(); } private static void initSevenZipBindings() { if (!libraryInit) { synchronized (SevenZipJBindingROArchiveFile.class) { try { if (OsFamily.getCurrent() == OsFamily.MAC_OS_X && OsFamily.isAarch64()) { SevenZip.initSevenZipFromPlatformJAR("Mac-arm64"); } else if (OsFamily.getCurrent() == OsFamily.MAC_OS_X) { SevenZip.initSevenZipFromPlatformJAR("Mac-x86_64"); } else if (OsFamily.getCurrent() == OsFamily.LINUX && OsFamily.isAmd64()) { SevenZip.initSevenZipFromPlatformJAR("Linux-amd64"); } else if (OsFamily.getCurrent() == OsFamily.LINUX) { SevenZip.initSevenZipFromPlatformJAR("Linux-i386"); } else { System.out.println("Arch: " + System.getProperty("os.arch")); SevenZip.initSevenZipFromPlatformJAR(); } libraryInit = true; } catch (SevenZipNativeInitializationException ex) { throw new RuntimeException("Unable to init 7-Zip-JBinding library bindings", ex); } } } } // private IInArchive openInArchive() throws IOException { // if (inArchive == null) { // if (formatDetector != null) { // sevenZipJBindingFormat = formatDetector.detect(file); // } // SignatureCheckedRandomAccessFile in = new SignatureCheckedRandomAccessFile(file, formatSignature); // inArchive = SevenZip.openInArchive(sevenZipJBindingFormat, in); // } // return inArchive; // } /** * Open the file and check its signature compared to the one provided in {@link #SevenZipJBindingROArchiveFile(AbstractFile, ArchiveFormat, byte[])} * @return this {@code SevenZipJBindingROArchiveFile} instance when file signature matches the specified signature * @throws IOException in case the file cannot be opened or its signature differs from the specified signature */ public SevenZipJBindingROArchiveFile check() throws IOException { openInArchive(); return this; } private IInArchive openInArchive() throws IOException { if (inArchive == null) { boolean multiPartRar = MULTI_PART_RAR_PATTERN.matcher(file.getName()).find(); boolean multiPartSevenZip = file.getName().toLowerCase().endsWith(MULTI_PART_7Z_EXT); // if (formatDetector != null) { // sevenZipJBindingFormat = formatDetector.detect(file); // } // SignatureCheckedRandomAccessFile in = new SignatureCheckedRandomAccessFile(file, formatSignature); // inArchive = SevenZip.openInArchive(sevenZipJBindingFormat, in); if (multiPartRar) { SevenZipRarMultiVolumeCallbackHandler handler = new SevenZipRarMultiVolumeCallbackHandler(formatSignature, password); IInStream firstStream = handler.getStream(file.getAbsolutePath()); IInArchive tmpInArchive = SevenZip.openInArchive(sevenZipJBindingFormat, firstStream, handler); inArchive = new InArchiveWrapper(tmpInArchive, handler); } else if (multiPartSevenZip) { SevenZipMultiVolumeCallbackHandler handler = new SevenZipMultiVolumeCallbackHandler(formatSignature, file, password); IInArchive tmpInArchive = SevenZip.openInArchive(sevenZipJBindingFormat, new VolumedArchiveInStream(handler)); if (isEnc(tmpInArchive) && password == null) { // Throwing this exception to trigger password dialog throw new IOException(String.format("Password protected file but password is null [file = %s]", file.getName())); } inArchive = new InArchiveWrapper(tmpInArchive, handler); } else { SignatureCheckedRandomAccessFile in = new SignatureCheckedRandomAccessFile(file, formatSignature); IInArchive tmpInArchive = SevenZip.openInArchive(sevenZipJBindingFormat, in, password); inArchive = new InArchiveWrapper(tmpInArchive, in); } } return inArchive; } private boolean isEnc(IInArchive archive) { try { if (Boolean.TRUE.equals(archive.getArchiveProperty(PropID.ENCRYPTED))) { return true; } for (int i = 0; i < archive.getNumberOfItems(); i++) { if (Boolean.TRUE.equals(archive.getProperty(i, PropID.ENCRYPTED))) { return true; } } } catch (SevenZipException e) { LOGGER.error("Error checking if file is encrypted", e); } return false; } @Override public ArchiveEntryIterator getEntryIterator() throws IOException { try { final IInArchive sevenZipFile = openInArchive(); int nbEntries = sevenZipFile.getNumberOfItems(); List entries = new ArrayList<>(); for (int i = 0; i < nbEntries; i++) { entries.add(createArchiveEntry(i)); } return new WrapperArchiveEntryIterator(entries.iterator()); } catch (SevenZipException e) { throw new IOException(e); } finally { try { if (inArchive != null) { inArchive.close(); } } catch (SevenZipException e) { LOGGER.error("Error closing archive", e); } inArchive = null; } } @Override public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) { final int[] in = new int[1]; in[0] = (Integer)entry.getEntryObject(); final CircularByteBuffer cbb = new CircularByteBuffer(CircularByteBuffer.INFINITE_SIZE); new Thread(() -> { synchronized (SevenZipJBindingROArchiveFile.this) { try { final IInArchive sevenZipFile = openInArchive(); sevenZipFile.extract(in, false, new ExtractCallback(inArchive, cbb.getOutputStream())); } catch (IOException e) { LOGGER.error("Can't open archive", e); } finally { if (inArchive != null) { try { inArchive.close(); } catch (SevenZipException e) { LOGGER.error("Can't close archive", e); } } try { cbb.getOutputStream().close(); } catch (IOException e) { LOGGER.error("Can't close outputstream", e); } inArchive = null; } } }).start(); return cbb.getInputStream(); } /** * Creates and return an {@link ArchiveEntry()} whose attributes are fetched from the given {@link com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZipEntry} * * @param i the index of entry * @return an ArchiveEntry whose attributes are fetched from the given SevenZipEntry */ private ArchiveEntry createArchiveEntry(int i) throws IOException { final IInArchive sevenZipFile = openInArchive(); String path = sevenZipFile.getStringProperty(i, PropID.PATH); boolean isDirectory = (Boolean)sevenZipFile.getProperty(i, PropID.IS_FOLDER); Date time = (Date) sevenZipFile.getProperty(i, PropID.LAST_MODIFICATION_TIME); Long size = (Long) sevenZipFile.getProperty(i, PropID.SIZE); if (org.apache.commons.lang.StringUtils.isEmpty(path)) { path = file.getNameWithoutExtension(); } path = path.replace(File.separatorChar, ArchiveEntry.SEPARATOR_CHAR); ArchiveEntry result = new ArchiveEntry(path, isDirectory, time == null ? -1 : time.getTime(), size == null ? -1 : size, true); result.setEntryObject(i); return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/adb/AdbFile.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.adb; import com.mucommander.commons.file.*; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import se.vidstige.jadb.JadbConnection; import se.vidstige.jadb.JadbDevice; import se.vidstige.jadb.JadbException; import se.vidstige.jadb.RemoteFile; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; /** * @author Oleg Trifonov * Created on 09/09/15. */ public class AdbFile extends ProtocolFile { private final RemoteFile remoteFile; private List childs; private AbstractFile parent; private JadbConnection jadbConnection; private String rootFolder; private static FileURL lastModifiedPath; // FIXME that's a bad way to detect directory changes /** * Creates a new file instance with the given URL. * * @param url the FileURL instance that represents this file's location */ private AdbFile(FileURL url, RemoteFile remoteFile) throws IOException { super(url); if (remoteFile == null) { JadbDevice device = getDevice(url); if (device == null) { throw new IOException("ADB file error"); } String path = url.getPath(); if (path.isEmpty() || "\\".equals(path)) { path = "/"; } remoteFile = tryLs(device, path); if (remoteFile == null && "/".equals(path)) { remoteFile = tryLs(device, "/sdcard/"); if (remoteFile != null) { rootFolder = "/sdcard/"; } } if (remoteFile == null && "/".equals(path)) { remoteFile = tryLs(device, "/mnt/sdcard/"); if (remoteFile != null) { rootFolder = "/mnt/sdcard/"; } } closeConnection(); } else { if (remoteFile.isDirectory()) { rebuildChildrenList(url); } } if (rootFolder == null) { rootFolder = "/"; } this.remoteFile = remoteFile; } private RemoteFile tryLs(JadbDevice device, String path) throws IOException { RemoteFile result = null; try { List files = device.list(path); childs = new ArrayList<>(); for (RemoteFile rf : files) { if (".".equals(rf.getPath())) { result = rf; } else { childs.add(rf); } } } catch (JadbException e) { e.printStackTrace(); } return result; } private void rebuildChildrenList(FileURL url) throws IOException { try { JadbDevice device = getDevice(url); List files = device.list("/" + url.getPath()); childs = new ArrayList<>(); for (RemoteFile rf : files) { if (!".".equals(rf.getPath())) { childs.add(rf); } } } catch (JadbException e) { e.printStackTrace(); } closeConnection(); } JadbDevice getDevice(FileURL url) throws IOException { closeConnection(); jadbConnection = new JadbConnection(); JadbDevice device = null; try { List devices = jadbConnection.getDevices(); final String host = url.getHost(); for (JadbDevice dev : devices) { if (dev.getSerial().equalsIgnoreCase(host)) { device = dev; break; } } } catch (JadbException e) { e.printStackTrace(); } return device; } private void closeConnection() { if (jadbConnection != null) { jadbConnection = null; } } AdbFile(FileURL url) throws IOException { this(url, null); } @Override public long getLastModifiedDate() { if (remoteFile == null) { return 0; } return remoteFile.getLastModified(); } @Override public void setLastModifiedDate(long lastModified) { } @Override public void changeReplication(short replication) throws IOException { } @Override public long getSize() { if (remoteFile == null) { return 0; } return remoteFile.getSize(); } @Override public AbstractFile getParent() { if (parent == null && !"/".equals(getURL().getPath())) { try { parent = new AdbFile(getURL().getParent(), null); } catch (IOException e) { e.printStackTrace(); } } return parent; } @Override public void setParent(AbstractFile parent) { } @Override public boolean exists() { AdbFile adbParent = (AdbFile) getParent(); if (adbParent == null || adbParent.childs == null) { String path = getURL().getPath(); return "/".equals(path); } for (RemoteFile rf : adbParent.childs) { if (getName().equals(rf.getPath())) { return true; } } return false; } @Override public FilePermissions getPermissions() { return childs == null ? FilePermissions.DEFAULT_FILE_PERMISSIONS : FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS; // TODO !!! } @Override public PermissionBits getChangeablePermissions() { return null; } @Override public void changePermission(int access, int permission, boolean enabled) { } @Override public String getOwner() { return null; } @Override public short getReplication() { return 0; } @Override public long getBlocksize() { return 0; } @Override public boolean canGetOwner() { return false; } @Override public String getGroup() { return null; } @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { return remoteFile == null || remoteFile.isDirectory(); } @Override public boolean isSymlink() { return false; } @Override public boolean isSystem() { return false; } @Override public AbstractFile[] ls() throws IOException { if (getURL().equals(lastModifiedPath)) { rebuildChildrenList(lastModifiedPath); lastModifiedPath = null; } if (childs == null) { return null; } AbstractFile[] result = new AbstractFile[childs.size() - 1]; // skip ".." int index = 0; for (RemoteFile rf : childs) { if ("..".equals(rf.getPath())) { continue; } FileURL url = FileURL.getFileURL(getURL() + rootFolder + rf.getPath()); AdbFile adbFile = new AdbFile(url, rf); adbFile.parent = this; result[index++] = adbFile; } return result; } @Override public void mkdir() throws IOException { JadbDevice device = getDevice(getURL()); if (device == null) { closeConnection(); throw new IOException("file not found: " + getURL()); } try { device.executeShell("mkdir", getURL().getPath()); } catch (JadbException e) { throw new IOException(e); } // TODO doesn't work without this delay FIXME try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } closeConnection(); if (getParent() instanceof AdbFile) { AdbFile parent = (AdbFile) getParent(); lastModifiedPath = parent.getURL(); parent.rebuildChildrenList(parent.getURL()); } } @Override public InputStream getInputStream() throws IOException { return new AdbInputStream(this); } @Override public OutputStream getOutputStream() throws IOException { return null; } @Override public OutputStream getAppendOutputStream() { return null; } @Override public RandomAccessInputStream getRandomAccessInputStream() { return null; } @Override public RandomAccessOutputStream getRandomAccessOutputStream() { return null; } private void finishFileOperation() throws IOException { // TODO doesn't work without this delay FIXME try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } closeConnection(); if (getParent() instanceof AdbFile) { AdbFile parent = (AdbFile) getParent(); lastModifiedPath = parent.getURL(); parent.rebuildChildrenList(parent.getURL()); } } @Override public void delete() throws IOException { JadbDevice device = getDevice(getURL()); if (device == null) { closeConnection(); throw new IOException("file not found: " + getURL()); } try { if (isDirectory()) { device.executeShell("rmdir", getURL().getPath()); } else { device.executeShell("rm", getURL().getPath()); } } catch (JadbException e) { closeConnection(); e.printStackTrace(); throw new IOException(e); } finishFileOperation(); } @Override public void renameTo(AbstractFile destFile) throws IOException { JadbDevice device = getDevice(getURL()); if (device == null) { closeConnection(); throw new IOException("file not found: " + getURL()); } try { device.executeShell("mv", getURL().getPath(), destFile.getURL().getPath()); } catch (JadbException e) { throw new IOException(e); } finishFileOperation(); } @Override public void copyRemotelyTo(AbstractFile destFile) { } @Override public long getFreeSpace() { return 0; } @Override public long getTotalSpace() { return 0; } @Override public Object getUnderlyingFileObject() { return null; } @Override public boolean isFileOperationSupported(FileOperation op) { return op != FileOperation.WRITE_FILE && super.isFileOperationSupported(op); } public void pushTo(AbstractFile destFile) throws IOException { JadbDevice device = getDevice(getURL()); if (device == null) { closeConnection(); throw new IOException("file not found: " + getURL()); } try { device.pull(new RemoteFile(getURL().getPath()), destFile.getOutputStream()); } catch (JadbException e) { throw new IOException(e); } closeConnection(); } public void pullFrom(AbstractFile sourceFile) throws IOException { JadbDevice device = getDevice(getURL()); if (device == null) { closeConnection(); throw new IOException("file not found: " + getURL()); } long lastModified = sourceFile.getLastModifiedDate(); int mode = 0664; try { device.push(sourceFile.getInputStream(), lastModified, mode, new RemoteFile(getURL().getPath())); } catch (JadbException e) { closeConnection(); e.printStackTrace(); throw new IOException(e); } closeConnection(); finishFileOperation(); // try { // Thread.sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } // if (getParent() instanceof AdbFile) { // AdbFile parent = (AdbFile)getParent(); // lastModifiedPath = parent.getURL(); // parent.rebuildChildrenList(parent.getURL()); // } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/adb/AdbInputStream.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.adb; import se.vidstige.jadb.JadbDevice; import se.vidstige.jadb.JadbException; import se.vidstige.jadb.RemoteFile; import java.io.*; import java.util.Random; /** * @author Oleg Trifonov * Created on 29/12/15. */ public class AdbInputStream extends InputStream { private static final long MAX_CACHED_SIZE = 10*1024*1024; private final ByteArrayOutputStream bos; private InputStream is; private final File tempFile; AdbInputStream(AdbFile file) throws IOException { this.bos = file.getSize() <= MAX_CACHED_SIZE ? new ByteArrayOutputStream() : null; this.tempFile = bos == null ? File.createTempFile(file.getName(), ""+System.currentTimeMillis() + "-" + new Random().nextInt(0xffff)) : null; JadbDevice device = file.getDevice(file.getURL()); if (device == null) { throw new IOException("file not found: " + file.getURL()); } if (bos != null) { try { device.pull(new RemoteFile(file.getURL().getPath()), bos); } catch (JadbException e) { close(); throw new IOException(e); } } else { try { device.pull(new RemoteFile(file.getURL().getPath()), new FileOutputStream(tempFile)); } catch (JadbException e) { close(); throw new IOException(e); } } } @Override public int read() throws IOException { if (is == null) { is = bos != null ? new ByteArrayInputStream(bos.toByteArray()) : new FileInputStream(tempFile); } return is.read(); } @Override public synchronized void reset() throws IOException { super.reset(); if (is != null) { is.reset(); } } @Override public void close() throws IOException { super.close(); if (is != null) { is.close(); } if (bos != null) { bos.close(); } if (tempFile != null) { tempFile.delete(); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/adb/AdbProtocolProvider.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.adb; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import java.io.IOException; /** * @author Oleg Trifonov * Created on 09/09/15. */ public class AdbProtocolProvider implements ProtocolProvider { @Override public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return new AdbFile(url); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ar/ArArchiveEntryIterator.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.ar; import com.mucommander.commons.file.ArchiveEntry; import com.mucommander.commons.file.ArchiveEntryIterator; import com.mucommander.commons.io.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; /** * An ArchiveEntryIterator that iterates through an AR archive. * * @author Maxence Bernard */ class ArArchiveEntryIterator implements ArchiveEntryIterator { private static final Logger LOGGER = LoggerFactory.getLogger(ArArchiveEntryIterator.class); /** InputStream to the the archive file */ private InputStream in; /** The current entry, where the stream is currently positionned */ private ArchiveEntry currentEntry; /** GNU variant: extended filenames contained in the special // entry's data */ private byte gnuExtendedNames[]; /** * Creates a new ArArchiveEntryIterator that parses the given AR InputStream. * The InputStream will be closed by {@link #close()}. * * @param in an AR archive InputStream * @throws IOException if an I/O error occurred while initializing this iterator */ ArArchiveEntryIterator(InputStream in) throws IOException { this.in = in; // Skip the global header: "!" string followed by LF char (8 characters in total). StreamUtils.skipFully(in, 8); } /** * Reads the next file header and returns an {@link ArchiveEntry} representing the entry. * * @return an ArchiveEntry representing the entry * @throws IOException if an error occurred */ ArchiveEntry getNextEntry() throws IOException { byte fileHeader[] = new byte[60]; try { // Fully read the 60 file header bytes. If it cannot be read, it most likely means we've reached // the end of the archive. StreamUtils.readFully(in, fileHeader); }catch(IOException e) { return null; } try { // Read the 16 filename characters and trim string to remove any trailing white space String name = new String(fileHeader, 0, 16).trim(); // Read the 12 file date characters, trim string to remove any trailing white space // and parse date as a long. // If the entry is the special // GNU one (see below), date is null and thus should not be parsed // (would throw a NumberFormatException) long date = name.equals("//")?0:Long.parseLong(new String(fileHeader, 16, 12).trim()) * 1000; // No use for file's Owner ID, Group ID and mode at the moment, skip them // Read the 10 file size characters, trim string to remove any trailing white space // and parse size as a long long size = Long.parseLong(new String(fileHeader, 48, 10).trim()); // BSD variant : BSD ar store extended filenames by placing the string "#1/" followed by the file name length // in the file name field, and appending the real filename to the file header. if (name.startsWith("#1/")) { // Read extended name int extendedNameLength = Integer.parseInt(name.substring(3)); name = new String(StreamUtils.readFully(in, new byte[extendedNameLength])).trim(); // Decrease remaining file size size -= extendedNameLength; } // GNU variant: GNU ar stores multiple extended filenames in the data section of a file with the name "//", // this record is referred to by future headers. A header references an extended filename by storing a "/" // followed by a decimal offset to the start of the filename in the extended filename data section. // This entry appears first in the archive, i.e. before any other entries. else if(name.equals("//")) { this.gnuExtendedNames = StreamUtils.readFully(in, new byte[(int)size]); // Skip one padding byte if size is odd if (size % 2 != 0) { StreamUtils.skipFully(in, 1); } // Don't return this entry which should not be visible, but recurse to return next entry instead return getNextEntry(); } // GNU variant: entry with an extended name, look up extended name in // entry else if(this.gnuExtendedNames!=null && name.startsWith("/")) { int off = Integer.parseInt(name.substring(1)); name = ""; byte b; while((b=this.gnuExtendedNames[off++])!='/') { name += (char)b; } } return new ArchiveEntry(name, false, date, size, true); } catch(IOException e) { // Re-throw IOException LOGGER.info("Caught IOException", e); throw e; } catch(Exception e2) { // Catch any other exceptions (NumberFormatException for instance) and throw an IOException instead LOGGER.info("Caught Exception", e2); throw new IOException(); } } ///////////////////////////////////////// // ArchiveEntryIterator implementation // ///////////////////////////////////////// public ArchiveEntry nextEntry() throws IOException { if(currentEntry!=null) { // Skip the current entry's data, plus 1 padding byte if size is odd long size = currentEntry.getSize(); StreamUtils.skipFully(in, size + (size%2)); } // Get the next entry, if any currentEntry = getNextEntry(); return currentEntry; } public void close() throws IOException { in.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ar/ArArchiveFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.ar; import com.mucommander.commons.file.*; import com.mucommander.commons.io.BoundedInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; /** * ArArchiveFile provides read-only access to archives in the unix AR format. Both the BSD and GNU variants (which adds * support for extended filenames) are supported. * * @see com.mucommander.commons.file.impl.ar.ArFormatProvider * @author Maxence Bernard */ public class ArArchiveFile extends AbstractROArchiveFile { private static final Logger LOGGER = LoggerFactory.getLogger(ArArchiveFile.class); /** * Creates a ArArchiveFile around the given file. * * @param file the underlying file to wrap this archive file around */ public ArArchiveFile(AbstractFile file) { super(file); } //////////////////////////////////////// // AbstractArchiveFile implementation // //////////////////////////////////////// @Override public ArchiveEntryIterator getEntryIterator() throws IOException { return new ArArchiveEntryIterator(getInputStream()); } @Override public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException { InputStream in = getInputStream(); ArchiveEntryIterator iterator = new ArArchiveEntryIterator(in); ArchiveEntry currentEntry; while((currentEntry = iterator.nextEntry())!=null) { if(currentEntry.getName().equals(entry.getName())) { LOGGER.trace("found entry {}", entry.getName()); return new BoundedInputStream(in, entry.getSize(), false); } } // Entry not found, should not normally happen LOGGER.info("Warning: entry not found, throwing IOException"); throw new IOException(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ar/ArFormatProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.ar; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import java.io.IOException; /** * This class is the provider for the 'Ar' archive format implemented by {@link ArArchiveFile}. * * @see com.mucommander.commons.file.impl.ar.ArArchiveFile * @author Nicolas Rinaudo, Maxence Bernard */ public class ArFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".ar", ".a", ".deb", ".udeb"}; /** * Static instance of the filename filter that matches archive filenames */ private static final ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new ArArchiveFile(file); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ar/package.html ================================================ Provides an implementation of the AR archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/arj/ArjFormatProvider.java ================================================ package com.mucommander.commons.file.impl.arj; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class ArjFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".arj" }; /** * Static instance of the filename filter that matches archive filenames */ private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = { 0x60, (byte) 0xEA }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.ARJ, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/AvrConfigFileUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; import java.io.*; import java.util.Properties; /** * @author Oleg Trifonov * Created on 23/03/16. */ public class AvrConfigFileUtils { private static final String KEY_DEVICE_NAME = "avr_device_name"; private static final String KEY_PROGRAMMER = "programmer"; private static final String KEY_VERIFY = "verify"; private static final String KEY_PORT = "port"; private static final String KEY_BAUDRATE = "baudrate"; private static final String KEY_BITCLOCK = "bitclock"; private static final String KEY_CONFIG_FILE = "config_file"; private static final String KEY_AUTOERASE = "autoerase"; private static final String KEY_ISP_CLOCK_DELAY = "isp_clock_delay"; private static final String KEY_OVERRIDE_INVALID_SIGNATURE_CHECK = "override_invalid_signature_check"; private static final String KEY_EXTENDED_PARAM = "extended_param"; private static final String KEY_AVRDUDE_PATH = "avrdude_path"; public static AvrdudeConfiguration load(String filePath) throws IOException { Properties properties = new Properties(); Reader reader = new BufferedReader(new FileReader(filePath)); properties.load(reader); AvrdudeConfiguration result = load(properties); reader.close(); return result; } public static AvrdudeConfiguration load(InputStream is) throws IOException { Properties properties = new Properties(); properties.load(is); AvrdudeConfiguration result = load(properties); is.close(); return result; } private static AvrdudeConfiguration load(Properties properties) { String deviceName = properties.getProperty(KEY_DEVICE_NAME); Integer baudrate = getPropertyInt(properties, KEY_BAUDRATE); Integer bitclock = getPropertyInt(properties, KEY_BITCLOCK); String configFile = properties.getProperty(KEY_CONFIG_FILE); String programmer = properties.getProperty(KEY_PROGRAMMER); boolean flashAutoerase = getPropertyBool(properties, KEY_AUTOERASE, true); Integer ispCockDelay = getPropertyInt(properties, KEY_ISP_CLOCK_DELAY); String port = properties.getProperty(KEY_PORT); boolean overrideInvalidSignatureCheck = getPropertyBool(properties, KEY_OVERRIDE_INVALID_SIGNATURE_CHECK, false); boolean verify = getPropertyBool(properties, KEY_VERIFY, true); String extendedParam = properties.getProperty(KEY_EXTENDED_PARAM); String avrdudeLocation = properties.getProperty(KEY_AVRDUDE_PATH); return new AvrdudeConfiguration(deviceName, baudrate, bitclock, configFile, programmer, flashAutoerase, ispCockDelay, port, overrideInvalidSignatureCheck, verify, extendedParam, avrdudeLocation); } private static Integer getPropertyInt(Properties properties, String key) { try { return Integer.parseInt(properties.getProperty(key)); } catch (Throwable t) { return null; } } private static boolean getPropertyBool(Properties properties, String key, boolean defaultValue) { String val = properties.getProperty(key); if (val == null) { return defaultValue; } if ("true".equalsIgnoreCase(val) || "yes".equalsIgnoreCase(val)) { return true; } else if ("false".equalsIgnoreCase(val) || "no".equalsIgnoreCase(val)) { return false; } return defaultValue; } public static void save(AvrdudeConfiguration config, String filePath) throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(filePath)); writer.write(build(config)); writer.close(); } private static String build(AvrdudeConfiguration config) { StringBuilder sb = new StringBuilder(); sb.append("### trolCommander avrdude configuration\n\n"); sb.append("# Required. Specify AVR device. (etc. \"m8\", \"m32\", \"t13\")\n"); addRequiredParam(sb, KEY_DEVICE_NAME, config.deviceName); sb.append("# Specify programmer type. (etc \"usbasp\")\n"); addRequiredParam(sb, KEY_PROGRAMMER, config.programmer); sb.append("# Verify after write\n"); addRequiredParam(sb, KEY_VERIFY, config.verify); sb.append("# Specify connection port\n"); addOptionalParam(sb, KEY_PORT, config.port); sb.append("# RS-232 baud rate\n"); addOptionalParam(sb, KEY_BAUDRATE, config.baudrate); sb.append("# JTAG/STK500v2 bit clock period (us)\n"); addOptionalParam(sb, KEY_BITCLOCK, config.bitclock); sb.append("# Specify location of configuration file\n"); addOptionalParam(sb, KEY_CONFIG_FILE, config.configFile); sb.append("# Enable auto erase for flash memory\n"); addRequiredParam(sb, KEY_AUTOERASE, config.flashAutoerase); sb.append("# ISP Clock Delay [in microseconds]\n"); addOptionalParam(sb, KEY_ISP_CLOCK_DELAY, config.ispCockDelay); sb.append("# Override invalid signature check.\n"); addRequiredParam(sb, KEY_OVERRIDE_INVALID_SIGNATURE_CHECK, config.overrideInvalidSignatureCheck); sb.append("# Pass extended_param to programmer.\n"); addOptionalParam(sb, KEY_EXTENDED_PARAM, config.extendedParam); sb.append("# Path to avrdude\n"); addOptionalParam(sb, KEY_AVRDUDE_PATH, config.avrdudeLocation); return sb.toString(); } private static void addRequiredParam(StringBuilder sb, String name, Object value) { sb.append(name); sb.append(" = "); sb.append(value); sb.append("\n\n"); } private static void addOptionalParam(StringBuilder sb, String name, Object value) { if (value != null) { sb.append(name); sb.append(" = "); sb.append(value); } else { sb.append("# "); sb.append(name); sb.append(" = "); } sb.append("\n\n"); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/AvrDudeInputStream.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; import com.mucommander.commons.HasProgress; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; /** * @author Oleg Trifonov * Created on 31/03/16. */ public class AvrDudeInputStream extends InputStream implements HasProgress { private StreamType type; private Avrdude avrdude; private AvrdudeConfiguration config; private Avrdude.Operation operation; private ByteArrayInputStream data; public AvrDudeInputStream(StreamType type, AvrdudeConfiguration config, Avrdude.Operation operation) { this.type = type; this.config = config; this.operation = operation; this.avrdude = new Avrdude(); } @Override public int read() throws IOException { if (avrdude.getStatus() == Avrdude.Status.NONE) { readAll(); } return data.read(); } @Override public int available() { return data.available(); } @Override public synchronized void reset() { data.reset(); } @Override public boolean markSupported() { return data.markSupported(); } @Override public synchronized void mark(int readlimit) { data.mark(readlimit); } @Override public int getProgress() { return avrdude.getProgress(); } @Override public boolean hasProgress() { return true; } void readAll() throws IOException { try { avrdude.execute(config, operation, type); avrdude.waitFor(); if (type == StreamType.HEX) { data = new ByteArrayInputStream(avrdude.getHexOutput().getBytes()); } } catch (InterruptedException e) { e.printStackTrace(); throw new IOException(e); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/Avrdude.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; import com.mucommander.command.Command; import com.mucommander.commons.io.StreamUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.process.*; import org.apache.commons.lang.StringUtils; import java.io.*; import java.util.ArrayList; import java.util.List; /** * @author Oleg Trifonov * Created on 21/04/16. */ public class Avrdude { enum Status { NONE, IN_PROGRESS, FINISHED } public enum Operation { READ_FLASH(false), READ_EEPROM(false), READ_SIGNATURE(false), READ_FUSES(false), READ_CALIBRATION(false), WRITE_FLASH(true), WRITE_EEPROM(true), WRITE_FUSES(true), WRITE_CALIBRATION(true); final boolean isWriteOperation; Operation(boolean isWriteOperation) { this.isWriteOperation = isWriteOperation; } } private volatile int progress; private int exitCode; private volatile Status status; private AbstractProcess process; private final InputStream inputStream; private final ByteArrayOutputStream fullOutputStream = new ByteArrayOutputStream(); public Avrdude() { this.inputStream = null; status = Status.NONE; } public Avrdude(InputStream inputStream) { this.inputStream = inputStream; status = Status.NONE; } private static String buildCommandLine(AvrdudeConfiguration config, Operation operation, StreamType streamType) { String cmd; if (config.avrdudeLocation != null) { cmd = config.avrdudeLocation; } else { cmd = OsFamily.WINDOWS.isCurrent() ? "avrdude.exe" : "avrdude"; } cmd += " -p " + config.deviceName; if (config.baudrate != null) { cmd += " -b " + config.baudrate; } if (config.bitclock != null) { cmd += " -B " + config.bitclock; } if (config.configFile != null) { cmd += " -C " + config.configFile; } if (config.programmer != null) { cmd += " -c " + config.programmer; } if (!config.flashAutoerase) { cmd += " -D"; } if (config.ispCockDelay != null) { cmd += " -i" + config.ispCockDelay; } if (config.port != null) { cmd += " -P " + config.port; } if (config.overrideInvalidSignatureCheck) { cmd += " -F"; } if (!config.verify) { cmd += " -V"; } if (config.extendedParam != null) { cmd += " -x " + config.extendedParam; } AvrdudeDevice device = AvrdudeDevice.getDevice(config.deviceName); switch (operation) { case READ_FLASH: return cmd + " -u -U flash:r:-:" + streamType.getAvrdudeName(); case READ_EEPROM: return cmd + " -u -U eeprom:r:-:" + streamType.getAvrdudeName(); case READ_SIGNATURE: return cmd + " -u -U signature:r:-:" + streamType.getAvrdudeName(); case READ_FUSES: cmd += " -u "; if (device.blockSizes.containsKey("efuse")) { cmd += "-U efuse:-:" + streamType.getAvrdudeName(); } if (device.blockSizes.containsKey("hfuse")) { cmd += "-U hfuse:-:" + streamType.getAvrdudeName(); } if (device.blockSizes.containsKey("lfuse")) { cmd += "-U lfuse:-:" + streamType.getAvrdudeName(); } return cmd; case READ_CALIBRATION: return cmd + " -u -U calibration:r:-:" + streamType.getAvrdudeName(); case WRITE_FLASH: return cmd + " -u -U flash:w:-:" + streamType.getAvrdudeName(); case WRITE_EEPROM: return cmd + " -u -U eeprom:w:-:" + streamType.getAvrdudeName(); case WRITE_FUSES: // TODO case WRITE_CALIBRATION: return cmd + " -u -U calibration:w:-:" + streamType.getAvrdudeName(); } throw new RuntimeException("unknown operation"); } public void execute(AvrdudeConfiguration config, Operation operation, StreamType streamType) throws IOException, InterruptedException { String cmd = buildCommandLine(config, operation, streamType); ProcessListener processListener = new ProcessListener() { int operationCount; List lines = new ArrayList<>(); int nextLineIndex = 0; int progressCount = 0; @Override public void processDied(int returnValue) { System.out.println("--------- "); for (String s : lines) { System.out.println("@"+s); } } @Override public void processOutput(String output) { String[] outLines = StringUtils.splitByWholeSeparatorPreserveAllTokens(output, "\n");//output.split("\n"); for (int i = 0; i < outLines.length; i++) { String s = outLines[i]; if (i == 0 && lines.size() > 0) { String old = lines.get(lines.size()-1); lines.set(lines.size()-1, old + s); } else { lines.add(s); } } //if (output.equals("#")) System.out.println(">"+output + " (" + progress + ")"); else //System.out.println(">"+output); for (int i = nextLineIndex; i < lines.size(); i++) { if (i >= lines.size()) { break; } String s = lines.get(i); if (s.contains("Reading |") || s.contains("Writing |")) { operationCount++; nextLineIndex = i + 1; } //System.out.println("?"+s + " (" + operationCount + ")"); } if (nextLineIndex < lines.size()-1) { nextLineIndex = lines.size()-1; } // if (output.contains("Reading |") || output.contains("Writing |")) { // operationCount++; // } if (operationCount > 0 && status == Status.NONE) { status = Status.IN_PROGRESS; progressCount = 0; progress = 0; } if (status == Status.IN_PROGRESS && output.contains("#")) { //System.out.println("\n?+ " + StringUtils.countMatches(output, "#")); progressCount += StringUtils.countMatches(output, "#")*2; if (progressCount > 100) { progress = progressCount - 100; } } } @Override public void processOutput(byte[] buffer, int offset, int length) { fullOutputStream.write(buffer, offset, length); } }; String[] tokens = Command.getTokens(cmd); process = ProcessRunner.execute(tokens, null, processListener, null); if (inputStream != null) { //Thread.sleep(1000); StreamUtils.copyStream(inputStream, process.getOutputStream()); process.getOutputStream().close(); inputStream.close(); } // process.getOutputStream().write("!!!!!!!\n".getBytes()); process.getOutputStream().close(); } public void waitFor() throws IOException, InterruptedException { exitCode = process.waitFor(); process.waitMonitoring(); process.destroy(); status = Status.FINISHED; } public String getHexOutput() { final String startTemplate = "writing output file \"\""; String fullOutput = fullOutputStream.toString(); int start = fullOutput.indexOf(startTemplate); if (start < 0) { return null; } start = fullOutput.indexOf(':', start + startTemplate.length()); int finish = fullOutput.indexOf("avrdude done", start); if (finish > 0) { return fullOutput.substring(start-1, finish); } return null;//fullOutput.substring(start); } public int getProgress() { return progress; } public Status getStatus() { return status; } public static void main(String args[]) throws IOException { AvrdudeConfiguration config = new AvrdudeConfiguration("m8", null, null, null, "usbasp", true, null, null, true, false, null, "/Users/trol/-avrdude/avrdude-6.3/avrdude"); //OutputStream os = new FileOutputStream("/Users/trol/--------.bin");//System.out; /* AvrDudeInputStream is = new AvrDudeInputStream(StreamType.HEX, config, Operation.READ_FLASH); new Thread() { int lastProgress = -1; @Override public void run() { while (true) { int progress = is.getProgress(); if (progress != lastProgress) { System.out.println("progress: " + progress); } lastProgress = progress; if (progress == 100) { break; } try { Thread.sleep(50); } catch (InterruptedException e) { } } } }.start(); StreamUtils.copyStream(is, new FileOutputStream(new File("/Users/trol/--------.hex"))); */ AvrdudeOutputStream os = new AvrdudeOutputStream(StreamType.HEX, config, Operation.WRITE_FLASH); new Thread() { int lastProgress; @Override public void run() { while (true) { int progress = os.getProgress(); if (progress != lastProgress) { System.out.println("progress: " + progress); } lastProgress = progress; if (progress == 100) { break; } try { Thread.sleep(50); } catch (InterruptedException e) { } } } }.start(); StreamUtils.copyStream(new FileInputStream(new File("/Users/trol/--------.hex")), os); os.close(); /* Avrdude avrdude = new Avrdude(); avrdude.execute(config, Operation.READ_FLASH, StreamType.HEX); new Thread() { @Override public void run() { int progress = avrdude.progress; while (progress < 100) { progress = avrdude.progress; System.out.println("Progress: " + progress); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } try { avrdude.waitFor(); } catch (Exception e) { e.printStackTrace(); } System.out.println("------------"); System.out.println(avrdude.getHexOutput()); } }.start(); */ } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/AvrdudeConfiguration.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; /** * @author Oleg Trifonov * Created on 23/03/16. */ public class AvrdudeConfiguration { /** * Required. Specify AVR device. */ public final String deviceName; /** * Override RS-232 baud rate. */ public final Integer baudrate; /** * Specify JTAG/STK500v2 bit clock period (us). */ public final Integer bitclock; /** * Specify location of configuration file. */ public final String configFile; /** * Specify programmer type. */ public final String programmer; /** * Enable auto erase for flash memory */ public final boolean flashAutoerase; /** * ISP Clock Delay [in microseconds] */ public final Integer ispCockDelay; /** * Specify connection port. */ public final String port; /** * Override invalid signature check. */ public final boolean overrideInvalidSignatureCheck; public final boolean verify; /** * Pass extended_param to programmer. */ public final String extendedParam; /** * Path to avrdude */ public final String avrdudeLocation; public AvrdudeConfiguration(String deviceName, Integer baudrate, Integer bitclock, String configFile, String programmer, boolean flashAutoerase, Integer ispCockDelay, String port, boolean overrideInvalidSignatureCheck, boolean verify, String extendedParam, String avrdudeLocation) { this.deviceName = deviceName; this.baudrate = baudrate; this.bitclock = bitclock; this.configFile = configFile; this.programmer = programmer; this.flashAutoerase = flashAutoerase; this.ispCockDelay = ispCockDelay; this.port = port; this.overrideInvalidSignatureCheck = overrideInvalidSignatureCheck; this.verify = verify; this.extendedParam = extendedParam; this.avrdudeLocation = avrdudeLocation; } public AvrdudeConfiguration() { this.deviceName = "m8"; this.baudrate = null; this.bitclock = null; this.configFile = null; this.programmer = "usbasp"; this.flashAutoerase = true; this.ispCockDelay = null; this.port = null; this.overrideInvalidSignatureCheck = false; this.verify = true; this.extendedParam = null; this.avrdudeLocation = null; } public boolean isValid() { return deviceName != null && programmer != null && !deviceName.trim().isEmpty() && !programmer.trim().isEmpty(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/AvrdudeDevice.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; import com.mucommander.commons.file.util.ResourceLoader; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; /** * @author Oleg Trifonov * Created on 13/04/16. */ public class AvrdudeDevice { private static final String AVRDUDE_RESOURCE_NAME = "/avr/avrdude.csv"; public final String id; public final String name; public final int signature; public final Map blockSizes; private static WeakReference> devices; private AvrdudeDevice(String id, String name, int signature, Map blockSizes) { this.id = id; this.name = name; this.signature = signature; this.blockSizes = blockSizes; } public static AvrdudeDevice getDevice(String id) { return getDevices().get(id); } private static Map getDevices() { Map map = devices != null ? devices.get() : null; if (map == null) { map = load(); devices = new WeakReference<>(map); } return map; } private static Map load() { Map result = new HashMap<>(); try (BufferedReader br = new BufferedReader(new InputStreamReader(ResourceLoader.getResourceAsStream(AVRDUDE_RESOURCE_NAME)))) { String line; while ((line = br.readLine()) != null) { String[] parts = line.split(":"); String id = parts[0]; String name = parts[1]; int signature = Integer.parseInt(parts[2].substring(2), 16); Map blocks = new HashMap<>(); for (int blockIndex = 3; blockIndex < parts.length; blockIndex++) { String block = parts[blockIndex]; int nameEndIndex = block.indexOf('['); String blockName = block.substring(0, nameEndIndex); int length = Integer.parseInt(block.substring(nameEndIndex+1, block.length()-1)); blocks.put(blockName, length); } result.put(id, new AvrdudeDevice(id, name, signature, blocks)); } } catch (IOException e) { e.printStackTrace(); } return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/AvrdudeOutputStream.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; import com.mucommander.commons.HasProgress; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; /** * @author Oleg Trifonov * Created on 31/03/16. */ public class AvrdudeOutputStream extends OutputStream implements HasProgress { private StreamType type; private Avrdude avrdude; private AvrdudeConfiguration config; private Avrdude.Operation operation; private ByteArrayOutputStream data; public AvrdudeOutputStream(StreamType type, AvrdudeConfiguration config, Avrdude.Operation operation) { this.type = type; this.config = config; this.operation = operation; this.data = new ByteArrayOutputStream(); } @Override public void write(int b) throws IOException { if (data == null) { throw new IOException("Stream is closed"); } data.write(b); } @Override public void close() throws IOException { if (data == null) { throw new IOException("Stream is closed"); } writeToDevice(); data.close(); data = null; } private void writeToDevice() throws IOException { this.avrdude = new Avrdude(new ByteArrayInputStream(data.toByteArray())); try { avrdude.execute(config, operation, type); avrdude.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); throw new IOException(e); } } @Override public int getProgress() { return avrdude == null ? 0 : avrdude.getProgress(); } @Override public boolean hasProgress() { return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/AvrdudeProtocolProvider.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import com.mucommander.commons.file.impl.avrdude.files.*; import java.io.IOException; /** * @author Oleg Trifonov * Created on 09/02/16. */ public class AvrdudeProtocolProvider implements ProtocolProvider { private static final String STORAGE_DIR = "avr"; @Override public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { if (isRootUrl(url)) { return new AvrRootDir(url, getUrlPath(url)); } else if (isRootUrl(url.getParent())) { return new AvrDeviceDir(url); } else if (isRootUrl(url.getParent().getParent())) { if (url.getFilename().equalsIgnoreCase(AvrConfigFile.FILENAME)) { return new AvrConfigFile(url); } else { return new AvrMemoryDir(url); } } return new AvrMemoryFile(url); } private static String getUrlPath(FileURL url) { if (url == null) { return null; } String location = url.toString(); int schemeDelimPos = location.indexOf("://"); if (schemeDelimPos > 0) { return location.substring(schemeDelimPos + 3); } return ""; } private static boolean isRootUrl(FileURL url) { if (url == null) { return true; } final String path = getUrlPath(url); return path == null || path.isEmpty() || path.equals("/") || path.equals("\\"); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/StreamType.java ================================================ package com.mucommander.commons.file.impl.avrdude; /** * @author Oleg Trifonov * Created on 24/06/16. */ public enum StreamType { BIN('r'), HEX('i'); private final char avrdudeName; StreamType(char avrdudeName) { this.avrdudeName = avrdudeName; } public char getAvrdudeName() { return avrdudeName; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/files/AvrConfigFile.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude.files; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.avrdude.AvrConfigFileUtils; import com.mucommander.commons.file.impl.avrdude.AvrdudeConfiguration; import com.mucommander.commons.file.impl.avrdude.AvrdudeDevice; import java.io.*; /** * @author Oleg Trifonov * Created on 24/03/16. */ public class AvrConfigFile extends AvrdudeFile { public static final String FILENAME = "config.properties"; private static class ConfigInputStream extends ByteArrayInputStream { public ConfigInputStream(String s) { super(s.getBytes()); } } private class ConfigOutputStream extends ByteArrayOutputStream { @Override public void close() throws IOException { AvrdudeConfiguration configuration = AvrConfigFileUtils.load(new ByteArrayInputStream(buf)); if (!configuration.isValid()) { throw new IOException("wrong configuration"); } if (AvrdudeDevice.getDevice(configuration.deviceName) == null) { throw new IOException("unknown device"); } AvrConfigFileUtils.save(configuration, getLocalConfigFile().getAbsolutePath()); AvrConfigFile.this.device = null; AvrConfigFile.this.configuration = configuration; super.close(); } } public AvrConfigFile(FileURL url) throws IOException { super(url); } private static String extractPathFromUrl(FileURL url) { return url.getParent().toString().substring(6) + FILENAME; // 6 - length of "avr://" } @Override public FilePermissions getPermissions() { return FilePermissions.DEFAULT_FILE_PERMISSIONS; } @Override public boolean isDirectory() { return false; } @Override public AbstractFile[] ls() throws IOException { return new AbstractFile[0]; } @Override public void mkdir() throws IOException { } @Override public boolean exists() { return true; } @Override public InputStream getInputStream() throws IOException { AbstractFile configFile = getLocalConfigFile(); return configFile.getInputStream(); } @Override public long getSize() { try { return getLocalConfigFile().getSize(); } catch (IOException e) { e.printStackTrace(); return -1; } } @Override public OutputStream getOutputStream() throws IOException { return new ConfigOutputStream(); } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { getLocalConfigFile().copyRemotelyTo(destFile); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/files/AvrDeviceDir.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude.files; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.avrdude.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * @author Oleg Trifonov * Created on 24/03/16. */ public class AvrDeviceDir extends AvrdudeFile { private AvrdudeFile parent; public AvrDeviceDir(FileURL url) throws IOException { super(url); } @Override public FilePermissions getPermissions() { return FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS; } @Override public boolean isDirectory() { return true; } @Override public AbstractFile[] ls() throws IOException { List childs = new ArrayList<>(); AvrdudeDevice device = getDevice(); Set blocks = device.blockSizes.keySet(); childs.add(new AvrConfigFile(FileURL.getFileURL(getURL() + AvrConfigFile.FILENAME))); if (blocks.contains("flash")) { childs.add(new AvrMemoryDir(FileURL.getFileURL(getURL() + AvrMemoryFile.Type.FLASH.name))); } if (blocks.contains("eeprom")) { childs.add(new AvrMemoryDir(FileURL.getFileURL(getURL() + AvrMemoryFile.Type.EEPROM.name))); } if (blocks.contains("fuse") || blocks.contains("lfuse") || blocks.contains("hfuse") || blocks.contains("efuse")) { childs.add(new AvrMemoryFile(FileURL.getFileURL(getURL() + AvrMemoryFile.Type.FUSES.name))); } if (blocks.contains("signature")) { //childs.add(new AvrMemoryFile(FileURL.getFileURL(getURL() + getDevice().name + SIGNATURE_FILE_EXT))); childs.add(new AvrMemoryFile(FileURL.getFileURL(getURL() + AvrMemoryFile.Type.SIGNATURE.name))); } if (blocks.contains("calibration")) { childs.add(new AvrMemoryFile(FileURL.getFileURL(getURL() + AvrMemoryFile.Type.CALIBRATION.name))); } if (blocks.contains("lock")) { childs.add(new AvrMemoryFile(FileURL.getFileURL(getURL() + AvrMemoryFile.Type.LOCK.name))); } AbstractFile[] result = new AbstractFile[childs.size()]; return childs.toArray(result); } @Override public void mkdir() throws IOException { AbstractFile file = getBaseFolder().getChild(getURL().getFilename() + CONFIG_FILE_EXT); if (file.exists()) { throw new IOException("already exist"); } file.mkfile(); AvrConfigFileUtils.save(new AvrdudeConfiguration(), file.getAbsolutePath()); } @Override public boolean exists() { try { AbstractFile[] devices = getConfigFiles(); final String name = getName() + CONFIG_FILE_EXT; for (AbstractFile configFile : devices) { if (configFile.getName().equalsIgnoreCase(name)) { return true; } } } catch (IOException e) { e.printStackTrace(); } return false; } @Override public AbstractFile getParent() { if (parent == null) { try { parent = new AvrRootDir(FileURL.getFileURL(getURL().getScheme() + "://"), null); } catch (IOException e) { e.printStackTrace(); } } return parent; } @Override public void delete() throws IOException { getLocalConfigFile().delete(); } @Override public void renameTo(AbstractFile destFile) throws IOException { if (destFile.getParent().getURL().getHost() == null) { AbstractFile newConfig = getLocalConfigFile().getParent().getChild(destFile.getName() + CONFIG_FILE_EXT); getLocalConfigFile().renameTo(newConfig); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/files/AvrMemoryDir.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude.files; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import java.io.IOException; /** * @author Oleg Trifonov * Created on 25/03/16. */ public class AvrMemoryDir extends AvrdudeFile { private AvrdudeFile parent; public AvrMemoryDir(FileURL url) throws IOException { super(url); } @Override public FilePermissions getPermissions() { return FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS; } @Override public boolean isDirectory() { return true; } @Override public AbstractFile[] ls() throws IOException { AbstractFile[] files = new AbstractFile[2]; files[0] = new AvrMemoryFile(FileURL.getFileURL(getURL() + "/" + getURL().getFilename()+ ".hex")); files[1] = new AvrMemoryFile(FileURL.getFileURL(getURL() + "/" + getURL().getFilename() + ".bin")); return files; } @Override public void mkdir() throws IOException { } @Override public boolean exists() { return true; } @Override public AbstractFile getParent() { if (parent == null) { try { parent = new AvrDeviceDir(getURL().getParent()); } catch (IOException e) { e.printStackTrace(); } } return parent; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/files/AvrMemoryFile.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude.files; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.avrdude.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * @author Oleg Trifonov * Created on 24/03/16. */ public class AvrMemoryFile extends AvrdudeFile { public enum Type { FLASH("flash"), EEPROM("eeprom"), FUSES("fuses"), SIGNATURE("signature"), CALIBRATION("calibration"), LOCK("lock"); Type(String name) { this.name = name; } static Type fromFileName(String fileName) { for (Type type : values()) { if (fileName.startsWith(type.name)) { return type; } } return null; } public final String name; } private final Type type; public AvrMemoryFile(FileURL url) throws IOException { super(url); this.type = Type.fromFileName(getURL().getFilename()); } @Override public FilePermissions getPermissions() { return FilePermissions.DEFAULT_FILE_PERMISSIONS; } @Override public boolean isDirectory() { return false; } @Override public AbstractFile[] ls() throws IOException { return new AbstractFile[0]; } @Override public void mkdir() throws IOException { } @Override public boolean exists() { return true; } @Override public long getSize() { String fullName = getURL().getFilename(); int dotPos = fullName.indexOf('.'); String fileName = dotPos > 0 ? fullName.substring(0, dotPos) : fullName; if (fileName.contains("fuse")) { int size = 0; for (String blockName : getDevice().blockSizes.keySet()) { if (blockName.toLowerCase().contains("fuse")) { size += getDevice().blockSizes.get(blockName); } } return size; } else if (fullName.endsWith(SIGNATURE_FILE_EXT)) { return getDevice().blockSizes.get("signature"); } else { return getDevice().blockSizes.get(fileName); } } @Override public InputStream getInputStream() throws IOException { System.out.println("?-> " + type); Avrdude.Operation operation; switch (type) { case FLASH: operation = Avrdude.Operation.READ_FLASH; break; case EEPROM: operation = Avrdude.Operation.READ_EEPROM; break; case SIGNATURE: operation = Avrdude.Operation.READ_SIGNATURE; break; case CALIBRATION: operation = Avrdude.Operation.READ_CALIBRATION; break; default: throw new RuntimeException("unsupported operation for " + type); } return new AvrDudeInputStream(StreamType.HEX, configuration, operation); } @Override public OutputStream getOutputStream() throws IOException { Avrdude.Operation operation; switch (type) { case FLASH: operation = Avrdude.Operation.WRITE_FLASH; break; case EEPROM: operation = Avrdude.Operation.WRITE_EEPROM; break; case CALIBRATION: operation = Avrdude.Operation.WRITE_CALIBRATION; break; default: throw new RuntimeException("unsupported operation for " + type); } return new AvrdudeOutputStream(StreamType.HEX, configuration, operation); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/files/AvrRootDir.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude.files; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import java.io.IOException; /** * @author Oleg Trifonov * Created on 24/03/16. */ public class AvrRootDir extends AvrdudeFile { public AvrRootDir(FileURL url, String path) throws IOException { super(url); } @Override public boolean isDirectory() { return true; } @Override public AbstractFile[] ls() throws IOException { AbstractFile[] devices = getConfigFiles(); AvrDeviceDir[] result = new AvrDeviceDir[devices.length]; for (int i = 0; i < devices.length; i++) { result[i] = new AvrDeviceDir(FileURL.getFileURL(getURL() + devices[i].getBaseName())); } return result; } @Override public void mkdir() throws IOException { } @Override public boolean exists() { return true; } @Override public FilePermissions getPermissions() { return FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/avrdude/files/AvrdudeFile.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.avrdude.files; import com.mucommander.PlatformManager; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.impl.avrdude.AvrConfigFileUtils; import com.mucommander.commons.file.impl.avrdude.AvrdudeConfiguration; import com.mucommander.commons.file.impl.avrdude.AvrdudeDevice; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * @author Oleg Trifonov * Created on 09/02/16. * * File hierarhy: * DEVICE-NAME * |- config.properties * |- flash * | |- flash.bin * | |- flash.hex * |- eeprom * | |- eeprom.bin * | |- eeprom.hex * |- fuses * |- fuses.bin * |- fuses.hex * */ public abstract class AvrdudeFile extends ProtocolFile { private static final String STORAGE_DIR = "avr"; protected static final String CONFIG_FILE_EXT = ".conf"; protected static final String SIGNATURE_FILE_EXT = ".sign"; protected AvrdudeConfiguration configuration; protected AvrdudeDevice device; AvrdudeFile(FileURL url) throws IOException { super(url); AbstractFile baseFolder = PlatformManager.getPreferencesFolder().getChild(STORAGE_DIR); if (!baseFolder.exists()) { baseFolder.mkdir(); } // if (path.isEmpty() || path.equals("/") || path.equals("\\")) { // // } // System.out.println("path " + path + " [" + getClass().getName()); // System.out.println("baseFolder " + baseFolder + getClass().getName()); } static AbstractFile getBaseFolder() throws IOException { AbstractFile baseFolder = PlatformManager.getPreferencesFolder().getChild(STORAGE_DIR); if (!baseFolder.exists()) { baseFolder.mkdir(); } return baseFolder; } static AbstractFile[] getConfigFiles() throws IOException { return getBaseFolder().ls(new ExtensionFilenameFilter(CONFIG_FILE_EXT)); } AbstractFile getLocalConfigFile() throws IOException { // new Exception().printStackTrace(); //System.out.println("::>"+getURL()); //System.out.println("::>"+getBaseFolder()); //System.out.println(getURL().getHost() + CONFIG_FILE_EXT); //System.out.println("::>"+getBaseFolder().getChild(getURL().getHost() + CONFIG_FILE_EXT)); return getBaseFolder().getChild(getURL().getHost() + CONFIG_FILE_EXT); } public AvrdudeDevice getDevice() { try { if (device == null) { device = AvrdudeDevice.getDevice(getConfiguration().deviceName); } return device; } catch (IOException e) { e.printStackTrace(); return null; } } public AvrdudeConfiguration getConfiguration() throws IOException { if (configuration == null) { configuration = AvrConfigFileUtils.load(getLocalConfigFile().getAbsolutePath()); } return configuration; } @Override public boolean isFileOperationSupported(FileOperation op) { if (op == FileOperation.CHANGE_DATE || op == FileOperation.CHANGE_PERMISSION) { return false; } return super.isFileOperationSupported(op); } @Override public long getLastModifiedDate() { // TODO store last modification data try { return getLocalConfigFile().getLastModifiedDate(); } catch (IOException e) { e.printStackTrace(); return System.currentTimeMillis(); } } @Override public void setLastModifiedDate(long lastModified) { } @Override public void changeReplication(short replication) throws IOException { } @Override public long getSize() { return 0; } @Override public AbstractFile getParent() { return null; } @Override public void setParent(AbstractFile parent) { } @Override public boolean exists() { return false; } @Override public PermissionBits getChangeablePermissions() { return null; } @Override public void changePermission(int access, int permission, boolean enabled) { } @Override public String getOwner() { return null; } @Override public short getReplication() { return 0; } @Override public long getBlocksize() { return 0; } @Override public boolean canGetOwner() { return false; } @Override public String getGroup() { return null; } @Override public boolean canGetGroup() { return false; } // @Override // public boolean isDirectory() { // return false; // } @Override public boolean isSymlink() { return false; } @Override public boolean isSystem() { return false; } @Override public InputStream getInputStream() throws IOException { return null; } @Override public OutputStream getOutputStream() throws IOException { return null; } @Override public OutputStream getAppendOutputStream() { return null; } @Override public RandomAccessInputStream getRandomAccessInputStream() { return null; } @Override public RandomAccessOutputStream getRandomAccessOutputStream() { return null; } @Override public void delete() throws IOException { } @Override public void renameTo(AbstractFile destFile) throws IOException { } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { } @Override public long getFreeSpace() { return 0; } @Override public long getTotalSpace() { return 0; } @Override public Object getUnderlyingFileObject() { return null; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/bzip2/Bzip2FormatProvider.java ================================================ package com.mucommander.commons.file.impl.bzip2; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; import java.io.IOException; /** * This class is the provider for the 'Bzip2' archive format. * * @author Nicolas Rinaudo, Maxence Bernard */ public class Bzip2FormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".bz2"}; private static final byte[] SIGNATURE = {}; /** * Static instance of the filename filter that matches archive filenames * */ private static final ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.BZIP2, SIGNATURE); //return new Bzip2ArchiveFile(file); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/bzip2/package.html ================================================ Provides an implementation of the bzip2 archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/cab/CabFormatProvider.java ================================================ package com.mucommander.commons.file.impl.cab; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class CabFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".cab" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = { 0x4D, 0x53, 0x43, 0x46 }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.CAB, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/cpio/CpioFormatProvider.java ================================================ package com.mucommander.commons.file.impl.cpio; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class CpioFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".cpio" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); // private final static byte[] SIGNATURE = { 0x30, 0x37, 0x30, 0x37, 0x30 }; //=google but sevenzipjbinding:C771050823 private final static byte[] SIGNATURE = { }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.CPIO, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/deb/DebFormatProvider.java ================================================ package com.mucommander.commons.file.impl.deb; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class DebFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".deb" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {}; // TODO check in libmagic source @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.AR, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ftp/FTPFile.java ================================================ package com.mucommander.commons.file.impl.ftp; import com.mucommander.commons.file.*; import com.mucommander.commons.file.connection.ConnectionHandler; import com.mucommander.commons.file.connection.ConnectionHandlerFactory; import com.mucommander.commons.file.connection.ConnectionPool; import com.mucommander.commons.io.ByteUtils; import com.mucommander.commons.io.FilteredOutputStream; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.util.StringUtils; import com.mucommander.core.FolderChangeMonitor; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPConnectionClosedException; import org.apache.commons.net.ftp.FTPReply; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.net.SocketException; import java.net.SocketTimeoutException; import java.text.SimpleDateFormat; import java.util.Date; /** * FTPFile provides access to files located on an FTP server. * *

The associated {@link FileURL} scheme is {@link FileProtocols#FTP}. The host part of the URL designates the * FTP server. Credentials must be specified in the login and password parts as FTP servers require a login and * password. The path separator is '/'. * *

Here are a few examples of valid FTP URLs: * * ftp://garfield/stuff/somefile
* ftp://john:p4sswd@garfield/stuff/somefile
* ftp://anonymous:john@somewhere.net@garfield/stuff/somefile
*
* *

Internally, FTPFile uses {@link ConnectionPool} to create FTP connections as needed and allows them to be reused * by FTPFile instances located on the same server, dealing with concurrency issues. Connections are thus managed * transparently and need not be manually managed. * *

Some FileURL properties control certain FTP connection settings: *

    *
  • {@link #PASSIVE_MODE_PROPERTY_NAME}: controls whether passive or active transfer mode, "true" for * passive mode, "false" for activemode. If the property is not specified when the connection is created, * passive mode is assumed. *
  • {@link #ENCODING_PROPERTY_NAME}: specifies the character encoding used by the server. If the property is not * specified when the connection is created, {@link #DEFAULT_ENCODING} is assumed. *
* These properties are only used when the FTP connection is created. Setting them after the connection is created * will not have any immediate effect, their values will only be used if the connection needs to be re-established. * *

Access to FTP files is provided by the Commons-net library distributed under the Apache Software License. * The {@link #getUnderlyingFileObject()} method allows to retrieve a org.apache.commons.net.ftp.FTPFile * instance corresponding to this FTPFile. * * @see ConnectionPool * @author Maxence Bernard */ public class FTPFile extends ProtocolFile implements ConnectionHandlerFactory { private static final Logger LOGGER = LoggerFactory.getLogger(FTPFile.class); private org.apache.commons.net.ftp.FTPFile file; private final String absPath; private AbstractFile parent; private boolean parentValSet; private final FilePermissions permissions; private boolean fileExists; private AbstractFile canonicalFile; private final static String SEPARATOR = "/"; /** Name of the FTP passive mode property */ public final static String PASSIVE_MODE_PROPERTY_NAME = "passiveMode"; /** Name of the FTP encoding property */ public final static String ENCODING_PROPERTY_NAME = "encoding"; /** Default FTP encoding if {@link #ENCODING_PROPERTY_NAME} is not set */ public final static String DEFAULT_ENCODING = "UTF-8"; /** Name of the property that holds the number of retries after a recoverable connection failure (connection error * or temporary server error in the 4xx range) */ public final static String NB_CONNECTION_RETRIES_PROPERTY_NAME = "nbConnectionRetries"; /** Default value if {@link #NB_CONNECTION_RETRIES_PROPERTY_NAME} is not set */ public final static int DEFAULT_NB_CONNECTION_RETRIES = 0; /** Name of the property that holds the amount of time (in seconds) to wait before retrying to connect after a * temporary connection failure. */ public final static String CONNECTION_RETRY_DELAY_PROPERTY_NAME = "connectionRetryDelay"; /** Default value if {@link #CONNECTION_RETRY_DELAY_PROPERTY_NAME} is not set */ public final static int DEFAULT_CONNECTION_RETRY_DELAY = 15; /** Date format used by the SITE UTIME command */ private final static SimpleDateFormat SITE_UTIME_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmm"); FTPFile(FileURL fileURL) throws IOException { this(fileURL, null); } FTPFile(FileURL fileURL, org.apache.commons.net.ftp.FTPFile file) throws IOException { super(fileURL); this.absPath = fileURL.getPath(); if (file == null) { this.file = getFTPFile(fileURL); // If file doesn't exist (could not be resolved), create it if (this.file == null) { String name = fileURL.getFilename(); // Filename could potentially be null this.file = createFTPFile(name == null ? "" : name, false); this.fileExists = false; } else { this.fileExists = true; } } else { this.file = file; this.fileExists = true; } this.permissions = new FTPFilePermissions(this.file); } private org.apache.commons.net.ftp.FTPFile getFTPFile(FileURL fileURL) throws IOException { // Todo: this method is very ineffective as it lists the parent directory to retrieve the information about the // requested file to workaround the fact that FTPClient#listFiles follows directories. // => Use the MLST command if supported by the server (use FEAT command to find out if it is supported). // See http://tools.ietf.org/html/draft-ietf-ftpext-mlst-16 FileURL parentURL = fileURL.getParent(); LOGGER.trace("fileURL={} parent={}", fileURL, parentURL); // Parent is null, create '/' file if (parentURL == null) { return createFTPFile("/", true); } else { FTPConnectionHandler connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); org.apache.commons.net.ftp.FTPFile[] files; try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // List files contained by this file's parent in order to retrieve the FTPFile instance corresponding // to this file files = listFiles(connHandler, parentURL.getPath()); } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } // File doesn't exist if (files == null || files.length == 0) { return null; } // Find the file in the parent folder's contents String wantedName = fileURL.getFilename(); for (org.apache.commons.net.ftp.FTPFile f : files) { if (f.getName().equalsIgnoreCase(wantedName)) { return f; } } // File doesn't exists return null; } } private org.apache.commons.net.ftp.FTPFile createFTPFile(String name, boolean isDirectory) { org.apache.commons.net.ftp.FTPFile file = new org.apache.commons.net.ftp.FTPFile(); file.setName(name); file.setSize(0); file.setTimestamp(java.util.Calendar.getInstance()); file.setType(isDirectory?org.apache.commons.net.ftp.FTPFile.DIRECTORY_TYPE:org.apache.commons.net.ftp.FTPFile.FILE_TYPE); return file; } /** * Lists and returns the contents of the given path on the server using the given connection handler. * The directory contents is listed by issuing a CWD followed by a LIST so after this method is called, the current * working directory is left to the specified path. * * @param connHandler the connection handler to use for communicating with the server * @param absPath absolute path to the directory to list * @return the directory's contents. The returned array may be empty but never null. The array may contain null * individual entries as FTPClient#listFiles's Javadoc mentions. * @throws IOException if an error occurred while communicating with the server * @throws AuthException if the user is not allowed to access this directory */ private static org.apache.commons.net.ftp.FTPFile[] listFiles(FTPConnectionHandler connHandler, String absPath) throws IOException { org.apache.commons.net.ftp.FTPFile[] files; try { // Important: the folder is listed by changing the current working directory using the CWD command and then // issuing a LIST to list the current directory, instead of issuing a LIST with the path as an argument. // So we're sending: // // CWD path // LIST // // Instead of: // // LIST path // // The reason for that is that on some servers 'LIST path with spaces' fails whereas 'CWD path with spaces' // succeeds. Most FTP clients seem to be doing this (CWD/LIST instead of LIST), there must be a reason. // // See: // http://www.mucommander.com/forums/viewtopic.php?f=4&t=714 // http://issues.apache.org/jira/browse/NET-10 connHandler.ftpClient.changeWorkingDirectory(absPath); files = connHandler.ftpClient.listFiles(); // Throw an IOException if server replied with an error connHandler.checkServerReply(); if (files==null) // In some rare conditions (bug) this method can return null return new org.apache.commons.net.ftp.FTPFile[0]; return files; } // This exception is not an IOException and needs to be caught and thrown back as an IOException catch(org.apache.commons.net.ftp.parser.ParserInitializationException e) { LOGGER.info("ParserInitializationException caught", e); throw new IOException(); } catch(IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Throw back the IOException throw e; } } ///////////////////////////////////////////// // ConnectionHandlerFactory implementation // ///////////////////////////////////////////// public ConnectionHandler createConnectionHandler(FileURL location) { return new FTPConnectionHandler(location); } ///////////////////////////////////////// // AbstractFile methods implementation // ///////////////////////////////////////// @Override public boolean isSymlink() { return file.isSymbolicLink(); } @Override public boolean isSystem() { return false; } @Override public long getLastModifiedDate() { if (isSymlink()) { return ((org.apache.commons.net.ftp.FTPFile) getCanonicalFile().getUnderlyingFileObject()).getTimestamp().getTimeInMillis(); } return file.getTimestamp().getTimeInMillis(); } /** * Attempts to change this file's date using the 'SITE UTIME' FTP command. * This command seems to be implemented by modern FTP servers such as ProFTPd or PureFTP Server but since it is not * part of the basic FTP command set, it may as well not be supported by the remote server. */ @Override public void setLastModifiedDate(long lastModified) throws IOException { // Note: FTPFile.setTimeStamp only changes the instance's date, but doesn't change it on the server-side. FTPConnectionHandler connHandler = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); // Throw UnsupportedFileOperationException if we know the 'SITE UTIME' command is not supported by the server if (!connHandler.utimeCommandSupported) { throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } // Makes sure the connection is started, if not starts it connHandler.checkConnection(); String sdate; // SimpleDateFormat instance must be synchronized externally if it is accessed concurrently synchronized(SITE_UTIME_DATE_FORMAT) { sdate = SITE_UTIME_DATE_FORMAT.format(new Date(lastModified)); } LOGGER.info("sending SITE UTIME {} {}", sdate, absPath); boolean success = connHandler.ftpClient.sendSiteCommand("UTIME "+sdate+" "+absPath); LOGGER.info("server reply: {}", connHandler.ftpClient.getReplyString()); if (!success) { int replyCode = connHandler.ftpClient.getReplyCode(); // If server reported that the command is not supported, mark it in the ConnectionHandler so that // we don't try it anymore if(replyCode==FTPReply.UNRECOGNIZED_COMMAND || replyCode==FTPReply.COMMAND_NOT_IMPLEMENTED || replyCode==FTPReply.COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER) { LOGGER.info("marking UTIME command as unsupported"); connHandler.utimeCommandSupported = false; } throw new IOException(); } } catch (IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection if(connHandler!=null) connHandler.checkSocketException(e); throw e; } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } @Override public long getSize() { if (isSymlink()) { return ((org.apache.commons.net.ftp.FTPFile) getCanonicalFile().getUnderlyingFileObject()).getSize(); } return file.getSize(); } @Override public AbstractFile getParent() { if (!parentValSet) { FileURL parentFileURL = this.fileURL.getParent(); if (parentFileURL != null) { try { parent = FileFactory.getFile(parentFileURL, null, createFTPFile(parentFileURL.getFilename(), true)); } catch(IOException e) { // No parent, that's all } } parentValSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValSet = true; } @Override public boolean exists() { return this.fileExists; } @Override public FilePermissions getPermissions() { if (isSymlink()) { FTPFile ancestor = getCanonicalFile().getAncestor(FTPFile.class); return ancestor != null ? ancestor.permissions : null; } return permissions; } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { changePermissions(ByteUtils.setBit(permissions.getIntValue(), (permission << (access*3)), enabled)); } /** * Returns {@link PermissionBits#FULL_PERMISSION_BITS} if the server supports the 'site chmod' command (not all * servers do), {@link PermissionBits#EMPTY_PERMISSION_BITS} otherwise. * * @return {@link PermissionBits#FULL_PERMISSION_BITS} if the server supports the 'site chmod' command (not all * servers do), {@link PermissionBits#EMPTY_PERMISSION_BITS} otherwise */ @Override public PermissionBits getChangeablePermissions() { try { // Do not lock the connection handler, not needed. return ((FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, false)).chmodCommandSupported ?PermissionBits.FULL_PERMISSION_BITS // Full permission support (777 octal) :PermissionBits.EMPTY_PERMISSION_BITS; // Permissions can't be changed } catch (InterruptedIOException e) { // Should not happen in practice return PermissionBits.EMPTY_PERMISSION_BITS; // Permissions can't be changed } } @Override public String getOwner() { return file.getUser(); } @Override public boolean canGetOwner() { return true; } @Override public String getGroup() { return file.getGroup(); } @Override public boolean canGetGroup() { return true; } @Override public boolean isDirectory() { // org.apache.commons.net.ftp.FTPFile#isDirectory() returns false if the file is a symlink pointing to a // directory, this is a limitation of the Commons-net library. // Todo: fix this by either: // a) find a combination of 'LIST' switches which allows the output to contain both the 'is symlink' and the // 'is the symlink target a directory' information. At a first glance, there doesn't seem to be one: either // symlinks are followed or there aren't. // b) Patch #ls() to issue an extra 'LIST -ldH *' to retrieve all symlinks' information when the directory has // at least one symlink. // c) if this file is a symlink, retrieve the symlink's target using #getFTPFile(FileURL) with '-ldH' switches // and return the value of isDirectory(). This clearly is the least effective solution at it requires issuing // one 'ls' command per symlink. if (isSymlink()) { return ((org.apache.commons.net.ftp.FTPFile) getCanonicalFile().getUnderlyingFileObject()).isDirectory(); } return file.isDirectory(); } @Override public InputStream getInputStream() throws IOException { return getInputStream(0); } @Override public OutputStream getOutputStream() throws IOException { return new FTPOutputStream(false); } @Override public OutputStream getAppendOutputStream() throws IOException { return new FTPOutputStream(true); } /** * Always throws an {@link UnsupportedFileOperationException}: random read access is not available. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE); } // public RandomAccessInputStream getRandomAccessInputStream() throws IOException { // return new FTPRandomAccessInputStream(); // } /** * Always throws an {@link UnsupportedFileOperationException}: random write access is not available. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } @Override public void delete() throws IOException { // Retrieve a ConnectionHandler and lock it FTPConnectionHandler connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); if (isDirectory()) { connHandler.ftpClient.removeDirectory(absPath); } else { connHandler.ftpClient.deleteFile(absPath); } fileExists = false; // need be false because the file can be get from cache pool // Throw an IOException if server replied with an error connHandler.checkServerReply(); } catch(IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Re-throw IOException throw e; } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } } @Override public AbstractFile[] ls() throws IOException { // Retrieve a ConnectionHandler and lock it FTPConnectionHandler connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); org.apache.commons.net.ftp.FTPFile[] files; try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); files = listFiles(connHandler, absPath); } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } if (files == null || files.length == 0) { return new AbstractFile[]{}; } AbstractFile[] children = new AbstractFile[files.length]; int nbFiles = files.length; int fileCount = 0; String parentPath = fileURL.getPath(); if (!parentPath.endsWith(SEPARATOR)) { parentPath += SEPARATOR; } for (org.apache.commons.net.ftp.FTPFile file1 : files) { if (file1 == null) { continue; } String childName = file1.getName(); if (childName.equals(".") || childName.equals("..")) { continue; } // Note: properties and credentials are cloned for every children's url FileURL childURL = (FileURL) fileURL.clone(); childURL.setPath(parentPath + childName); // Discard '.' and '..' files if (childName.equals(".") || childName.equals("..")) { continue; } AbstractFile child = FileFactory.getFile(childURL, this, file1); children[fileCount++] = child; } // create new array of the exact file count if (fileCount < nbFiles) { AbstractFile[] newChildren = new AbstractFile[fileCount]; System.arraycopy(children, 0, newChildren, 0, fileCount); return newChildren; } return children; } @Override public void mkdir() throws IOException { // Retrieve a ConnectionHandler and lock it FTPConnectionHandler connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); connHandler.ftpClient.makeDirectory(absPath); // Throw an IOException if server replied with an error connHandler.checkServerReply(); file.setType(org.apache.commons.net.ftp.FTPFile.DIRECTORY_TYPE); fileExists = true; } catch(IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Re-throw IOException throw e; } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } /** * Returns an org.apache.commons.net.FTPFile instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } /** * Changes permissions using the SITE CHMOD FTP command. * * This command is optional but seems to be supported by modern FTP servers such as ProFTPd or PureFTP Server. * But it may as well not be supported by the remote FTP server as it is not part of the basic FTP command set. * * Implementation note: FTPFile.setPermission only changes the instance's permissions, but doesn't change it on the * server-side. */ @Override public void changePermissions(int permissions) throws IOException { FTPConnectionHandler connHandler = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); // Return if we know the CHMOD command is not supported by the server if(!connHandler.chmodCommandSupported) throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); LOGGER.info("sending SITE CHMOD {} {}", Integer.toOctalString(permissions), absPath); boolean success = connHandler.ftpClient.sendSiteCommand("CHMOD "+Integer.toOctalString(permissions)+" "+absPath); LOGGER.info("server reply: {}", connHandler.ftpClient.getReplyString()); if (!success) { int replyCode = connHandler.ftpClient.getReplyCode(); // If server reported that the command is not supported, mark it in the ConnectionHandler so that // we don't try it anymore if(replyCode==FTPReply.UNRECOGNIZED_COMMAND || replyCode==FTPReply.COMMAND_NOT_IMPLEMENTED || replyCode==FTPReply.COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER) { LOGGER.info("marking CHMOD command as unsupported"); connHandler.chmodCommandSupported = false; } throw new IOException(); } } catch(IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection if (connHandler != null) { connHandler.checkSocketException(e); } throw e; } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } /** * Implementation notes: always throws an {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Implementation notes: server-to-server renaming will work if the destination file also uses the 'FTP' scheme * and is located on the same host. */ @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination checkRenamePrerequisites(destFile, false, false); FTPConnectionHandler connHandler = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(this, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); if(!connHandler.ftpClient.rename(absPath, destFile.getURL().getPath())) throw new IOException(); } catch(IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection if(connHandler!=null) connHandler.checkSocketException(e); throw e; } finally { // Release the lock on the ConnectionHandler if (connHandler!=null) { connHandler.releaseLock(); } } } @Override public InputStream getInputStream(long offset) throws IOException { return new FTPInputStream(offset); } @Override public AbstractFile getCanonicalFile() { if (!isSymlink()) { return this; } // create the canonical file instance and cache it if (canonicalFile == null) { // getLink() returns the raw symlink target which can either be an absolute or a relative path. If the path is // relative, prepared the absolute path of the symlink's parent folder. String symlinkTargetPath = file.getLink(); if (!symlinkTargetPath.startsWith("/")) { String parentPath = fileURL.getParent().getPath(); if (!parentPath.endsWith("/")) { parentPath += "/"; } symlinkTargetPath = parentPath + symlinkTargetPath; } FileURL canonicalURL = (FileURL)fileURL.clone(); canonicalURL.setPath(symlinkTargetPath); canonicalFile = FileFactory.getFile(canonicalURL); } return canonicalFile; } /** * If the FTPFile is a symbolic link, this method returns the name of the file being pointed to by the symbolic link. * @return The file pointed to by the symbolic link (null if the FTPFile is not a symbolic link). */ public String getLink() { return file.getLink(); } /////////////////// // Inner classes // /////////////////// // private class FTPProcess extends AbstractProcess { // // /** True if the command returned a positive FTP reply code */ // private boolean success; // // /** Allows to read the command's output */ // private ByteArrayInputStream bais; // // // public FTPProcess(String tokens[]) throws IOException { // // // Concatenates all tokens to create the command string // String command = ""; // int nbTokens = tokens.length; // for(int i=0; i 0) { // Resume transfer at the given offset connHandler.ftpClient.setRestartOffset(skipBytes); } in = connHandler.ftpClient.retrieveFileStream(absPath); if (in == null) { if (skipBytes > 0) { // Reset offset connHandler.ftpClient.setRestartOffset(0); } throw new IOException(); } } catch(IOException e) { if (connHandler != null) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Release the lock on the ConnectionHandler if the InputStream could not be created connHandler.releaseLock(); } // Re-throw IOException throw e; } } @Override public void close() throws IOException { // Make sure this method is only executed once, otherwise FTPClient#completePendingCommand() would lock if (isClosed) { return; } // we need to refresh the file after an update // otherwise the displayed size of archive files is incorrect file = getFTPFile(getURL()); isClosed = true; try { super.close(); LOGGER.info("complete pending commands"); connHandler.ftpClient.completePendingCommand(); LOGGER.info("commands completed"); // Todo: An IOException will be thrown by completePendingCommand if the transfer has not finished before calling close. // An 'abort' command should be issued to the server before closing if the transfer is not finished yet. // Currently in that case (transfer not finished) the whole connection has to be re-established (bad!). // FTPClient#abort() is difficult to use to say the least. This post gives some insight: http://mail-archives.apache.org/mod_mbox/commons-user/200604.mbox/%3c78A73ABD8DB470439179DB682EA990B3025B87DF@mtlex02.NEXXLINK.INT%3e } catch (IOException e) { LOGGER.info("exception in completePendingCommands()", e); // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Do not re-throw the exception because an IOException will be thrown if close is called before // the transfer is finished (see above) which is pseudo-normal behavior (though sub-optimal). // // Re-throw IOException // throw e; } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } } } // This class works but because of the bug in FTPInputStream#close() which fails to interrupt an ongoing transfer // gracefully, seek() will re-establish the FTP connection each time it is called, which is definitely not acceptable. // Therefore, this class cannot be used at the moment. private class FTPRandomAccessInputStream extends RandomAccessInputStream { private FTPInputStream in; private long offset; private FTPRandomAccessInputStream() throws IOException { this.in = new FTPInputStream(0); } @Override public int read() throws IOException { int read = in.read(); if (read != -1) { offset += 1; } return read; } @Override public int read(byte[] b, int off, int len) throws IOException { int nbRead = in.read(b, off, len); if (nbRead != -1) { offset += nbRead; } return nbRead; } public long getOffset() { return offset; } public long getLength() { return FTPFile.this.getSize(); } public void seek(final long offset) throws IOException { try { in.close(); } catch(IOException ignore) {} in = new FTPInputStream(offset); this.offset = offset; } @Override public void close() throws IOException { in.close(); } } private class FTPOutputStream extends FilteredOutputStream { private FTPConnectionHandler connHandler; private boolean isClosed; private FTPOutputStream(boolean append) throws IOException { super(null); try { // Retrieve a ConnectionHandler and lock it connHandler = (FTPConnectionHandler)ConnectionPool.getConnectionHandler(FTPFile.this, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); if (append) { out = connHandler.ftpClient.appendFileStream(absPath); } else { out = connHandler.ftpClient.storeFileStream(absPath); // Note: do NOT use storeUniqueFileStream which appends .1 if the file already exists and fails with proftpd } if (out == null) { throw new IOException(); } } catch(IOException e) { if (connHandler != null) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Release the lock on the ConnectionHandler if the OutputStream could not be created connHandler.releaseLock(); } // Re-throw IOException throw e; } } @Override public void close() throws IOException { // Make sure this method is only executed once, otherwise FTPClient#completePendingCommand() would lock if (isClosed) { return; } // we need to refresh the file after update // otherwise the file size for archives will be show incorrect etc. try { FTPFile.this.file = getFTPFile(getURL()); } catch (IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); } // force to refresh folder pane with this file FolderChangeMonitor.addFileToRefresh(getAbsolutePath()); isClosed = true; try { super.close(); LOGGER.trace("complete pending commands"); if (connHandler != null && connHandler.ftpClient != null) { connHandler.ftpClient.completePendingCommand(); } LOGGER.trace("commands completed"); } catch(IOException e) { LOGGER.info("exception in completePendingCommands()", e); // Checks if the IOException corresponds to a socket error and in that case, closes the connection connHandler.checkSocketException(e); // Re-throw IOException throw e; } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } } } /** * Handles connection to an FTP server. */ private static class FTPConnectionHandler extends ConnectionHandler { private FTPClient ftpClient; // private CustomFTPClient ftpClient; /** Controls whether passive mode should be used for data transfers (default is true) */ private final boolean passiveMode; /** Encoding used by the FTP control connection */ private String encoding; /** Number of connection retry attempts after a recoverable connection failure */ private int nbConnectionRetries; /** Amount of time (in seconds) to wait before retrying to connect after a recoverable connection failure */ private int connectionRetryDelay; /** False if SITE UTIME command is not supported by the remote server (once tried and failed) */ private boolean utimeCommandSupported = true; /** False if SITE CHMOD command is not supported by the remote server (once tried and failed) */ private boolean chmodCommandSupported = true; /** Controls how ofter should keepAlive() be called by ConnectionPool */ private final static long KEEP_ALIVE_PERIOD = 60; // /** Connection timeout to the FTP server in seconds */ // private final static int CONNECTION_TIMEOUT = 30; // private class CustomFTPClient extends FTPClient { // // private Socket getSocket() { // return _socket_; // } // } private FTPConnectionHandler(FileURL location) { super(location); // Use the passive mode property if it is set String passiveModeProperty = location.getProperty(PASSIVE_MODE_PROPERTY_NAME); // Passive mode is enabled by default if property isn't specified this.passiveMode = passiveModeProperty==null || !passiveModeProperty.equals("false"); // Use the encoding property if it is set this.encoding = location.getProperty(ENCODING_PROPERTY_NAME); if (StringUtils.isNullOrBlank(encoding)) { encoding = DEFAULT_ENCODING; } // Use the property that controls the number of connection retries after a recoverable connection failure, // if the property is set String prop = location.getProperty(NB_CONNECTION_RETRIES_PROPERTY_NAME); if (prop==null) { nbConnectionRetries = DEFAULT_NB_CONNECTION_RETRIES; } else { try { nbConnectionRetries = Integer.parseInt(prop); } catch(NumberFormatException e) { nbConnectionRetries = DEFAULT_NB_CONNECTION_RETRIES; } } // Use the property that controls the connection retry delay after a recoverable connection failure, // if the property is set prop = location.getProperty(CONNECTION_RETRY_DELAY_PROPERTY_NAME); if (prop == null) { connectionRetryDelay = DEFAULT_CONNECTION_RETRY_DELAY; } else { try { connectionRetryDelay = Integer.parseInt(prop); } catch(NumberFormatException e) { connectionRetryDelay = DEFAULT_CONNECTION_RETRY_DELAY; } } setKeepAlivePeriod(KEEP_ALIVE_PERIOD); } /** * Checks the last server reply code and throws an IOException if the code doesn't correspond to a positive * FTP reply: * *

    *
  • If the reply is a credentials error (lack of permissions or not logged in), an {@link AuthException} * is thrown. For all other error codes, an IOException is thrown with the server reply message. *
  • If the reply code is FTPReply.SERVICE_NOT_AVAILABLE (connection dropped prematurely), the connection * will be closed before an IOException with the server reply message is thrown. *
* *

If the reply is a positive one (not an error error), this method does nothing. */ private void checkServerReply() throws IOException { // Check that connection went ok int replyCode = ftpClient.getReplyCode(); LOGGER.trace("server reply="+ftpClient.getReplyString()); // Close connection if the connection dropped prematurely so that isConnected() returns false if (replyCode == FTPReply.SERVICE_NOT_AVAILABLE) { closeConnection(); } // If not, throw an exception using the reply string if (!FTPReply.isPositiveCompletion(replyCode)) { if (replyCode == FTPReply.BAD_COMMAND_SEQUENCE || replyCode == FTPReply.NEED_PASSWORD || replyCode == FTPReply.NOT_LOGGED_IN) { throwAuthException(ftpClient.getReplyString()); } else { throw new IOException(ftpClient.getReplyString()); } } } /** * Checks if the given IOException corresponds to a low-level socket exception, and if that is the case, * closes the connection so that {@link #isConnected()} returns false. * All IOException raised by FTPClient should be checked by this method so that socket errors are properly detected. */ private void checkSocketException(IOException e) { if (((e instanceof FTPConnectionClosedException) || (e instanceof SocketException) || (e instanceof SocketTimeoutException)) && isConnected()) { LOGGER.info("socket exception detected, closing connection", e); closeConnection(); } } ////////////////////////////////////// // ConnectionHandler implementation // ////////////////////////////////////// @Override public void startConnection() throws IOException { LOGGER.info("connecting to {}", getRealm().getHost()); // this.ftpClient = new CustomFTPClient(); this.ftpClient = new FTPClient(); int retriesLeft = nbConnectionRetries; int retryDelay = connectionRetryDelay * 1000; do { try { FileURL realm = getRealm(); // Override default port (21) if a custom port was specified in the URL int port = realm.getPort(); LOGGER.info("custom port={}", port); if (port >= 0) { ftpClient.setDefaultPort(port); } // Sets the control encoding // - most modern FTP servers seem to default to UTF-8, but not all of them do. // - commons-ftp defaults to ISO-8859-1 which is not good // Note: this has to be done before the connection is established otherwise it won't be taken into account LOGGER.info("encoding={}", encoding); ftpClient.setControlEncoding(encoding); // Connect to the FTP server ftpClient.connect(realm.getHost()); // // Set a socket timeout: default value is 0 (no timeout) // ftpClient.setSoTimeout(CONNECTION_TIMEOUT*1000); // FileLogger.finer("soTimeout="+ftpClient.getSoTimeout()); // Throw an IOException if server replied with an error checkServerReply(); Credentials credentials = getCredentials(); // Throw an AuthException if there are no credentials LOGGER.info("fileURL={} credentials={}", realm.toString(true), credentials); if (credentials == null) { throwAuthException(null); } // Login if (credentials != null) { ftpClient.login(credentials.getLogin(), credentials.getPassword()); } // Throw an IOException (potentially an AuthException) if the server replied with an error checkServerReply(); // Enables/disables passive mode LOGGER.info("passiveMode={}", passiveMode); if (passiveMode) { this.ftpClient.enterLocalPassiveMode(); } else { this.ftpClient.enterLocalActiveMode(); } // Set file type to 'binary' ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); // Issue 'LIST -al' command to list hidden files (instead of LIST -l), only if the corresponding // configuration option has been manually enabled in the preferences. // The reason for not doing so by default is that the commons-net library will fail to properly parse // directory listings on some servers when 'LIST -al' is used (bug). // Note that by default, if 'LIST -l' is used, the decision to list hidden files is left to the // FTP server: some servers will choose to show them, some will not. This behavior is usually a // configuration setting of the FTP server. ftpClient.setListHiddenFiles(FTPProtocolProvider.getForceHiddenFilesListing()); if (encoding.equalsIgnoreCase("UTF-8")) { // This command enables UTF8 on the remote server... but only a few FTP servers currently support this command ftpClient.sendCommand("OPTS UTF8 ON"); } break; } catch(IOException e) { // Attempt to retry if the connection failed, or if the server reply corresponds to a temporary error. // Unlike 5xx errors which are permanent, 4xx errors are temporary and may be retried, quote from // RFC 959: "The command was not accepted and the requested action did not take place, but the error // condition is temporary and the action may be requested again." int replyCode = ftpClient.getReplyCode(); if (!ftpClient.isConnected() || FTPReply.isNegativeTransient(replyCode)) { LOGGER.info((!ftpClient.isConnected()?"Connection error":"Temporary server error ("+replyCode+")")+", retries left="+retriesLeft, e); // Retry to connect, if we have at least an attempt left if (retriesLeft > 0) { retriesLeft--; // Wait before retrying if (retryDelay > 0) { LOGGER.info("waiting {} ms before retrying to connect", retryDelay); try { Thread.sleep(retryDelay); } catch(InterruptedException ignore) {} } continue; } } // Disconnect if the connection could not be established if (ftpClient.isConnected()) try { ftpClient.disconnect(); } catch(IOException ignore) {} // Re-throw the exception throw e; } } while(true); } @Override public boolean isConnected() { // FTPClient#isConnected() will always return true once it is connected and does not detect socket // disconnections. Furthermore, retrieving the underlying Socket instance does not help any more: // Socket#isConnected() and Socket#isClosed() do not reflect socket errors that happen after the socket is // connected. // Thus, the only way (AFAIK) to know if the socket is still connected is to intercept all IOException // thrown by FTPClient and check if they correspond to a socket exception. return ftpClient != null && ftpClient.isConnected(); // if(ftpClient==null || !ftpClient.isConnected()) // return false; // // Socket socket = ftpClient.getSocket(); // FileLogger.finest("socket="+socket+" socket.isConnected()"+socket.isConnected()+" socket.isClosed()="+socket.isClosed()); // // return socket!=null && socket.isConnected() && !socket.isClosed(); } @Override public void closeConnection() { if (ftpClient != null) { // Try to logout, this may fail if the connection is broken try { ftpClient.logout(); } catch(IOException e) { e.printStackTrace(); } // Close the socket connection try { ftpClient.disconnect(); } catch(IOException e) { e.printStackTrace(); } ftpClient = null; } } @Override public void keepAlive() { // Send a NOOP command to the server to keep the connection alive. // Note: not all FTP servers support the NOOP command. if (ftpClient != null) { try { ftpClient.sendNoOp(); } catch (IOException e) { // Checks if the IOException corresponds to a socket error and in that case, closes the connection checkSocketException(e); } } } } /** * A Permissions implementation for FTPFile. */ private static class FTPFilePermissions extends IndividualPermissionBits implements FilePermissions { private final org.apache.commons.net.ftp.FTPFile file; FTPFilePermissions(org.apache.commons.net.ftp.FTPFile file) { this.file = file; } public boolean getBitValue(int access, int type) { int fAccess; if (access == USER_ACCESS) { fAccess = org.apache.commons.net.ftp.FTPFile.USER_ACCESS; } else if (access == GROUP_ACCESS) { fAccess = org.apache.commons.net.ftp.FTPFile.GROUP_ACCESS; } else if (access == OTHER_ACCESS) { fAccess = org.apache.commons.net.ftp.FTPFile.WORLD_ACCESS; } else { return false; } int fPermission; if (type == READ_PERMISSION) { fPermission = org.apache.commons.net.ftp.FTPFile.READ_PERMISSION; } else if (type == WRITE_PERMISSION) { fPermission = org.apache.commons.net.ftp.FTPFile.WRITE_PERMISSION; } else if (type == EXECUTE_PERMISSION) { fPermission = org.apache.commons.net.ftp.FTPFile.EXECUTE_PERMISSION; } else { return false; } return file.hasPermission(fAccess, fPermission); } public PermissionBits getMask() { return FULL_PERMISSION_BITS; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ftp/FTPProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.ftp; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import java.io.IOException; /** * This class is the provider for the FTP filesystem implemented by {@link com.mucommander.commons.file.impl.ftp.FTPFile}. * * @author Nicolas Rinaudo * @see com.mucommander.commons.file.impl.ftp.FTPFile */ public class FTPProtocolProvider implements ProtocolProvider { /** Controls whether to force the listing of hidden files */ private static boolean forceHiddenFilesListing = false; /** * Controls whether to force the listing of hidden files. Enabling this option will cause 'LIST -al' commands * to be issued when listing files, instead of 'LIST -l'. * When this option is disabled, the decision to list hidden files is left to the FTP server: some servers will * choose to show them, some will not. This behavior is usually a configuration setting of the FTP server. *

* This option is disabled by default. The reason for this is that the commons-net library will fail to properly * parse directory listings on some servers when 'LIST -al' is used (bug). * * @param value true to force the listing of hidden files, false to leave it for the * server to decide whether to show hidden files or not. */ // Todo: check if this is still needed after upgrading to commons-net 2.0 // Todo: this should not be a configuration variable but rather a FileURL property public static void setForceHiddenFilesListing(boolean value) { forceHiddenFilesListing = value; } /** * Returns true if the listing of hidden files is forced, false if the decision to show * them is left to the server. * * @return true if the listing of hidden files is forced, false if the decision to show * them is left to the server. * @see #setForceHiddenFilesListing(boolean) */ public static boolean getForceHiddenFilesListing() { return forceHiddenFilesListing; } @Override public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return instantiationParams.length==0 ?new FTPFile(url) :new FTPFile(url, (org.apache.commons.net.ftp.FTPFile)instantiationParams[0]); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/ftp/package.html ================================================ Provides an implementation of the FTP protocol. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/gzip/GzipArchiveFile.java ================================================ package com.mucommander.commons.file.impl.gzip; import com.mucommander.commons.file.*; import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; /** * GzipArchiveFile provides read-only access to archives in the Gzip format. * *

The actual decompression work is performed by the {@link java.util.zip.GZIPInputStream} class. * * @see com.mucommander.commons.file.impl.gzip.GzipFormatProvider * @author Maxence Bernard */ public class GzipArchiveFile extends AbstractROArchiveFile { /** * Creates a GzipArchiveFile on top of the given file. * * @param file the underlying file to wrap this archive file around */ public GzipArchiveFile(AbstractFile file) { super(file); } //////////////////////////////////////// // AbstractArchiveFile implementation // //////////////////////////////////////// @Override public ArchiveEntryIterator getEntryIterator() { String extension = getExtension(); String name = getName(); if (extension != null) { extension = extension.toLowerCase(); // Remove the 'gz' or 'tgz' extension from the entry's name if (extension.equals("tgz")) { name = name.substring(0, name.length()-3) + "tar"; } else if (extension.equals("gz")) { name = name.substring(0, name.length()-3); } } return new SingleArchiveEntryIterator(new ArchiveEntry("/"+name, false, getLastModifiedDate(), -1, true)); } @Override public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException { return new GZIPInputStream(getInputStream()); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/gzip/GzipFormatProvider.java ================================================ package com.mucommander.commons.file.impl.gzip; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; import java.io.IOException; /** * This class is the provider for the 'Gzip' archive format. * * @see com.mucommander.commons.file.impl.gzip.GzipArchiveFile * @author Nicolas Rinaudo, Maxence Bernard */ public class GzipFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".gz"}; private final static byte[] SIGNATURE = {}; /** * Static instance of the filename filter that matches archive filenames */ private static final ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.GZIP, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/gzip/package.html ================================================ Provides an implementation of the gzip archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/hadoop/HDFSFile.java ================================================ package com.mucommander.commons.file.impl.hadoop; import com.mucommander.commons.file.*; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.permission.FsPermission; //import org.apache.hadoop.security.UnixUserGroupInformation; import java.io.IOException; import java.net.URI; /** * {@link HadoopFile} implementation for the HDFS protocol. * * @author Maxence Bernard */ public class HDFSFile extends HadoopFile { // TODO: allow a custom group to be set (see TODO below) // /** Name of the property holding the file's group */ // public static final String GROUP_PROPERTY_NAME = "group"; /** Default username */ private static final String DEFAULT_USERNAME; /** Default group */ private static final String DEFAULT_GROUP; /** Default file permissions */ private final static FilePermissions DEFAULT_PERMISSIONS = new SimpleFilePermissions( FsPermission.getDefault().applyUMask(FsPermission.getUMask(DEFAULT_CONFIGURATION)).toShort() & PermissionBits.FULL_PERMISSION_INT ); static { // try { // UnixUserGroupInformation ugi = UnixUserGroupInformation.login(DEFAULT_CONFIGURATION); // DEFAULT_USERNAME = ugi.getUserName(); // // Do not use default groups, as these are pretty much useless // } // catch(Exception e) { // // Should never happen but default to a reasonable value if it does // DEFAULT_USERNAME = System.getProperty("user.name"); // } DEFAULT_USERNAME = System.getProperty("user.name"); DEFAULT_GROUP = DEFAULT_CONFIGURATION.get("dfs.permissions.supergroup", "supergroup"); } protected HDFSFile(FileURL url) throws IOException { super(url); } protected HDFSFile(FileURL url, FileSystem fs, FileStatus fileStatus) throws IOException { super(url, fs, fileStatus); } public static String getDefaultUsername() { return DEFAULT_USERNAME; } public static String getDefaultGroup() { return DEFAULT_GROUP; } private static String getUsername(FileURL url) { Credentials credentials = url.getCredentials(); String username; if (credentials == null || (username = credentials.getLogin()).isEmpty()) username = getDefaultUsername(); return username; } private static String getGroup(FileURL url) { // // Import the group from the URL's 'group' property, if set // String group = url.getProperty(GROUP_PROPERTY_NAME); // if(group==null || group.isEmpty() // group = getDefaultGroup(); // // return group; return getDefaultGroup(); } @Override protected FileSystem getHadoopFileSystem(FileURL url) throws IOException { // Note: getRealm returns a fresh instance every time FileURL realm = url.getRealm(); Configuration conf = new Configuration(); // Import the user from the URL's authority, if set // TODO: for some reason, setting the group has no effect: files are still created with the default supergroup //conf.setStrings(UnixUserGroupInformation.UGI_PROPERTY_NAME, getUsername(url), getGroup(url)); if (url.containsCredentials()) { try { return FileSystem.get(URI.create(realm.toString(false)), conf, url.getCredentials().getLogin()); } catch (InterruptedException e) { throw new IOException(e); } } else { return FileSystem.get(URI.create(realm.toString(false)), conf); } } @Override protected void setDefaultFileAttributes(FileURL url, HadoopFile.HadoopFileAttributes atts) { atts.setOwner(getUsername(url)); atts.setGroup(getGroup(url)); atts.setPermissions(DEFAULT_PERMISSIONS); } @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/hadoop/HDFSProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.hadoop; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import java.io.IOException; /** * A file protocol provider for the Hadoop HDFS filesystem. * * @author Maxence Bernard */ public class HDFSProtocolProvider implements ProtocolProvider { public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return instantiationParams.length==0 ?new HDFSFile(url) :new HDFSFile(url, (FileSystem)instantiationParams[0], (FileStatus)instantiationParams[1]); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/hadoop/HadoopFile.java ================================================ package com.mucommander.commons.file.impl.hadoop; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.io.*; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.*; import org.apache.hadoop.fs.permission.FsPermission; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * This abstact class provides access to the Hadoop virtual filesystem, which, like the muCommander file API, provides a * unified access to a number of file protocols. * *

{@link ProtocolFile} is fully implemented by HadoopFile. All is left for subclasses is to implement * the abstract methods defined in this class. * * @see HDFSFile * @author Maxence Bernard */ public abstract class HadoopFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(HadoopFile.class); /** The Hadoop FileSystem object */ private final FileSystem fs; /** The Hadoop */ private final Path path; /** Holds file attributes */ private final HadoopFileAttributes fileAttributes; /** Cached parent file instance, null if not created yet or if this file has no parent */ private AbstractFile parent; /** Has the parent file been determined yet? */ private boolean parentValSet; /** True if this file is currently being written */ private boolean isWriting; /** Default Hadoop Configuration, whose values are fetched from XML configuration files. */ final static Configuration DEFAULT_CONFIGURATION = new Configuration(); HadoopFile(FileURL url) throws IOException { this(url, null, null); } HadoopFile(FileURL url, FileSystem fs, FileStatus fileStatus) throws IOException { super(url); if(fs==null) { try { this.fs = getHadoopFileSystem(url); } catch(IOException e) { throw e; } catch(Exception e) { // FileSystem implementations throw IllegalArgumentException under various circumstances throw new IOException(e.getMessage()); } } else { this.fs = fs; } if (fileStatus == null) { this.path = new Path(fileURL.getPath()); this.fileAttributes = new HadoopFileAttributes(); } else { this.fileAttributes = new HadoopFileAttributes(fileStatus); this.path = fileStatus.getPath(); } } private OutputStream getOutputStream(boolean append) throws IOException { OutputStream out = new CounterOutputStream( append?fs.append(path):fs.create(path, true), new ByteCounter() { @Override public synchronized void add(long nbBytes) { fileAttributes.addToSize(nbBytes); fileAttributes.setDate(System.currentTimeMillis()); } } ) { @Override public void close() throws IOException { super.close(); isWriting = false; } }; // Update local attributes fileAttributes.setExists(true); fileAttributes.setDate(System.currentTimeMillis()); fileAttributes.setSize(0); isWriting = true; return out; } ///////////////////////////////// // AbstractFile implementation // ///////////////////////////////// @Override public AbstractFile getParent() { if(!parentValSet) { FileURL parentFileURL = this.fileURL.getParent(); if(parentFileURL!=null) parent = FileFactory.getFile(fileURL.getParent()); parentValSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValSet = true; } @Override public Object getUnderlyingFileObject() { return fileAttributes; } // File attributes manipulation @Override public boolean exists() { return fileAttributes.exists(); } @Override public boolean isDirectory() { return fileAttributes.isDirectory(); } /** * Always returns false, Hadoop filesystems have no symlink support. * * @return returns false, Hadoop filesystems have no symlink support. */ @Override public boolean isSymlink() { // No support for symlinks return false; } @Override public boolean isSystem() { return false; } @Override public long getLastModifiedDate() { return fileAttributes.getLastModifiedDate(); } @Override public long getSize() { return fileAttributes.getSize(); } @Override public PermissionBits getChangeablePermissions() { return FilePermissions.FULL_PERMISSION_BITS; } @Override public FilePermissions getPermissions() { return fileAttributes.getPermissions(); } @Override public String getOwner() { return fileAttributes.getOwner(); } @Override public boolean canGetOwner() { return true; } @Override public String getGroup() { return fileAttributes.getGroup(); } @Override public boolean canGetGroup() { return true; } @Override public short getReplication() { return fileAttributes.getReplication(); } @Override public long getBlocksize() { return fileAttributes.getBlockSize(); } // Supported file operations @Override public void mkdir() throws IOException { if(exists() || !fs.mkdirs(path)) throw new IOException(); // Update local attributes fileAttributes.setExists(true); fileAttributes.setDirectory(true); fileAttributes.setDate(System.currentTimeMillis()); fileAttributes.setSize(0); } @Override public void delete() throws IOException { if(!fs.delete(path, false)) throw new IOException(); // Update local attributes fileAttributes.setExists(false); fileAttributes.setDirectory(false); fileAttributes.setSize(0); } @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination checkRenamePrerequisites(destFile, false, false); // Delete the destination if it already exists as FileSystem#rename would otherwise fail. // Note: HadoopFile#delete() does not delete directories recursively (good). if(destFile.exists()) destFile.delete(); if(!fs.rename(path, ((HadoopFile)destFile).path)) throw new IOException(); // Update destination file attributes by fetching them from the server ((HadoopFileAttributes)destFile.getUnderlyingFileObject()).fetchAttributes(); // Update this file's attributes locally fileAttributes.setExists(false); fileAttributes.setDirectory(false); fileAttributes.setSize(0); } @Override public void setLastModifiedDate(long lastModified) throws IOException { // Note: setTimes seems to fail on HDFS directories. fs.setTimes(path, lastModified, lastModified); // Update local attributes fileAttributes.setDate(lastModified); } @Override public void changeReplication(short replication) throws IOException { // Note: setTimes seems to fail on HDFS directories. fs.setReplication(path, replication); // Update local attributes fileAttributes.setReplication(replication); } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { changePermissions(ByteUtils.setBit(getPermissions().getIntValue(), (permission << (access*3)), enabled)); } @Override public InputStream getInputStream() throws IOException { return fs.open(path); } @Override public OutputStream getOutputStream() throws IOException { return getOutputStream(false); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new HadoopRandomAccessInputStream(fs.open(path), getSize()); } @Override public AbstractFile[] ls() throws IOException { return ls(null); } // Unsupported file operations @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws IOException { // Currently not supported by any of the filesystems (S3, HDFS) throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); } @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { // TODO: implement for S3 throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override public long getFreeSpace() throws IOException { return fs.getStatus().getRemaining(); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override public long getTotalSpace() throws IOException { return fs.getStatus().getCapacity(); } @Override public AbstractFile[] ls(FilenameFilter filter) throws IOException { // We need to ensure that the file is a directory: if it isn't listStatus returns an empty array but doesn't // throw an exception if(!exists() || !isDirectory()) throw new IOException(); FileStatus[] statuses = filter==null ?fs.listStatus(path) :fs.listStatus(path, new HadoopFilenameFilter(filter)); int nbChildren = statuses==null?0:statuses.length; AbstractFile[] children = new AbstractFile[nbChildren]; String parentPath = fileURL.getPath(); if(!parentPath.endsWith("/")) parentPath += "/"; FileURL childURL; FileStatus childStatus; for(int i=0; iSyncedFileAttributes, this class caches attributes for a certain amount of time * after which fresh values are retrieved from the server. */ class HadoopFileAttributes extends SyncedFileAttributes { private final static int TTL = 60000; // this constructor is called by the public constructor private HadoopFileAttributes() throws AuthException { super(TTL, false); // no initial update fetchAttributes(); // throws AuthException if no or bad credentials updateExpirationDate(); // declare the attributes as 'fresh' } // this constructor is called by #ls() private HadoopFileAttributes(FileStatus fileStatus) { super(TTL, false); // no initial update setAttributes(fileStatus); setExists(true); updateExpirationDate(); // declare the attributes as 'fresh' } private void fetchAttributes() throws AuthException { // Do not update attributes while the file is being written, as they are not reflected immediately on the // name node. if(isWriting) return; try { setAttributes(fs.getFileStatus(path)); setExists(true); } catch (IOException e) { // File doesn't exist on the server setExists(false); setDefaultFileAttributes(getURL(), this); // Rethrow AuthException if (e instanceof AuthException) { throw (AuthException) e; } } } /** * Sets the file attributes using the values contained in the specified J2SSH FileAttributes instance. * * @param fileStatus FileStatus instance that contains the file attributes values to use */ private void setAttributes(FileStatus fileStatus) { setDirectory(fileStatus.isDirectory()); setDate(fileStatus.getModificationTime()); setSize(fileStatus.getLen()); setPermissions(new SimpleFilePermissions( fileStatus.getPermission().toShort() & PermissionBits.FULL_PERMISSION_INT )); setOwner(fileStatus.getOwner()); setGroup(fileStatus.getGroup()); setReplication(fileStatus.getReplication()); setBlockSize(fileStatus.getBlockSize()); } /** * Increments the size attribute's value by the given number of bytes. * * @param increment number of bytes to add to the current size attribute's value */ private void addToSize(long increment) { setSize(getSize()+increment); } ///////////////////////////////////////// // SyncedFileAttributes implementation // ///////////////////////////////////////// @Override public void updateAttributes() { try { fetchAttributes(); } catch(Exception e) { // AuthException LOGGER.info("Failed to update attributes", e); } } } /** * Turns a Hadoop {@link FSDataInputStream} into a {@link RandomAccessInputStream}. */ private static class HadoopRandomAccessInputStream extends RandomAccessInputStream { private FSDataInputStream in; private long length; private HadoopRandomAccessInputStream(FSDataInputStream in, long length) { this.in = in; this.length = length; } public long getOffset() throws IOException { return in.getPos(); } public long getLength() { return length; } public void seek(long offset) throws IOException { in.seek(offset); } @Override public int read() throws IOException { return in.read(); } @Override public int read(byte[] b, int off, int len) throws IOException { return in.read(b, off, len); } @Override public void close() throws IOException { } } /** * Turns a {@link FilenameFilter} into a Hadoop {@link PathFilter}. */ private static class HadoopFilenameFilter implements PathFilter { private FilenameFilter filenameFilter; private HadoopFilenameFilter(FilenameFilter filenameFilter) { this.filenameFilter = filenameFilter; } /////////////////////////////// // PathFilter implementation // /////////////////////////////// public boolean accept(Path path) { return filenameFilter.accept(path.getName()); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/hadoop/S3File.java ================================================ package com.mucommander.commons.file.impl.hadoop; import com.mucommander.commons.file.AuthException; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import java.io.IOException; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; /** * {@link HadoopFile} implementation for the Amazon S3 protocol. * *

Even though it is working for the most part, it is flawed in several ways and should not be used. * See the {@link com.mucommander.commons.file.impl.s3} package for a better implementation of the Amazon S3 protocol. * * @deprecated * @author Maxence Bernard */ public class S3File extends HadoopFile { protected S3File(FileURL url) throws IOException { super(url); } protected S3File(FileURL url, FileSystem fs, FileStatus fileStatus) throws IOException { super(url, fs, fileStatus); } @Override protected FileSystem getHadoopFileSystem(FileURL url) throws IOException { if(!url.containsCredentials()) throw new AuthException(url); // Note: getRealm returns a fresh instance every time FileURL realm = url.getRealm(); // Import credentials Credentials creds = url.getCredentials(); if(creds!=null) { // URL-encode secret as it may contain non URL-safe characters ('+' and '/') realm.setCredentials(new Credentials(creds.getLogin(), URLEncoder.encode(creds.getPassword(), StandardCharsets.UTF_8))); } // Change the scheme to the actual Hadoop fileystem (s3 -> s3n) realm.setScheme("s3n"); return FileSystem.get(URI.create(realm.toString(true, false)), DEFAULT_CONFIGURATION); } @Override protected void setDefaultFileAttributes(FileURL url, HadoopFileAttributes atts) { // Implemented as a no-op (S3 has no user info) } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/hadoop/S3ProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.hadoop; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import java.io.IOException; /** * A file protocol provider for the Amazon S3 protocol, provided by the Hadoop virtual filesystem. * *

Even though it is working for the most part, it is flawed in several ways and should not be used. * See the {@link com.mucommander.commons.file.impl.s3} package for a better implementation of the Amazon S3 protocol. * * @deprecated * @author Maxence Bernard */ public class S3ProtocolProvider implements ProtocolProvider { public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return instantiationParams.length==0 ?new S3File(url) :new S3File(url, (FileSystem)instantiationParams[0], (FileStatus)instantiationParams[1]); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/http/HTTPFile.java ================================================ package com.mucommander.commons.file.impl.http; import com.mucommander.commons.file.*; import com.mucommander.commons.io.BlockRandomInputStream; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.io.base64.Base64Encoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * HTTPFile provides access to files located on an HTTP/HTTPS server. * *

The associated {@link FileURL} schemes are {@link FileProtocols#HTTP} and {@link FileProtocols#HTTPS}. * The host part of the URL designates the HTTP server. Credentials can be specified in the login and password parts * and will be used for HTTP Basic Authentication. * *

Here are a few examples of valid HTTP URLs: * * http://www.mucommander.com/index.html
* http://www.mucommander.com/index.php?
* http://john:p4sswd@www.mucommander.com/restricted_area/
*
* *

* A notable feature of HTTPFile is that it handles HTML/XHTML files as archives: when any of the {@link #ls()} methods * is called, the HTML file is parsed and any link found in the code is considered as a file: *

    *
  • If the link looks like a link to an HTML file, the child HTTPFile will be 'browsable' ({@link #isBrowsable()} * will return true). *
  • If not, the file will just be a regular file. *
* *

In order to avoid the cost of having to perform a HEAD request for each file, some guessing based on the URL and * its filename is performed to determine if the file is an HTML/XHTML file or not. * In practice, this works quite well for most sites but the algorithm will be confused by some non-conventional * file naming, for instance if an HTML file ends with the '.gif' extension. *

* A HEAD request is then issued only for non-HTML files, to determine their size and last modified date. * HTML files will thus have a size returned by {@link #getSize()} of -1 (undetermined), and a date * returned by {@link #getLastModifiedDate()} corresponding to 'now' (current time). * *

Access to HTTP files is provided by the java.net API. The {@link #getUnderlyingFileObject()} method * allows to retrieve a java.net.URL instance corresponding to this HTTPFile. * * @author Maxence Bernard */ public class HTTPFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(HTTPFile.class); /** java.net.URL corresponding to this */ private final URL url; /** Contains the attributes of the remote HTTP resource. Contains default values until the file has been resolved */ private final SimpleFileAttributes attributes; /** True if the file should be resolved on the remote HTTP server to fetch attribute values, false if these are * guessed. */ private final boolean resolve; /** True if file has been resolved on the remote HTTP server, either successfully or unsuccessfully */ private boolean fileResolved; private boolean parentValSet; protected AbstractFile parent; /** Permissions for HTTP files: r-- (400 octal). Only the 'user' permissions bits are supported. */ private final static FilePermissions PERMISSIONS = new SimpleFilePermissions(256, 448); /** User agent used for all HTTP connections made by HTTPFile */ // TODO: add file API version, like trolCommander-file-API/1.0 private static final String USER_AGENT = "trolCommander-file-API (Java "+System.getProperty("java.vm.version") + "; " + System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch") + ")"; /** Matches HTML and XHTML attribute key/value pairs, where the value is surrounded by Single Quotes */ private final static Pattern linkAttributePatternSQ = Pattern.compile("(src|href|SRC|HREF)='.*?'"); /** Matches HTML and XHTML attribute key/value pairs, where the value is surrounded by Double Quotes */ private final static Pattern linkAttributePatternDQ = Pattern.compile("(src|href|SRC|HREF)=\".*?\""); HTTPFile(FileURL fileURL) throws IOException { // TODO: optimize this this(fileURL, new URL(fileURL.toString(false))); } HTTPFile(FileURL fileURL, URL url) throws IOException { super(fileURL); String scheme = fileURL.getScheme().toLowerCase(); if ((!scheme.equals(FileProtocols.HTTP) && !scheme.equals(FileProtocols.HTTPS)) || fileURL.getHost() == null) { throw new IOException(); } this.url = url; attributes = getDefaultAttributes(); String mimeType; String filename = fileURL.getFilename(); // Simple/fuzzy heuristic to avoid file resolution (HEAD) in cases where we have good reasons to believe that // the URL denotes a HTML/XTHML document: // - URL's path has no filename (e.g. http://www.mucommander.com/) or path ends with '/' (e.g. http://www.mucommander.com/download/) // - URL has a query part (works most of the time, must not always) // - URL has an extension that registered with an HTML/XHTML mime type if ((filename == null || fileURL.getPath().endsWith("/") || fileURL.getQuery()!=null || ((mimeType=MimeTypes.getMimeType(this))!=null && isParsableMimeType(mimeType)))) { attributes.setDirectory(true); resolve = false; } else { resolve = true; } } private static SimpleFileAttributes getDefaultAttributes() { SimpleFileAttributes attributes = new SimpleFileAttributes(); attributes.setDate(System.currentTimeMillis()); attributes.setSize(-1); // Unknown attributes.setPermissions(PERMISSIONS); // exist = false // isDirectory = false // path = null (unused) return attributes; } /** * Returns true if the given mime type corresponds to HTML or XHTML and can be parsed. * * @param mimeType a MIME type / content type * @return true if the given mime type corresponds to HTML or XHTML and can be parsed */ private boolean isParsableMimeType(String mimeType) { return mimeType != null && (mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml") || mimeType.startsWith("application/xml")); } /** * Performs a HEAD request on the HTTP server to retrieve the file's attributes. * * @throws IOException if the HEAD request failed, either because the resource doesn't exist (404) or for any other * reason */ private void resolveFile() throws IOException { try { LOGGER.info("Resolving {}", url); // Get URLConnection instance HttpURLConnection conn = getHttpURLConnection(url); // Use HEAD instead of GET as we don't need the body conn.setRequestMethod("HEAD"); // Establish connection conn.connect(); // Check HTTP response code and throw appropriate IOException if request failed checkHTTPResponse(conn); // Resolve date: use last-modified header, if not set use date header, and if still not set use System.currentTimeMillis long date = conn.getLastModified(); if (date == 0) { date = conn.getDate(); if (date == 0) { date = System.currentTimeMillis(); } } attributes.setDate(date); // Resolve size with content-length header (-1 if not available) attributes.setSize(conn.getContentLength()); // Test if content is HTML String contentType = conn.getContentType(); if (isParsableMimeType(contentType)) { attributes.setDirectory(true); } // File was successfully resolved on the remote HTTP server and thus exists attributes.setExists(true); } catch(IOException e) { LOGGER.info("Failed to resolve file {}", url, e); } finally { // Mark the file as resolved, even if the request failed fileResolved = true; } } /** * Opens and returns a HttpURLConnection to the resource denoted by the specified URL. * If the {@link FileURL} contained by this HTTPFile contains {@link Credentials}, these will be used as credentials * for HTTP Basic Authentication. * * @param url the URL to open * @return a HttpURLConnection to the resource denoted by the specified URL * @throws IOException if the HttpURLConnection could not be opened */ private HttpURLConnection getHttpURLConnection(URL url) throws IOException { // Get URLConnection instance HttpURLConnection conn = (HttpURLConnection)url.openConnection(); // If credentials are contained in this HTTPFile's FileURL, use them for Basic HTTP Authentication Credentials credentials = fileURL.getCredentials(); if (credentials != null) conn.setRequestProperty( "Authorization", "Basic "+ Base64Encoder.encode(credentials.getLogin()+":"+credentials.getPassword()) ); // Set user-agent header. conn.setRequestProperty("User-Agent", USER_AGENT); return conn; } /** * Checks the response code of the given HttpURLConnection and : *

    *
  • throws an {@link AuthException} if the response code is 401 (Unauthorized) *
  • throws an IOException if the response code is not in the 2xx - 3xx range (not a positive response) *
  • does nothing otherwise * * @param conn the HttpURLConnection connection to examine * @throws AuthException if the response code is 401 (Unauthorized) * @throws IOException if the response code is not in the 2xx - 3xx range (not a positive response) */ private void checkHTTPResponse(HttpURLConnection conn) throws IOException { int responseCode = conn.getResponseCode(); LOGGER.info("response code = {}", responseCode); // If we got a 401 (Unauthorized) response, throw an AuthException to ask for credentials if (responseCode == 401) { throw new AuthException(fileURL, conn.getResponseMessage()); } if (responseCode < 200 || responseCode >= 400) { throw new IOException(conn.getResponseMessage()); } } private void checkResolveFile() { if (resolve && !fileResolved) { try { resolveFile(); } catch(IOException e) { LOGGER.info("Failed to resolve {}", url, e); // file will be considered as resolved } } } ///////////////////////////////////////// // AbstractFile methods implementation // ///////////////////////////////////////// @Override public long getLastModifiedDate() { checkResolveFile(); return attributes.getLastModifiedDate(); } /** * Implementation notes: always throws {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always. */ @Override @UnsupportedFileOperation public void setLastModifiedDate(long date) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } @Override public long getSize() { checkResolveFile(); return attributes.getSize(); // Size == -1 if not known } @Override public AbstractFile getParent() { if (!parentValSet) { FileURL parentURL = fileURL.getParent(); this.parent = parentURL == null ? null : FileFactory.getFile(parentURL); this.parentValSet = true; } return this.parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValSet = true; } @Override public boolean exists() { if (!fileResolved) { // Note: file will only be resolved once, even if the request failed try { resolveFile(); } catch(IOException ignore) {} } return attributes.exists(); } @Override public FilePermissions getPermissions() { return attributes.getPermissions(); } @Override public PermissionBits getChangeablePermissions() { return PermissionBits.EMPTY_PERMISSION_BITS; } @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } @Override public String getOwner() { return null; } @Override public boolean canGetOwner() { return false; } @Override public String getGroup() { return null; } @Override public boolean canGetGroup() { return false; } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } @Override public boolean isDirectory() { checkResolveFile(); return attributes.isDirectory(); } @Override public boolean isSymlink() { return false; } @Override public boolean isSystem() { return false; } @Override public InputStream getInputStream() throws IOException { HttpURLConnection conn = getHttpURLConnection(this.url); // Establish connection conn.connect(); // Check HTTP response code and throw appropriate IOException if request failed checkHTTPResponse(conn); return conn.getInputStream(); } /** * Always throws an {@link UnsupportedFileOperationException}: HTTP files are read-only. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE); } /** * Always throws an {@link UnsupportedFileOperationException}: HTTP files are read-only. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new HTTPRandomAccessInputStream(); } /** * Always throws an {@link UnsupportedFileOperationException}: HTTP files are read-only. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } /** * Always throws an {@link UnsupportedFileOperationException}: HTTP files are read-only. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void delete() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.DELETE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Always throws an {@link UnsupportedFileOperationException}: HTTP files are read-only. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void renameTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RENAME); } /** * Always throws an {@link UnsupportedFileOperationException}: HTTP files are read-only. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void mkdir() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CREATE_DIRECTORY); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } /** * Returns a java.net.URL instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return url; } @Override public AbstractFile[] ls() throws IOException { // Implementation note: javax.swing.text.html.HTMLEditorKit isn't quite powerful enough to be used try { HttpURLConnection conn = resolveAndConnect(this.url); URL contextURL = conn.getURL(); try (BufferedReader br = createReader(conn)) { return findChildren(br, contextURL); } } catch (Exception e) { LOGGER.info("Exception caught while parsing HTML, throwing IOException", e); if (e instanceof IOException) { throw (IOException) e; } throw new IOException(); } } private HttpURLConnection resolveAndConnect(URL url) throws IOException { final int maxRedirects = 10; for (int i = 0; i < maxRedirects; i++) { // Get a connection instance HttpURLConnection conn = getHttpURLConnection(url); // Disable automatic redirections to track URL change conn.setInstanceFollowRedirects(false); // Establish connection conn.connect(); // Check HTTP response code and throw appropriate IOException if request failed checkHTTPResponse(conn); int responseCode = conn.getResponseCode(); // Test if response code is in the 3xx range (redirection) and if 'Location' field is set if (responseCode >= 300 && responseCode < 400) { String locationHeader = conn.getHeaderField("Location"); if (locationHeader != null) { // Redirect to Location field and remember context url LOGGER.info("Location header = {}", locationHeader); url = new URL(url, locationHeader); // One more time continue; } } return conn; } throw new IOException("too many redirects"); } private AbstractFile[] findChildren(BufferedReader br, URL contextURL) throws IOException { List children = new ArrayList<>(); // List that contains children URL, a TreeSet for fast (log(n)) search operations Set childrenURL = new TreeSet<>(); Credentials credentials = fileURL.getCredentials(); // String parentPath = fileURL.getPath(); // if (!parentPath.endsWith("/")) { // parentPath += "/"; // } String parentHost = fileURL.getHost(); //FileURL tempChildURL = (FileURL)fileURL.clone(); Pattern pattern; String line; final String parentPath = contextURL.toString(); while ((line = br.readLine()) != null) { for (pattern = linkAttributePatternSQ; ; pattern = linkAttributePatternDQ) { Matcher matcher = pattern.matcher(line); while (matcher.find()) { String match = matcher.group(); String link = match.substring(match.indexOf(pattern==linkAttributePatternSQ ? '\'' : '\"') + 1, match.length()-1); // These are not proper URLs, skip them // Don't add the same link more than once if (!linkCanBeDownloaded(link) || childrenURL.contains(link)) { continue; } try { LOGGER.trace("creating child {} context={}", link, contextURL); URL childURL = new URL(contextURL, link); // create the child FileURL instance FileURL childFileURL = FileURL.getFileURL(childURL.toExternalForm()); // Keep the parent's credentials (HTTP basic authentication), only if the host is the same. // It would otherwise be unsafe. if (parentHost.equals(childFileURL.getHost())) { childFileURL.setCredentials(credentials); } // TODO: resolve file here instead of in the constructor, and multiplex requests just like a browser // skip parent if (!childURL.toString().contains(parentPath)) { continue; } children.add(FileFactory.getFile(childFileURL, null, childURL, childURL.toString())); childrenURL.add(link); } catch (IOException e) { LOGGER.info("Cannot create child: {}", e); } } if (pattern == linkAttributePatternDQ) { break; } } } AbstractFile[] childrenArray = new AbstractFile[children.size()]; children.toArray(childrenArray); return childrenArray; } private boolean linkCanBeDownloaded(String link) { link = link.toLowerCase(); return !(link.startsWith("mailto") || link.startsWith("#") || link.startsWith("javascript:")); } private String getContentEncoding(HttpURLConnection conn) throws IOException { // Retrieve content type and throw an IOException if doesn't correspond to a parsable type (HTML/XHTML) String contentType = conn.getContentType(); if (!isParsableMimeType(contentType)) { throw new IOException("Document cannot be parsed (not HTML or XHTML)"); // Todo: localize this message } int pos; String enc = null; // Extract content type information (if any) if ((pos = contentType.indexOf("charset")) >= 0 || (pos = contentType.indexOf("Charset")) >= 0) { StringTokenizer st = new StringTokenizer(contentType.substring(pos)); enc = st.nextToken(); } return enc; } private BufferedReader createReader(HttpURLConnection conn) throws IOException { // Use the encoding reported in HTTP header if there was one, otherwise just use the default encoding String enc = getContentEncoding(conn); InputStream in = conn.getInputStream(); InputStreamReader ir; if (enc == null) { ir = new InputStreamReader(in); } else { try { ir = new InputStreamReader(in, enc); } catch (UnsupportedEncodingException e) { ir = new InputStreamReader(in); } } return new BufferedReader(ir); } @Override public boolean isHidden() { return false; } @Override public String getName() { try { return java.net.URLDecoder.decode(super.getName(), StandardCharsets.UTF_8); } catch(Exception e) { return super.getName(); } } /** * Overrides AbstractFile's getInputStream(long) method to provide a more efficient implementation: * use the HTTP 1.1 header to start the transfer at the given offset. */ @Override public InputStream getInputStream(long offset) throws IOException { HttpURLConnection conn = getHttpURLConnection(this.url); // Set header that allows to resume transfer conn.setRequestProperty("Range", "bytes="+offset+"-"); // Establish connection conn.connect(); // Check HTTP response code and throw appropriate IOException if request failed checkHTTPResponse(conn); return conn.getInputStream(); } /////////////////// // Inner classes // /////////////////// /** * HTTPRandomAccessInputStream extends BlockRandomInputStream to provide random read access to an HTTPFile. * It uses the 'Range' request header to read the HTTP resource partially, chunk by chunk and reposition the offset * when {@link #seek(long)} is called. */ private class HTTPRandomAccessInputStream extends BlockRandomInputStream { /** Amount of data returned */ private final static int CHUNK_SIZE = 1024; /** Length of the HTTP resource */ private final long length; private HTTPRandomAccessInputStream() throws IOException { super(CHUNK_SIZE); // HEAD the HTTP resource to get its length if (!fileResolved) { resolveFile(); } length = getSize(); if (length == -1) { // Knowing the content length is required throw new IOException(); } } /////////////////////////////////////////// // BlockRandomInputStream implementation // /////////////////////////////////////////// @Override protected int readBlock(long fileOffset, byte[] block, int blockLen) throws IOException { HttpURLConnection conn = getHttpURLConnection(url); // Note: 'Range' may not be supported by the HTTP server, in that case an IOException will be thrown conn.setRequestProperty("Range", "bytes="+fileOffset +"-"+ Math.min(fileOffset+blockLen, length-1)); conn.connect(); checkHTTPResponse(conn); // Read up to blockLen bytes try (InputStream in = conn.getInputStream()) { int totalRead = 0; int read; while (totalRead < blockLen) { read = in.read(block, totalRead, blockLen - totalRead); if (read == -1) { break; } totalRead += read; } return totalRead; } } public long getLength() { return length; } @Override public void close() throws IOException { // No-op, the underlying stream is already closed } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/http/HTTPProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.http; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.*; import java.io.IOException; import java.net.URL; import java.security.SecureRandom; import java.security.cert.X509Certificate; /** * This class is the provider for the HTTP/HTTPS filesystem implemented by {@link com.mucommander.commons.file.impl.http.HTTPFile}. * * @author Nicolas Rinaudo * @see com.mucommander.commons.file.impl.http.HTTPFile */ public class HTTPProtocolProvider implements ProtocolProvider { private static final Logger LOGGER = LoggerFactory.getLogger(HTTPProtocolProvider.class); static { try { disableCertificateVerifications(); } catch(Exception e) { LOGGER.info("Failed to install a custom TrustManager", e); } } /** * Installs a custom javax.net.ssl.X509TrustManager and javax.net.ssl.HostnameVerifier * to bypass the default SSL certificate verifications and blindly trust all SSL certificates, even if they are * self-signed, expired, or do not match the requested hostname. * As a result in such cases, HttpsURLConnection#openConnection() will succeed instead of throwing a * javax.net.ssl.SSLException. * *

    This method needs to be called only once in the JVM lifetime and will impact all HTTPS connections made, * i.e. not only the ones made by this class. * *

    This clearly is unsecure for the user, but arguably better from a feature standpoint than systematically * failing untrusted connections. * * @throws Exception if an error occurred while installing the custom X509TrustManager. */ private static void disableCertificateVerifications() throws Exception { // Todo: find a way to warn the user when the server cannot be trusted // create a custom X509 trust manager that does not validate certificate chains TrustManager permissiveTrustManager = new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkServerTrusted(X509Certificate[] certs, String authType) { } public void checkClientTrusted(X509Certificate[] certs, String authType) { } }; // Install the permissive trust manager SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, new TrustManager[]{permissiveTrustManager}, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); // create and install a custom hostname verifier that allows hostname mismatches HostnameVerifier permissiveHostnameVerifier = (urlHostName, session) -> true; HttpsURLConnection.setDefaultHostnameVerifier(permissiveHostnameVerifier); } public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return instantiationParams.length == 0 ? new HTTPFile(url) :new HTTPFile(url, (URL)instantiationParams[0]); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/http/package.html ================================================ Provides an implementation of the HTTP protocol. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoArchiveEntry.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.ArchiveEntry; /** * This class represents an archive entry within an ISO archive. * * @author Maxence Bernard */ class IsoArchiveEntry extends ArchiveEntry { private long index; private int sectSize; private long shiftOffset; private boolean audio; IsoArchiveEntry(String path, boolean directory, long date, long size, long index, int sectSize, long shiftOffset, boolean audio) { super(path, directory, date, size, true); this.index = index; this.sectSize = sectSize; this.shiftOffset = shiftOffset; this.audio = audio; } long getIndex() { return index; } public int getSectSize() { return sectSize; } public long getShiftOffset() { return shiftOffset; } public boolean getAudio() { return audio; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoArchiveFile.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.*; import com.mucommander.commons.io.FilterRandomAccessInputStream; import com.mucommander.commons.io.RandomAccessInputStream; import java.io.IOException; import java.io.InputStream; /** * IsoArchiveFile provides read-only access to archives in the ISO and NRG formats. * * @author Maxence Bernard * @see com.mucommander.commons.file.impl.iso.IsoFormatProvider */ public class IsoArchiveFile extends AbstractROArchiveFile { public IsoArchiveFile(AbstractFile file) { super(file); } @Override public ArchiveEntryIterator getEntryIterator() throws IOException { RandomAccessInputStream rais = getRandomAccessInputStream(); return new IsoEntryIterator(IsoParser.getEntries(this, rais).iterator(), rais); } @Override public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException { // Cast the entry before creating the stream, in case it fails IsoArchiveEntry isoEntry = (IsoArchiveEntry) entry; RandomAccessInputStream rais; // If a IsoEntryIterator is specified, reuse the iterator's stream if (entryIterator != null && entryIterator instanceof IsoEntryIterator) { // Override close() as a no-op so that the stream is re-used from one entry to another -- the stream will // be closed when the iterator is closed. rais = new FilterRandomAccessInputStream(((IsoEntryIterator) entryIterator).getRandomAccessInputStream()) { @Override public void close() { // No-op } }; } else { rais = getRandomAccessInputStream(); } return new IsoEntryInputStream(rais, isoEntry); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoEntryInputStream.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.StreamUtils; import java.io.IOException; import java.io.InputStream; /** * IsoEntryInputStream allows to reads an ISO entry. * * @author Xavier Martin */ class IsoEntryInputStream extends InputStream { private RandomAccessInputStream rais; private int pos; private long size; private int sectSize; private boolean audio; IsoEntryInputStream(RandomAccessInputStream rais, IsoArchiveEntry entry) throws IOException { this.rais = rais; this.size = entry.getSize(); this.pos = 0; this.sectSize = entry.getSectSize(); this.audio = entry.getAudio(); rais.seek(IsoUtil.offsetInSector(entry.getIndex(), sectSize, audio) + entry.getShiftOffset()); } //////////////////////////////// // InputStream implementation // //////////////////////////////// @Override public int read() throws IOException { return rais.read(); } //////////////////////// // Overridden methods // //////////////////////// @Override public int read(byte b[]) throws IOException { return read(b, 0, b.length); } @Override public int read(byte b[], int off, int len) throws IOException { int available = available(); int toRead = len; if (toRead > available) { toRead = available; } if (available == 0) { return -1; } int ret; int cur = off; int before = pos; int skip = 0; // on the 1st run : generate a valid wav header if (audio && pos == 0) { IsoUtil.toArray(0x46464952, b, 0); // "RIFF" IsoUtil.toArray((int) size - 8, b, 4); // size of file - 8 IsoUtil.toArray(0x45564157, b, 8); // "WAVE" IsoUtil.toArray(0x20746D66, b, 12); // "fmt " b[16] = 0x10; // Chunk Data Size IsoUtil.toArray(0x00020001, b, 20); // WAVE type format : PCM header 0100, stereo 0200 IsoUtil.toArray(0x0000AC44, b, 24); // sample rate : 44100hz IsoUtil.toArray(0x0002B110, b, 28); // bytes/sec : 176400 IsoUtil.toArray(0x00100004, b, 32); // Block alignment 0400 + Bits/sample 1000 IsoUtil.toArray(0x61746164, b, 36); // "data" IsoUtil.toArray((int) size - IsoUtil.WAV_header, b, 40); // size of 'real' data cur = IsoUtil.WAV_header; toRead -= IsoUtil.WAV_header; pos += IsoUtil.WAV_header; } switch (sectSize) { case IsoUtil.MODE2_2336: skip = IsoUtil.MODE2_2336_skip; break; case IsoUtil.MODE2_2352: skip = IsoUtil.MODE2_2352_skip; break; case IsoUtil.MODE1_2048: // shortcut ret = rais.read(b, off, toRead); if (ret != -1) pos += ret; return ret; } // atm it work because it's called for 8192 (2048 * 4) int full = toRead >> 11; int half = toRead % IsoUtil.MODE1_2048; if (full >= 1) { for (int i = 0; i < full; i++) { ret = rais.read(b, cur, IsoUtil.MODE1_2048); if (ret != -1) pos += ret; if (!audio) StreamUtils.skipFully(rais, IsoUtil.MODE2_EC_skip + skip); cur += IsoUtil.MODE1_2048; } ret = rais.read(b, cur, half); if (ret != -1) pos += ret; } else { // in fact it doesn't work for internal viewer, because it's called by chunk of 1024 ret = rais.read(b, cur, half); if (ret != -1) pos += ret; if (((pos % 2048) == 0) && !audio) StreamUtils.skipFully(rais, IsoUtil.MODE2_EC_skip + skip); } return pos - before; } @Override public int available() { int available = (int) (size - pos); return (available < 0) ? 0 : available; } @Override public void close() throws IOException { rais.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoEntryIterator.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.ArchiveEntry; import com.mucommander.commons.file.WrapperArchiveEntryIterator; import com.mucommander.commons.io.RandomAccessInputStream; import java.io.IOException; import java.util.Iterator; /** * This class iterates through the entries of an ISO file, and keeps the ISO file's * {@link #getRandomAccessInputStream RandomAccessInputStream} so that it doesn't have to be opened each time a * new entry is read. {@link #close} closes the stream. * * @author Maxence Bernard */ class IsoEntryIterator extends WrapperArchiveEntryIterator { /** * The ISO file's InputStream */ private RandomAccessInputStream rais; public IsoEntryIterator(Iterator iterator, RandomAccessInputStream rais) { super(iterator); this.rais = rais; } /** * Returns the ISO file's {@link RandomAccessInputStream} that was passed to the constructor. * * @return the ISO file's {@link RandomAccessInputStream} that was passed to the constructor. */ RandomAccessInputStream getRandomAccessInputStream() { return rais; } /** * Closes the ISO file's {@link RandomAccessInputStream} that was passed to the constructor. * * @throws IOException if an I/O error occurs while closing the stream */ @Override public void close() throws IOException { rais.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoFormatProvider.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.SevenZipArchiveFormatDetector; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; import java.io.IOException; /** * This class is the provider for the 'Iso' and 'Nrg' archive formats implemented by {@link IsoArchiveFile}. * * @author Nicolas Rinaudo, Maxence Bernard * @see com.mucommander.commons.file.impl.iso.IsoArchiveFile */ public class IsoFormatProvider implements ArchiveFormatProvider { /** * Array of format extensions */ private final static String[] EXTENSIONS = {".iso", ".nrg"}; private static final int[] SIGNATURE_FAT_MBR = {0xEB, -1, -1, 'M', 'S', 'D', 'O', 'S'}; /** * Static instance of the filename filter that matches archive filenames */ private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private static final SevenZipArchiveFormatDetector detector = new SevenZipArchiveFormatDetector(SIGNATURE_FAT_MBR.length) { @Override protected ArchiveFormat detect(byte[] bytes) { if (checkSignature(bytes, SIGNATURE_FAT_MBR)) { return ArchiveFormat.FAT; } return ArchiveFormat.ISO; } }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { if (file.getExtension().equalsIgnoreCase("nrg")) { return new IsoArchiveFile(file); } return new SevenZipJBindingROArchiveFile(file, detector); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoParser.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; /** * Parses entries contained in an ISO/NRG file. *

    *

     * Reference: http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-119.pdf
     * 

    * Todo: * * test with more images * * rewrite/sanitize InputStream for cooked * * add RockRidge, UDF and others extensions * * add DiscJuggler & other weirdo file formats *

    * * @author Xavier Martin */ class IsoParser { private static final Logger LOGGER = LoggerFactory.getLogger(IsoParser.class); public static List getEntries(byte[] buffer, RandomAccessInputStream rais, int sectSize, long sector_offset, long shiftOffset) throws Exception { List entries = new ArrayList<>(); Calendar calendar = Calendar.getInstance(); int start = 16; isoPvd pvd = null; todo todo_idr = null; int level = 0; for (int i = 1; i < 17; i++) { // fuzzy search, can have type=0 (bootable el torito), type=2 (svd) pvd = new isoPvd(buffer, rais, start + i, sectSize, shiftOffset); if (pvd.type[0] == 2 && pvd.id[0] == 'C' && pvd.id[1] == 'D' && pvd.id[2] == '0' && pvd.id[3] == '0' && pvd.id[4] == '1') { // gotta read docs a little more about those UCS-2 Escape Sequences switch (pvd.unused3[2]) { case 0x40: level = 1; break; case 0x43: level = 2; break; case 0x45: level = 3; } break; } } if (level == 0) // if no SVD with Joliet, fallback to plain-old ISO9660 pvd = new isoPvd(buffer, rais, start, sectSize, shiftOffset); isoDr idr = new isoDr(pvd.root_directory_record, 0); todo_idr = parse_dir(todo_idr, "", isonum_733(idr.extent), isonum_733(idr.size), rais, buffer, entries, sectSize, level, shiftOffset, sector_offset, calendar); while (todo_idr != null) { todo_idr = parse_dir(todo_idr, todo_idr.name, todo_idr.extent, todo_idr.length, rais, buffer, entries, sectSize, level, shiftOffset, sector_offset, calendar); todo_idr = todo_idr.next; } return entries; } /** * Parses the given ISO file and returns the list of entries it contains. The specified stream will *not* be closed * by this method. * * @param file the ISO file to parse * @param rais random access stream to read the ISO file. It will *not* be closed by this method. * @return the list of entries contained by the ISO file * @throws IOException if an I/O error occurs */ static List getEntries(AbstractFile file, RandomAccessInputStream rais) throws IOException { byte[] buffer = BufferPool.getByteArray(IsoUtil.MODE1_2048); try { if ("nrg".equals(file.getExtension())) { return NrgParser.getEntries(buffer, file, rais); } int sectSize = IsoUtil.guessSectorSize(file); // sector shift : 0 most of the time long sector_offset = 0; // bytes : depend if there's earlier track we discard long shiftOffset = 0; return getEntries(buffer, rais, sectSize, sector_offset, shiftOffset); /* if ("cdi".equals(file.getExtension())) { // WIP // http://cvs.berlios.de/cgi-bin/viewcvs.cgi/libdiscmage/libdiscmage/src/filter/cdi.c?revision=1.3&view=markup int len = rais.available(); long offset = -1; rais.seek(len - 7); rais.read(buffer, 0, 8); long version = IsoUtil.toDwordBE(buffer, 0); offset = IsoUtil.toDwordBE(buffer, 4); FileLogger.finest("cdi root " + Long.toHexString(offset) + " version " + Long.toHexString(version)); } */ } catch (Exception e) { LOGGER.info("Exception caught while parsing iso, throwing IOException", e); throw new IOException(); } finally { // Release the buffer BufferPool.releaseByteArray(buffer); } } private static void newString(byte[] b, int len, int level, StringBuffer name) throws Exception { name.append((level == 0) ? new String(b, 0, len) : new String(b, 0, len, "UnicodeBigUnmarked")); } public static todo parse_dir(todo todo_idr, String rootname, int extent, int len, RandomAccessInputStream rais, byte[] buffer, List entries, int sectSize, int level, long shiftOffset, long sector_offset, Calendar calendar) throws Exception { todo td; int i; isoDr idr; while (len > 0) { rais.seek(IsoUtil.offsetInSector(extent - sector_offset, sectSize, false) + shiftOffset); StreamUtils.readFully(rais, buffer); len -= buffer.length; extent++; i = 0; while (true) { idr = new isoDr(buffer, i); if (idr.length[0] == 0) break; stat fstat_buf = new stat(); StringBuffer name_buf = new StringBuffer(); fstat_buf.st_size = isonum_733(idr.size); if ((idr.flags[0] & 2) > 0) fstat_buf.st_mode |= S_IFDIR; else fstat_buf.st_mode |= S_IFREG; if (idr.name_len[0] == 1 && idr.name[0] == 0) name_buf.append("."); else if (idr.name_len[0] == 1 && idr.name[0] == 1) name_buf.append(".."); else { newString(idr.name, idr.name_len[0] & 0xff, level, name_buf); //if (level == 0) { // strip ;VERSION int p = name_buf.lastIndexOf(";"); if (p != -1) name_buf.setLength(p); p = name_buf.lastIndexOf("."); // strip empty extension if (p != -1) { int s = name_buf.length() - 1; if (p == s) name_buf.setLength(s); } //} } if ((idr.flags[0] & 2) != 0 && (idr.name_len[0] != 1 || (idr.name[0] != 0 && idr.name[0] != 1))) { td = todo_idr; if (td != null) { while (td.next != null) td = td.next; td.next = new todo(); td = td.next; } else { todo_idr = td = new todo(); } td.next = null; td.extent = isonum_733(idr.extent); td.length = isonum_733(idr.size); td.name = rootname + name_buf + "/"; } boolean dir = false; String n = name_buf.toString(); if (!(".".equals(n) || "..".equals(n))) { StringBuilder name = new StringBuilder(rootname); name.append(n); if (S_ISDIR(fstat_buf.st_mode)) { dir = true; if (!n.endsWith("/")) { name.append('/'); } } calendar.set((idr.date[0] & 0xff) + 1900, idr.date[1] - 1, idr.date[2], idr.date[3], idr.date[4], idr.date[5]); // date_buf[6] // offset from Greenwich Mean Time, in 15-minute intervals, as a twos complement signed number, // positive for time zones east of Greenwich, and negative for time zones calendar.setTimeZone(new java.util.SimpleTimeZone(15 * 60 * 1000 * idr.date[6], "")); entries.add( new IsoArchiveEntry( name.toString(), dir, calendar.getTimeInMillis(), fstat_buf.st_size, isonum_733(idr.extent) - sector_offset, sectSize, shiftOffset, false) ); } i += (buffer[i] & 0xff); if (i > IsoUtil.MODE1_2048 - idr.s_length) break; } } return todo_idr; } // ====================================== private static int ISODCL(int start, int end) { return (end - start + 1); } private static int isonum_731(byte[] p) { return IsoUtil.toDwordBE(p, 0); } public static int isonum_733(byte[] p) { return (isonum_731(p)); } private static boolean S_ISDIR(int m) { return ((m & S_IFDIR) == S_IFDIR); } // ====================================== private static int S_IFREG = 0100000; private static int S_IFDIR = 0040000; private static class stat { int st_size; int st_mode; } public static class todo { public todo next; public String name; public int extent; public int length; } public static class isoDr { public byte[] length = new byte[ISODCL(1, 1)]; public byte[] ext_attr_length = new byte[ISODCL(2, 2)]; public byte[] extent = new byte[ISODCL(3, 10)]; public byte[] size = new byte[ISODCL(11, 18)]; public byte[] date = new byte[ISODCL(19, 25)]; public byte[] flags = new byte[ISODCL(26, 26)]; public byte[] file_unit_size = new byte[ISODCL(27, 27)]; public byte[] interleave = new byte[ISODCL(28, 28)]; public byte[] volume_sequence_number = new byte[ISODCL(29, 32)]; public byte[] name_len = new byte[ISODCL(33, 33)]; public byte[] name = new byte[/*38*/128]; // quickly bumped to 128 for Joliet : doesn't lead to a crash yet :) public int s_length = 34; public byte[][] dataDr = {length, ext_attr_length, extent, size, date, flags, file_unit_size, interleave, volume_sequence_number, name_len, name}; public isoDr(byte[] src, int pos) { for (int i = 0, max = dataDr.length; i < max; i++) { int l = dataDr[i].length; if ((src.length - pos) < dataDr[i].length && i == 10) l = src.length - pos; System.arraycopy(src, pos, dataDr[i], 0, l); pos += dataDr[i].length; } } } public static class isoPvd { public byte[] type = new byte[ISODCL(1, 1)]; public byte[] id = new byte[ISODCL(2, 6)]; public byte[] version = new byte[ISODCL(7, 7)]; public byte[] unused1 = new byte[ISODCL(8, 8)]; public byte[] system_id = new byte[ISODCL(9, 40)]; public byte[] volume_id = new byte[ISODCL(41, 72)]; public byte[] unused2 = new byte[ISODCL(73, 80)]; public byte[] volume_space_size = new byte[ISODCL(81, 88)]; public byte[] unused3 = new byte[ISODCL(89, 120)]; public byte[] volume_set_size = new byte[ISODCL(121, 124)]; public byte[] volume_sequence_number = new byte[ISODCL(125, 128)]; public byte[] logical_block_size = new byte[ISODCL(129, 132)]; public byte[] path_table_size = new byte[ISODCL(133, 140)]; public byte[] type_l_path_table = new byte[ISODCL(141, 144)]; public byte[] opt_type_l_path_table = new byte[ISODCL(145, 148)]; public byte[] type_m_path_table = new byte[ISODCL(149, 152)]; public byte[] opt_type_m_path_table = new byte[ISODCL(153, 156)]; public byte[] root_directory_record = new byte[ISODCL(157, 190)]; public byte[] volume_set_id = new byte[ISODCL(191, 318)]; public byte[] publisher_id = new byte[ISODCL(319, 446)]; public byte[] preparer_id = new byte[ISODCL(447, 574)]; public byte[] application_id = new byte[ISODCL(575, 702)]; public byte[] copyright_file_id = new byte[ISODCL(703, 739)]; public byte[] abstract_file_id = new byte[ISODCL(740, 776)]; public byte[] bibliographic_file_id = new byte[ISODCL(777, 813)]; public byte[] creation_date = new byte[ISODCL(814, 830)]; public byte[] modification_date = new byte[ISODCL(831, 847)]; public byte[] expiration_date = new byte[ISODCL(848, 864)]; public byte[] effective_date = new byte[ISODCL(865, 881)]; public byte[] file_structure_version = new byte[ISODCL(882, 882)]; public byte[] unused4 = new byte[ISODCL(883, 883)]; public byte[] application_data = new byte[ISODCL(884, 1395)]; public byte[] unused5 = new byte[ISODCL(1396, IsoUtil.MODE1_2048)]; byte[][] dataPvr = {type, id, version, unused1, system_id, volume_id, unused2, volume_space_size, unused3, volume_set_size, volume_sequence_number, logical_block_size, path_table_size, type_l_path_table, opt_type_l_path_table, type_m_path_table, opt_type_m_path_table, root_directory_record, volume_set_id, publisher_id, preparer_id, application_id, copyright_file_id, abstract_file_id, bibliographic_file_id, creation_date, modification_date, expiration_date, effective_date, file_structure_version, unused4, application_data, unused5}; public isoPvd(byte[] pvd, RandomAccessInputStream rais, int start, int sectSize, long shiftOffset) throws IOException { rais.seek(IsoUtil.offsetInSector(start, sectSize, false) + shiftOffset); if (rais.read(pvd) == -1) { throw new IOException("unable to read PVD"); } load(pvd); } void load(byte[] src) { for (int i = 0, pos = 0, max = dataPvr.length; i < max; i++) { System.arraycopy(src, pos, dataPvr[i], 0, dataPvr[i].length); pos += dataPvr[i].length; } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/IsoUtil.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.AbstractFile; /** * @author Xavier Martin */ class IsoUtil { static final int MODE1_2048 = 2048; static final int MODE2_2352 = 2352; static final int MODE2_2336 = 2336; static final int MODE2_2352_skip = 24; static final int MODE2_2336_skip = 8; static final int MODE2_EC_skip = 280; static final int WAV_header = 44; public static int guessSectorSize(AbstractFile file) { int sectSize = MODE1_2048; // cooked images are usually 2352 bytes/sector, so it's 1st check here if (file.getSize() % MODE2_2352 == 0) { sectSize = MODE2_2352; } else if (file.getSize() % MODE2_2336 == 0) { sectSize = MODE2_2336; } return sectSize; } public static int toDwordBE(byte[] p, int offset) { return ((p[offset] & 0xff) | ((p[1 + offset] & 0xff) << 8) | ((p[2 + offset] & 0xff) << 16) | ((p[3 + offset] & 0xff) << 24)); } public static int toDword(byte[] p, int offset) { return ((p[3 + offset] & 0xff) | ((p[2 + offset] & 0xff) << 8) | ((p[1 + offset] & 0xff) << 16) | ((p[offset] & 0xff) << 24)); } public static void toArray(int value, byte[] b, int offset) { b[offset] = (byte) (value & 0xff); b[offset + 1] = (byte) ((value & 0xff00) >> 8); b[offset + 2] = (byte) ((value & 0xff0000) >> 16); b[offset + 3] = (byte) ((value & 0xff000000) >> 24); } public static long offsetInSector(long index, int sectSize, boolean audio) { switch (sectSize) { case MODE2_2352: return (index * 2352) + (audio ? 0 : 24); case MODE2_2336: return (index * 2336) + 8; } return index << 11; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/MuCreateISO.java ================================================ /* * Copyright (c) 2010. Stephen Connolly. * Copyright (C) 2007. Jens Hatlak * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /* * This file is copied from https://github.com/stephenc/java-iso-tools/blob/master/iso9660-writer/src/main/java/com/github/stephenc/javaisotools/iso9660/impl/CreateISO.java * and has been modified to be able to retrieve data from the arhiving process */ package com.mucommander.commons.file.impl.iso; import com.github.stephenc.javaisotools.eltorito.impl.ElToritoConfig; import com.github.stephenc.javaisotools.eltorito.impl.ElToritoHandler; import com.github.stephenc.javaisotools.iso9660.ISO9660RootDirectory; import com.github.stephenc.javaisotools.iso9660.impl.ISO9660Config; import com.github.stephenc.javaisotools.iso9660.impl.ISO9660Element; import com.github.stephenc.javaisotools.iso9660.impl.ISO9660Handler; import com.github.stephenc.javaisotools.iso9660.impl.LogicalSectorPaddingHandler; import com.github.stephenc.javaisotools.joliet.impl.JolietConfig; import com.github.stephenc.javaisotools.joliet.impl.JolietHandler; import com.github.stephenc.javaisotools.rockridge.impl.RockRidgeConfig; import com.github.stephenc.javaisotools.sabre.HandlerException; import com.github.stephenc.javaisotools.sabre.StreamHandler; public class MuCreateISO { private final ISO9660RootDirectory root; private StreamHandler streamHandler; private MuFileHandler fileHandler; public MuCreateISO(StreamHandler streamHandler, ISO9660RootDirectory root) { this.streamHandler = new LogicalSectorPaddingHandler(streamHandler, streamHandler); this.root = root; } public void process(ISO9660Config iso9660Config, RockRidgeConfig rrConfig, JolietConfig jolietConfig, ElToritoConfig elToritoConfig) throws HandlerException { if (iso9660Config == null) { throw new NullPointerException("Cannot create ISO without ISO9660Config."); } ((LogicalSectorPaddingHandler) streamHandler).setPadEnd(iso9660Config.getPadEnd()); // Last handler added processes data first if (jolietConfig != null) { streamHandler = new JolietHandler(streamHandler, root, jolietConfig); } if (elToritoConfig != null) { streamHandler = new ElToritoHandler(streamHandler, elToritoConfig); } streamHandler = new ISO9660Handler(streamHandler, root, iso9660Config, rrConfig); fileHandler = new MuFileHandler(streamHandler, root); streamHandler = fileHandler; streamHandler.startDocument(); // System Area streamHandler.startElement(new ISO9660Element("SA")); streamHandler.endElement(); // Volume Descriptor Set streamHandler.startElement(new ISO9660Element("VDS")); streamHandler.endElement(); // Boot Info Area streamHandler.startElement(new ISO9660Element("BIA")); streamHandler.endElement(); // Path Table Area streamHandler.startElement(new ISO9660Element("PTA")); streamHandler.endElement(); // Directory Records Area streamHandler.startElement(new ISO9660Element("DRA")); streamHandler.endElement(); // Boot Data Area streamHandler.startElement(new ISO9660Element("BDA")); streamHandler.endElement(); // File Contents Area streamHandler.startElement(new ISO9660Element("FCA")); streamHandler.endElement(); streamHandler.endDocument(); } /** * @return Name of current file being processed */ public String getProcessingFile(){ return fileHandler != null ? fileHandler.getProcessingFile() : null; } /** * Written bytes in total without the current file progress * @return number of bytes written as a long */ public long totalWrittenBytes(){ return fileHandler != null ? fileHandler.totalWrittenBytes(): 0; } /** * Written bytes to the current file being processed, will be the same size as the * file if complete. * @return number of bytes written as a long */ public long writtenBytesCurrentFile() { return fileHandler != null ? fileHandler.writtenBytesCurrentFile(): 0; } /** * @return Size of the current file being processed in bytes */ public long currentFileLength(){ return fileHandler != null ? fileHandler.currentFileLength(): 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/MuFileHandler.java ================================================ /* * Copyright (c) 2010. Stephen Connolly. * Copyright (C) 2007. Jens Hatlak * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /* * This file is copied from https://github.com/stephenc/java-iso-tools/blob/master/iso9660-writer/src/main/java/com/github/stephenc/javaisotools/iso9660/impl/FileHandler.java * and has been modified to be able to retrieve data from the arhiving process */ package com.mucommander.commons.file.impl.iso; import com.github.stephenc.javaisotools.iso9660.ISO9660Directory; import com.github.stephenc.javaisotools.iso9660.ISO9660File; import com.github.stephenc.javaisotools.iso9660.ISO9660RootDirectory; import com.github.stephenc.javaisotools.iso9660.impl.FileElement; import com.github.stephenc.javaisotools.iso9660.impl.ISO9660Element; import com.github.stephenc.javaisotools.sabre.DataReference; import com.github.stephenc.javaisotools.sabre.Element; import com.github.stephenc.javaisotools.sabre.HandlerException; import com.github.stephenc.javaisotools.sabre.StreamHandler; import com.github.stephenc.javaisotools.sabre.impl.ChainingStreamHandler; import java.io.IOException; import java.io.InputStream; import java.util.Iterator; public class MuFileHandler extends ChainingStreamHandler { private final ISO9660RootDirectory root; private String processingFile = null; private DataReferenceProgress drp = null; private long totalWritenBytes = 0; public MuFileHandler(StreamHandler streamHandler, ISO9660RootDirectory root) { super(streamHandler, streamHandler); this.root = root; } public void startElement(Element element) throws HandlerException { if (element instanceof ISO9660Element) { String id = (String) element.getId(); process(id); } super.startElement(element); } private void process(String id) throws HandlerException { if (id.equals("FCA")) { doFCA(); } } private void doFCA() throws HandlerException { doFCADirs(root); Iterator it = root.sortedIterator(); while (it.hasNext()) { ISO9660Directory dir = it.next(); doFCADirs(dir); } } private void doFCADirs(ISO9660Directory dir) throws HandlerException { for (ISO9660File file : dir.getFiles()) { doFile(file); } } private void doFile(ISO9660File file) throws HandlerException { processingFile = file.getName(); DataReferenceProgress dataReferenceProgress = new DataReferenceProgress(file.getDataReference()); drp = dataReferenceProgress; super.startElement(new FileElement(file)); data(dataReferenceProgress); super.endElement(); totalWritenBytes += currentFileLength(); } /** * @return Name of current file being processed */ public String getProcessingFile() { return processingFile; } /** * Written bytes in total without the current file progress * @return number of bytes written as a long */ public long totalWrittenBytes(){ return drp != null ? totalWritenBytes : 0; } /** * @return Size of the current file being processed in bytes */ public long currentFileLength(){ return drp != null ? drp.getLength() : 0; } /** * Written bytes to the current file being processed, will be the same size as the * file if complete. * @return number of bytes written as a long */ public long writtenBytesCurrentFile() { return drp != null ? drp.getWrittenBytes(): 0; } /* * DataReference wrapper that will allow the progress to be retrieved */ private static class DataReferenceProgress implements DataReference{ private final DataReference dr; private InputStream is = null; DataReferenceProgress(DataReference dr){ this.dr = dr; } @Override public long getLength() { return dr.getLength(); } public int available(){ try { return is != null ? is.available() : 0; } catch (IOException ex) {} return 0; } public long getWrittenBytes(){ return is != null ? getLength() - available(): 0; } @Override public InputStream createInputStream() throws IOException { is = dr.createInputStream(); return is; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/NrgParser.java ================================================ package com.mucommander.commons.file.impl.iso; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.io.RandomAccessInputStream; import java.io.IOException; import java.util.List; import java.util.Vector; /** * @see NRG file format on Wikipedia * @author Xavier Martin */ class NrgParser extends IsoParser { static List getEntries(byte[] buffer, AbstractFile file, RandomAccessInputStream rais) throws Exception { int sectSize = IsoUtil.MODE1_2048; // sector shift : 0 most of the time long sector_offset = 0; // bytes : depend if there's earlier track we discard long shiftOffset = 0; int len = rais.available(); int[] tracksMode = new int[255]; long[] tracksOffset = new long[255]; long[] tracksStart = new long[255]; long[] tracksEnd = new long[255]; int tracks = 0; for (int i = 7; i <= 11; i += 4) { long offset = -1; rais.seek(len - i); if (rais.read(buffer, 0, (i == 7) ? 4 : 12) == -1) throw new IOException("unable to read tail of nrg file"); if (buffer[0] == 'N' && buffer[1] == 'E' && buffer[2] == 'R' && buffer[3] == '5') // v2 footer offset = IsoUtil.toDword(buffer, 8); else if (buffer[0] == 'N' && buffer[1] == 'E' && buffer[2] == 'R' && buffer[3] == 'O') // v1 footer offset = IsoUtil.toDword(buffer, 4); if (offset == -1) continue; // read chunks boolean end = false; for (int j = 0; j < 255 && !end; j++) { long clen; rais.seek(offset); if (rais.read(buffer, 0, 8) == -1) throw new IOException("unable to read chunk in tail of nrg file"); if (buffer[0] == 'E' && buffer[1] == 'N' && buffer[2] == 'D' && buffer[3] == '!') end = true; else if (buffer[0] == 'E' && buffer[1] == 'T' && buffer[2] == 'N') { clen = IsoUtil.toDword(buffer, 4); boolean ETN2 = buffer[3] == '2'; if (rais.read(buffer, 0, (int) clen) == -1) throw new IOException("unable to read chunk in tail of nrg file"); for (int z = 0; z < clen; z += ETN2 ? 32 : 20) { tracksOffset[tracks] = IsoUtil.toDword(buffer, ETN2 ? 4 : 0); tracksMode[tracks] = IsoUtil.toDword(buffer, ETN2 ? 16 : 8); tracks++; } end = true; } else if (buffer[0] == 'D' && buffer[1] == 'A' && buffer[2] == 'O') { boolean DAOX = buffer[3] == 'X'; clen = IsoUtil.toDword(buffer, 4); offset += 8 + clen; // skip endian if (rais.read(buffer, 0, 4) == -1) throw new IOException("unable to skip endian in DAO chunk"); if (rais.read(buffer, 0, (int) clen) == -1) throw new IOException("unable to read DAO chunk"); int first = buffer[16]; int cur = first - 1; for (int z = 18; z < clen - 4; z += DAOX ? 42 : 30) { tracksMode[cur] = buffer[z + 14]; tracksOffset[cur] = IsoUtil.toDword(buffer, DAOX ? z + 30 : 22); tracksEnd[cur] = IsoUtil.toDword(buffer, DAOX ? z + 38 : 26) - 1; cur++; } tracks = cur; rais.seek(offset); // TODO CUES } else if (buffer[0] == 'C' && buffer[1] == 'U' && buffer[2] == 'E' && buffer[3] == 'X') { clen = IsoUtil.toDword(buffer, 4); offset += 8 + clen; if (rais.read(buffer, 0, (int) clen) == -1) throw new IOException("unable to read CUEX chunk"); for (int z = 0; z < clen; z += 8) { //long toc = IsoUtil.toDword(buffer, z); long tstart = IsoUtil.toDword(buffer, z + 4); if (buffer[z + 2] == 0 || (buffer[z + 1] & 0xff) == 0xAA) { // skip toc/pregap ? } else { tracksStart[tracks] = tstart; tracks++; } } rais.seek(offset); } else { // skip irrelevant chunk clen = IsoUtil.toDword(buffer, 4); offset += 8 + clen; rais.seek(offset); } } boolean audioOnly = true; for (int k = 0; k < tracks; k++) { shiftOffset = tracksOffset[k]; sector_offset = tracksStart[k]; switch (tracksMode[k]) { case 0: sectSize = IsoUtil.MODE1_2048; audioOnly = false; break; case 3: sectSize = IsoUtil.MODE2_2336; audioOnly = false; break; case 7: // audio 2352 - 2352 sectSize = IsoUtil.MODE2_2352; break; case 16: // TODO find a sample image w/ subchannel data // audio 2448 - 2352 break; default: throw new Exception("unhandled mode " + tracksMode[k]); } } // fun : handle audio disc :) if (audioOnly) { List entries = new Vector<>(); for (int k = 0; k < tracks; k++) { entries.add( new IsoArchiveEntry( file.getName() + ".TRACK" + (k + 1) + ".wav", false, file.getLastModifiedDate(), tracksEnd[k] - tracksOffset[k] + IsoUtil.WAV_header, // adding wav header 0, sectSize, tracksOffset[k], true) ); } return entries; } } return getEntries(buffer, rais, sectSize, sector_offset, shiftOffset); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/iso/package.html ================================================ Provides an implementation of the ISO archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/local/LocalFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.local; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.util.Kernel32; import com.mucommander.commons.file.util.Kernel32API; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.FilteredOutputStream; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import java.nio.Buffer; import ru.trolsoft.jni.NativeFileUtils; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileOwnerAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.UserPrincipal; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * LocalFile provides access to files located on a locally-mounted filesystem. * Note that despite the class' name, LocalFile instances may indifferently be residing on a local hard drive, * or on a remote server mounted locally by the operating system. * *

    The associated {@link FileURL} scheme is {@link FileProtocols#FILE}. The host part should be {@link FileURL#LOCALHOST}, * except for Windows UNC URLs (see below). Native path separators ('/' or '\\' depending on the OS) can be used * in the path part. * *

    Here are a few examples of valid local file URLs: * * file://localhost/C:\winnt\system32\
    * file://localhost/usr/bin/gcc
    * file://localhost/~
    * file://home/maxence/..
    *
    * *

    Windows UNC paths can be represented as FileURL instances, using the host part of the URL. The URL format for * those is the following:
    * file:\\server\share .
    * *

    Under Windows, LocalFile will translate those URLs back into a UNC path. For example, a LocalFile created with the * file://garfield/stuff FileURL will have the getAbsolutePath() method return * \\garfield\stuff. Note that this UNC path translation doesn't happen on OSes other than Windows, which * would not be able to handle the path. * *

    Access to local files is provided by the java.io API, {@link #getUnderlyingFileObject()} allows * to retrieve an java.io.File instance corresponding to this LocalFile. * * @author Maxence Bernard */ public class LocalFile extends ProtocolFile { private final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LocalFile.class); private static final boolean IS_UNIX_BASED = OsFamily.getCurrent().isUnixBased(); /** Are we running Windows ? */ private static final boolean IS_WINDOWS = OsFamily.WINDOWS.isCurrent(); protected File file; private final FilePermissions permissions; /** Absolute file path, free of trailing separator */ protected String absPath; /** Caches the parent folder, initially null until getParent() gets called */ protected AbstractFile parent; /** Indicates whether the parent folder instance has been retrieved and cached or not (parent can be null) */ private boolean parentValueSet; /** Underlying local filesystem's path separator: "/" under UNIX systems, "\" under Windows and OS/2 */ public final static String SEPARATOR = File.separator; /** True if the underlying local filesystem uses drives assigned to letters (e.g. A:\, C:\, ...) instead * of having single a root folder '/' */ public final static boolean USES_ROOT_DRIVES = IS_WINDOWS || OsFamily.OS_2.isCurrent(); /** Pattern matching Windows-like drives' root, e.g. C:\ */ final static Pattern DRIVE_ROOT_PATTERN = Pattern.compile("^[a-zA-Z][:][\\\\]"); // Permissions can only be changed under Java 1.6 and up and are limited to 'user' access. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write) and as such can't be changed. /** Changeable permissions mask for Java 1.6 and up, on OSes other than Windows */ private static final PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_6_NON_WINDOWS = new GroupedPermissionBits(448); // rwx------ (700 octal) /** Changeable permissions mask for Java 1.6 and up, on Windows OS (any version) */ private static final PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS = new GroupedPermissionBits(128); // -w------- (200 octal) // /** Changeable permissions mask for Java 1.5 or below */ // private static PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_5 = PermissionBits.EMPTY_PERMISSION_BITS; // --------- (0) /** Bit mask that indicates which permissions can be changed */ private final static PermissionBits CHANGEABLE_PERMISSIONS = IS_WINDOWS ? CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS : CHANGEABLE_PERMISSIONS_JAVA_1_6_NON_WINDOWS; // JavaVersion.JAVA_1_6.isCurrentOrHigher() // ? (IS_WINDOWS ? CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS : CHANGEABLE_PERMISSIONS_JAVA_1_6_NON_WINDOWS) // : CHANGEABLE_PERMISSIONS_JAVA_1_5; /** * List of known UNIX filesystems. */ private static final String[] KNOWN_UNIX_FS = { "adfs", "affs", "autofs", "cifs", "coda", "cramfs", "debugfs", "efs", "ext2", "ext3", "fuseblk", "hfs", "hfsplus", "hpfs", "iso9660", "jfs", "minix", "msdos", "ncpfs", "nfs", "nfs4", "ntfs", "qnx4", "reiserfs", "smbfs", "udf", "ufs", "usbfs", "vfat", "xfs" }; private static final boolean NATIVE_FILE_UTILS_AVAILABLE = OsFamily.MAC_OS_X.isCurrent() && NativeFileUtils.init(); static { // Prevents Windows from poping up a message box when it cannot find a file. Those message box are triggered by // java.io.File methods when operating on removable drives such as floppy or CD-ROM drives which have no disk // inserted. // This has been fixed in Java 1.6 b55 but this fixes previous versions of Java. // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4089199 if (IS_WINDOWS && Kernel32.isAvailable()) { Kernel32.getInstance().SetErrorMode(Kernel32API.SEM_NOOPENFILEERRORBOX|Kernel32API.SEM_FAILCRITICALERRORS); } } /** * @param fileURL file URL * * Creates a new instance of LocalFile and a corresponding {@link File} instance. */ protected LocalFile(FileURL fileURL) throws IOException { this(fileURL, null); } /** * Creates a new instance of LocalFile, using the given {@link File} if not null, creating a new * {@link File} instance otherwise. * * @param fileURL * @param file */ protected LocalFile(FileURL fileURL, File file) throws IOException { super(fileURL); if (file == null) { String path = fileURL.getPath(); // Remove the leading '/' for Windows-like paths if (USES_ROOT_DRIVES) { path = path.substring(1); } // create the java.io.File instance and throw an exception if the path is not absolute. file = new File(path); if (!file.isAbsolute()) { throw new IOException(); } absPath = file.getAbsolutePath(); // Remove the trailing separator if present if (absPath.endsWith(SEPARATOR)) { absPath = absPath.substring(0, absPath.length() - 1); } } // the java.io.File instance was created by ls(), no need to re-create it or call the costly File#getAbsolutePath() else { this.absPath = fileURL.getPath(); // Remove the leading '/' for Windows-like paths if (USES_ROOT_DRIVES) { absPath = absPath.substring(1); } } this.file = file; this.permissions = new LocalFilePermissions(file); } /** * Returns the user home folder. Most if not all OSes have one, but in the unlikely event that the OS doesn't have * one or that the folder cannot be resolved, null will be returned. * * @return the user home folder */ public static AbstractFile getUserHome() { String userHomePath = System.getProperty("user.home"); return userHomePath == null ? null : FileFactory.getFile(userHomePath); } /** * Returns the total and free space on the volume where this file resides. * *

    Using this method to retrieve both free space and volume space is more efficient than calling * {@link #getFreeSpace()} and {@link #getTotalSpace()} separately -- the underlying method retrieving both * attributes at the same time. * * @return a {totalSpace, freeSpace} long array, both values can be null if the information could not be retrieved * @throws IOException if an I/O error occurred */ public long[] getVolumeInfo() throws IOException { // Under Java 1.6 and up, use the (new) java.io.File methods // if (JavaVersion.JAVA_1_6.isCurrentOrHigher()) { return new long[] { getTotalSpace(), getFreeSpace() }; // } // Under Java 1.5 or lower, use native methods // return getNativeVolumeInfo(); } // /** // * Uses platform dependent functions to retrieve the total and free space on the volume where this file resides. // * // * @return a {totalSpace, freeSpace} long array, both values can be null if the information could not // * be retrieved. // * @throws IOException if an I/O error occurred // */ // protected long[] getNativeVolumeInfo() throws IOException { // if (IS_WINDOWS) { // return getWindowsVolumeInfo(getAbsolutePath()); // } else if (IS_UNIX_BASED) { // return getUnixVolumeInfo(getAbsolutePath()); // } // // return new long[]{-1, -1}; // } // private long[] getWindowsVolumeInfo(String absPath) throws IOException { // long dfInfo[] = new long[]{-1, -1}; // // Use the Kernel32 DLL if it is available // if (Kernel32.isAvailable()) { // // Retrieves the total and free space information using the GetDiskFreeSpaceEx function of the // // Kernel32 API. // LongByReference totalSpaceLBR = new LongByReference(); // LongByReference freeSpaceLBR = new LongByReference(); // // if (Kernel32.getInstance().GetDiskFreeSpaceEx(absPath, null, totalSpaceLBR, freeSpaceLBR)) { // dfInfo[0] = totalSpaceLBR.getValue(); // dfInfo[1] = freeSpaceLBR.getValue(); // } else { // logger.warn("Call to GetDiskFreeSpaceEx failed, absPath={}", absPath); // } // } else if (OsVersion.WINDOWS_NT.isCurrentOrHigher()) { // // Otherwise, parse the output of 'dir "filePath"' command to retrieve free space information, if // // running Window NT or higher. // // Note: no command invocation under Windows 95/98/Me, because it causes a shell window to // // appear briefly every time this method is called (See ticket #63). // // // 'dir' command returns free space on the last line // Process process = Runtime.getRuntime().exec( // (OsVersion.getCurrent().compareTo(OsVersion.WINDOWS_NT)>=0 ? "cmd /c" : "command.com /c") // + " dir \""+absPath+"\""); // // // Check that the process was correctly started // if (process != null) { // String lastLine = readLastLine(process.getInputStream()); // // // Last dir line may look like something this (might vary depending on system's language, below in French): // // 6 Rep(s) 14 767 521 792 octets libres // if (lastLine != null) { // StringTokenizer st = new StringTokenizer(lastLine, " \t\n\r\f,."); // // Discard first token // st.nextToken(); // // // Concatenates as many contiguous groups of numbers // String token; // String freeSpace = ""; // while(st.hasMoreTokens()) { // token = st.nextToken(); // char c = token.charAt(0); // if (c >= '0' && c <= '9') { // freeSpace += token; // } else if(!freeSpace.isEmpty()) { // break; // } // } // // dfInfo[1] = Long.parseLong(freeSpace); // } // } // } // return dfInfo; // } // // private long[] getUnixVolumeInfo(String absPath) throws IOException { // long dfInfo[] = new long[]{-1, -1}; // // Parses the output of 'df -P -k "filePath"' command on UNIX-based systems to retrieve free and total space information // // // 'df -P -k' returns totals in block of 1K = 1024 bytes, -P uses the POSIX output format, ensures that line won't break // Process process = Runtime.getRuntime().exec(new String[]{"df", "-P", "-k", absPath}, null, file); // // // Check that the process was correctly started // if (process != null) { // // Discard the first line ("Filesystem 1K-blocks Used Avail Capacity Mounted on"); // String line = readSecondLine(process.getInputStream()); // // // Sample lines: // // /dev/disk0s2 116538416 109846712 6179704 95% / // // automount -fstab [202] 0 0 0 100% /automount/Servers // // /dev/disk2s2 2520 1548 972 61% /Volumes/muCommander 0.8 // // // We're interested in the '1K-blocks' and 'Avail' fields (only). // // The 'Filesystem' and 'Mounted On' fields can contain spaces (e.g. 'automount -fstab [202]' and // // '/Volumes/muCommander 0.8' resp.) and therefore be made of several tokens. A stable way to // // determine the position of the fields we're interested in is to look for the last token that // // starts with a '/' character which should necessarily correspond to the first token of the // // 'Mounted on' field. The '1K-blocks' and 'Avail' fields are 4 and 2 tokens away from it // // respectively. // // // Start by tokenizing the whole line // List tokenV = new ArrayList<>(); // if (line != null) { // StringTokenizer st = new StringTokenizer(line); // while(st.hasMoreTokens()) { // tokenV.add(st.nextToken()); // } // } // // int nbTokens = tokenV.size(); // if (nbTokens < 6) { // // This shouldn't normally happen // logger.warn("Failed to parse output of df -k {} line={}", absPath, line); // return dfInfo; // } // // // Find the last token starting with '/' // int pos = nbTokens-1; // while (!tokenV.get(pos).startsWith("/")) { // if (pos == 0) { // // This shouldn't normally happen // logger.warn("Failed to parse output of df -k {} line={}", absPath, line); // return dfInfo; // } // // --pos; // } // // // '1-blocks' field (total space) // dfInfo[0] = Long.parseLong(tokenV.get(pos - 4)) * 1024; // // 'Avail' field (free space) // dfInfo[1] = Long.parseLong(tokenV.get(pos - 2)) * 1024; // } // //// // Retrieves the total and free space information using the POSIX statvfs function //// POSIX.STATVFSSTRUCT struct = new POSIX.STATVFSSTRUCT(); //// if(POSIX.INSTANCE.statvfs(absPath, struct)==0) { //// dfInfo[0] = struct.f_blocks * (long)struct.f_frsize; //// dfInfo[1] = struct.f_bfree * (long)struct.f_frsize; //// } // // return dfInfo; // } // // // @Nullable // private String readLastLine(InputStream is) throws IOException { // String lastLine = null; // try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { // String line; // // Retrieves last line of dir // while ((line = br.readLine()) != null) { // if (!line.trim().isEmpty()) { // lastLine = line; // } // } // return lastLine; // } // } // // private static String readSecondLine(InputStream is) throws IOException { // try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { // br.readLine(); // return br.readLine(); // } // } /** * Attemps to detect if this file is the root of a removable media drive (floppy, CD, DVD, USB drive...). * This method produces accurate results only under Windows. * * @return true if this file is the root of a removable media drive (floppy, CD, DVD, USB drive...). */ public boolean guessRemovableDrive() { if (IS_WINDOWS && Kernel32.isAvailable()) { int driveType = Kernel32.getInstance().GetDriveType(getAbsolutePath(true)); if (driveType != Kernel32API.DRIVE_UNKNOWN) { return driveType == Kernel32API.DRIVE_REMOVABLE || driveType == Kernel32API.DRIVE_CDROM; } } // For other OS that have root drives (OS/2), a weak way to characterize removable drives is by checking if the // corresponding root folder is read-only. return hasRootDrives() && isRoot() && !file.canWrite(); } /** * Returns true if the underlying local filesystem uses drives assigned to letters (e.g. A:\, C:\, ...) * instead of having a single root folder '/' under which mount points are attached. * This is true for the following platforms: *

      *
    • Windows
    • *
    • OS/2
    • *
    • Any other platform that has '\' for a path separator
    • *
    * * @return true if the underlying local filesystem uses drives assigned to letters */ public static boolean hasRootDrives() { return IS_WINDOWS || OsFamily.OS_2.isCurrent() || "\\".equals(SEPARATOR); } /** * Resolves and returns all local volumes: *
      *
    • On UNIX-based OSes, these are the mount points declared in /etc/ftab.
    • *
    • On the Windows platform, these are the drives displayed in Explorer. Some of the returned volumes may * correspond to removable drives and thus may not always be available -- if they aren't, {@link #exists()} will * return false.
    • *
    *

    * The return list of volumes is purposively not cached so that new volumes will be returned as soon as they are * mounted. * * @return all local volumes */ public static AbstractFile[] getVolumes() { List volumesV = new ArrayList<>(); // Add Mac OS X's /Volumes subfolders and not file roots ('/') since Volumes already contains a named link // (like 'Hard drive' or whatever silly name the user gave his primary hard disk) to / if (OsFamily.MAC_OS_X.isCurrent()) { addMacOSXVolumes(volumesV); } else { // Add java.io.File's root folders addJavaIoFileRoots(volumesV); // Add /proc/mounts folders under UNIX-based systems. if (IS_UNIX_BASED) { addMountEntries(volumesV); } } // Add home folder, if it is not already present in the list AbstractFile homeFolder = getUserHome(); if (!(homeFolder == null || volumesV.contains(homeFolder))) { volumesV.add(homeFolder); } AbstractFile[] volumes = new AbstractFile[volumesV.size()]; volumesV.toArray(volumes); return volumes; } /** * Resolves the root folders returned by {@link File#listRoots()} and adds them to the given Vector. * * @param v the Vector to add root folders to */ private static void addJavaIoFileRoots(List v) { // Warning : No file operation should be performed on the resolved folders as under Win32, this would cause a // dialog to appear for removable drives such as A:\ if no disk is present. // File[] fileRoots = File.listRoots(); // for (File fileRoot : fileRoots) { // try { // v.add(FileFactory.getFile(fileRoot.getAbsolutePath(), true)); // } catch (IOException ignore) {} // } for (Path p : FileSystems.getDefault().getRootDirectories()) { try { v.add(FileFactory.getFile(p.toString(), true)); } catch (IOException ignore) {} } } /** * Parses the output of /sbin/mount -p on FreeBSD or the /proc/mounts kernel virtual file * otherwise, resolves all the mount points that look like regular filesystems it contains and adds them to * the given List. * * @param list the List to add mount points to */ private static void addMountEntries(List list) { try (BufferedReader br = new BufferedReader(new InputStreamReader(streamMountPoints()))) { String line; // read each line in file and parse it while ((line = br.readLine()) != null) { line = line.trim(); // split line into tokens separated by " \t\n\r\f" // tokens are: device, mount_point, fs_type, attributes, fs_freq, fs_passno StringTokenizer st = new StringTokenizer(line); st.nextToken(); String mountPoint = st.nextToken().replace("\\040", " "); String fsType = st.nextToken(); if (isKnownFileSystem(fsType)) { AbstractFile file = FileFactory.getFile(mountPoint); if (file != null && !list.contains(file)) { list.add(file); } } } } catch (Exception e) { String warning = "Error parsing" + (OsFamily.FREEBSD.isCurrent() ? "/sbin/mount -p output" : "/proc/mounts entries"); logger.warn(warning, e); } } private static InputStream streamMountPoints() throws IOException { return OsFamily.FREEBSD.isCurrent() ? new ProcessBuilder("/sbin/mount", "-p").start().getInputStream() : new FileInputStream("/proc/mounts"); } private static boolean isKnownFileSystem(String fsType) { for (String fs : KNOWN_UNIX_FS) { if (fs.equals(fsType)) { // this is really known physical FS return true; } } return false; } /** * Adds all /Volumes subfolders to the given Vector. * * @param v the Vector to add the volumes to */ private static void addMacOSXVolumes(List v) { // /Volumes not resolved for some reason, giving up AbstractFile volumesFolder = FileFactory.getFile("/Volumes"); if (volumesFolder == null) { return; } // Adds subfolders try { AbstractFile[] volumesFiles = volumesFolder.ls(); for (AbstractFile folder : volumesFiles) { if (folder.isDirectory()) { // The primary hard drive (the one corresponding to '/') is listed under Volumes and should be // returned as the first volume if (folder.getCanonicalPath().equals("/")) { v.add(0, folder); } else { v.add(folder); } } } } catch(IOException e) { logger.warn("Can't get /Volumes subfolders", e); } } /** * Returns a java.io.File instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } @Override public boolean isSymlink() { // At the moment symlinks under Windows (aka NTFS junction points) are not supported because java.io.File // knows nothing about them and there is no way to discriminate them. So there is no need to waste time // comparing canonical paths, just return false. // TODO: add support for .lnk files (~hard links) if (IS_WINDOWS) { return false; } // Check the case if we have a symbolic link with wrong target path if (!file.isFile()) { Path path = FileSystems.getDefault().getPath(getAbsolutePath(), ""); if (Files.isSymbolicLink(path)) { return true; } } // Note: this value must not be cached as its value can change over time (canonical path can change) return Files.isSymbolicLink(file.toPath()); // AbstractFile parent = getParent(); // String canonPath = getCanonicalPath(false); // if (parent == null || canonPath == null) { // return false; // } // String parentCanonPath = parent.getCanonicalPath(true); // return !canonPath.equalsIgnoreCase(parentCanonPath+getName()); } @Override public boolean isSystem() { if (OsFamily.MAC_OS_X.isCurrent()) { return MacOsSystemFolder.isSystemFile(this); } if (OsFamily.WINDOWS.isCurrent()) { if (!Kernel32.isAvailable()) { return false; } String filePath = file.getAbsolutePath(); int attributes = Kernel32.getInstance().GetFileAttributes(filePath); // if GetFileAttributes() fails we try FindFirstFile() as fallback // such a case would be pagefile.sys if (attributes == Kernel32API.INVALID_FILE_ATTRIBUTES) { Kernel32API.FindFileHandle findFileHandle = null; Kernel32API.WIN32_FIND_DATA findFileData = new Kernel32API.WIN32_FIND_DATA(); try { findFileHandle = Kernel32.getInstance().FindFirstFile(filePath, findFileData); if (findFileHandle.isValid()) { attributes = findFileData.dwFileAttributes; } } finally { if (findFileHandle != null && findFileHandle.isValid()) { Kernel32.getInstance().FindClose(findFileHandle); } } } return (attributes & Kernel32API.FILE_ATTRIBUTE_SYSTEM) != 0; } return false; } @Override public long getLastModifiedDate() { return file.lastModified(); } @Override public long getCreationDate() throws IOException { return Files.readAttributes(file.toPath(), BasicFileAttributes.class).creationTime().toMillis(); } @Override public long getLastAccessDate() throws IOException { return Files.readAttributes(file.toPath(), BasicFileAttributes.class).lastAccessTime().toMillis(); } @Override public void setLastModifiedDate(long lastModified) throws IOException { // java.io.File#setLastModified(long) throws an IllegalArgumentException if time is negative. // If specified time is negative, set it to 0 (01/01/1970). if (lastModified < 0) { lastModified = 0; } if (!file.setLastModified(lastModified)) { throw new IOException(); } } @Override public long getSize() { return file.length(); } @Override public AbstractFile getParent() { // Retrieve the parent AbstractFile instance and cache it if (!parentValueSet) { if (!isRoot()) { FileURL parentURL = getURL().getParent(); if (parentURL != null) { parent = FileFactory.getFile(parentURL); } } parentValueSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValueSet = true; } @Override public boolean exists() { return file.exists(); } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { return CHANGEABLE_PERMISSIONS; } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { // Only the 'user' permissions under Java 1.6 are supported if (access != USER_ACCESS) { throw new IOException(); } boolean success = false; if (permission == READ_PERMISSION) { success = file.setReadable(enabled); } else if (permission == WRITE_PERMISSION) { success = file.setWritable(enabled); } else if (permission == EXECUTE_PERMISSION) { success = file.setExecutable(enabled); } if (!success) { throw new IOException(); } } // private String getOwnerPosix() { // Path path = Paths.get(file.toURI()); // try { // PosixFileAttributes attr = Files.readAttributes(path, PosixFileAttributes.class); // return attr.owner().getName(); // } catch (IOException | UnsupportedOperationException e) { // e.printStackTrace(); // return null; // } // } @Override public String getOwner() { try { Path path = Paths.get(file.toURI()); if (Files.exists(path)) { FileOwnerAttributeView ownerAttributeView = Files.getFileAttributeView(path, FileOwnerAttributeView.class); UserPrincipal owner = ownerAttributeView.getOwner(); return owner.getName(); // PosixFileAttributes attr = Files.readAttributes(path, // PosixFileAttributes.class); // return attr.owner().getName(); } else { return null; } } catch (IOException | UnsupportedOperationException e) { e.printStackTrace(); return null; } } /** * Always returns true for unix-based systems */ @Override public boolean canGetOwner() { return IS_UNIX_BASED; } @Override public String getGroup() { Path path = Paths.get(file.toURI()); if (!Files.exists(path)) { return null; } try { PosixFileAttributes attr = Files.readAttributes(path, PosixFileAttributes.class); return attr.group().getName(); } catch (UnsupportedOperationException e) { logger.debug("File {} doesn't have an owner: {}. Enable trace to see the exception.", file.getAbsolutePath(), e.getMessage()); logger.trace("File {} doesn't have an owner: {}.", file.getAbsolutePath(), e.getMessage(), e); return null; } catch (IOException e) { logger.warn("Error for [{}]", file.getAbsolutePath(), e); // DosFileAttributeView dos = Files.getFileAttributeView( // path, DosFileAttributeView.class); // AclFileAttributeView aclFileAttributes = Files.getFileAttributeView(path, // AclFileAttributeView.class); // // for (AclEntry aclEntry : aclFileAttributes.getAcl()) { // System.out.println(aclEntry.principal() + ":"); // System.out.println(aclEntry.permissions() + "\n"); // } return null; } } /** * Always returns true for unit based systems */ @Override public boolean canGetGroup() { return IS_UNIX_BASED; } @Override public boolean isDirectory() { if (NATIVE_FILE_UTILS_AVAILABLE) { return NativeFileUtils.isLocalDirectory(file.getAbsolutePath()); } // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.isDirectory() // if(IS_WINDOWS && guessFloppyDrive()) // return true; return file.isDirectory(); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). */ @Override public InputStream getInputStream() throws IOException { return new LocalInputStream(new FileInputStream(file).getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). */ @Override public OutputStream getOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, false).getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). */ @Override public OutputStream getAppendOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, true).getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). */ @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new LocalRandomAccessInputStream(new RandomAccessFile(file, "r").getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). */ @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return new LocalRandomAccessOutputStream(new RandomAccessFile(file, "rw").getChannel()); } @Override public void delete() throws IOException { if (!file.delete()) { throw new IOException(); } } @Override public AbstractFile[] ls() throws IOException { return ls(null); } @Override public void mkdir() throws IOException { Path path = FileSystems.getDefault().getPath(getAbsolutePath(), ""); try { Files.createDirectory(path); } catch (AccessDeniedException e) { throw new FileAccessDeniedException(); } catch (IOException e) { throw e; } catch (Throwable t) { throw new IOException(t); } } @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // Fail in some situations where java.io.File#renameTo() doesn't. // Note that java.io.File#renameTo()'s implementation is system-dependant, so it's always a good idea to // perform all those checks even if some are not necessary on this or that platform. checkRenamePrerequisites(destFile, true, false); // The behavior of java.io.File#renameTo() when the destination file already exists is not consistent // across platforms: // - Under UNIX, it succeeds and return true // - Under Windows, it fails and return false // This ticket goes in great details about the issue: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4017593 // // => Since this method is required to succeed when the destination file exists, the Windows platform needs // special treatment. destFile = destFile.getTopAncestor(); File destJavaIoFile = ((LocalFile)destFile).file; if (IS_WINDOWS) { // This check is necessary under Windows because java.io.File#renameTo(java.io.File) does not return false // if the destination file is located on a different drive, contrary for example to Mac OS X where renameTo // returns false in this case. // Not doing this under Windows would mean files would get moved between drives with renameTo, which doesn't // allow the transfer to be monitored. // Note that Windows UNC paths are handled by checkRenamePrerequisites() when comparing hosts for equality. if (!getRoot().equals(destFile.getRoot())) { throw new IOException(); } // Windows 9x or Windows Me: Kernel32's MoveFileEx function is NOT available if (OsVersion.WINDOWS_ME.isCurrentOrLower()) { // The destination file is deleted before calling java.io.File#renameTo(). // Note that in this case, the atomicity of this method is not guaranteed anymore -- if // java.io.File#renameTo() fails (for whatever reason), the destination file is deleted anyway. if (destFile.exists()) { if (!destJavaIoFile.delete()) { throw new IOException(); } } } else if (Kernel32.isAvailable()) { // Windows NT: Kernel32's MoveFileEx can be used, if the Kernel32 DLL is available. // Note: MoveFileEx is always used, even if the destination file does not exist, to avoid having to // call #exists() on the destination file which has a cost. if (!Kernel32.getInstance().MoveFileEx(absPath, destFile.getAbsolutePath(), Kernel32API.MOVEFILE_REPLACE_EXISTING | Kernel32API.MOVEFILE_WRITE_THROUGH)) { String errorMessage = Integer.toString(Kernel32.getInstance().GetLastError()); // TODO: use Kernel32.FormatMessage throw new IOException("Rename using Kernel32 API failed: " + errorMessage); } else { // move successful return; } } // else fall back to java.io.File#renameTo } if (!file.renameTo(destJavaIoFile)) { throw new IOException(); } } @Override public long getFreeSpace() throws IOException { return file.getUsableSpace(); } @Override public long getTotalSpace() throws IOException { return file.getTotalSpace(); } // Unsupported file operations /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } @Override public String getName() { // If this file has no parent, return: // - the drive's name under OSes with root drives such as Windows, e.g. "C:" // - "/" under Unix-based systems if (isRoot()) { return hasRootDrives() ? absPath : "/"; } return file.getName(); } @Override public String getAbsolutePath() { // Append separator for root folders (C:\ , /) and for directories if (isRoot() || (isDirectory() && !absPath.endsWith(SEPARATOR))) { return absPath + SEPARATOR; } return absPath; } @Override public String getCanonicalPath() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.getCanonicalPath() // if(IS_WINDOWS && guessFloppyDrive()) // return absPath; // Note: canonical path must not be cached as its resolution can change over time, for instance // if a file 'Test' is renamed to 'test' in the same folder, its canonical path would still be 'Test' // if it was resolved prior to the renaming and thus be recognized as a symbolic link try { String canonicalPath = file.getCanonicalPath(); // Append separator for directories if (isDirectory() && !canonicalPath.endsWith(SEPARATOR)) { return canonicalPath + SEPARATOR; } return canonicalPath; } catch(IOException e) { return absPath; } } @Override public String getSeparator() { return SEPARATOR; } @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { File[] files = file.listFiles(filenameFilter == null ? null : new LocalFilenameFilter(filenameFilter)); if (files == null) { throw new IOException(); } int nbFiles = files.length; AbstractFile[] children = new AbstractFile[nbFiles]; for(int i = 0; i < nbFiles; i++) { // Clone the FileURL of this file and set the child's path, this is more efficient than creating a new // FileURL instance from scratch. FileURL childURL = (FileURL)fileURL.clone(); childURL.setPath(absPath + SEPARATOR + files[i].getName()); // Retrieves an AbstractFile (LocalFile or AbstractArchiveFile) instance that's potentially already in // the cache, reuse this file as the file's parent, and the already-created java.io.File instance. children[i] = FileFactory.getFile(childURL, this, files[i]); } return children; } @Override public boolean isHidden() { if (NATIVE_FILE_UTILS_AVAILABLE) { return NativeFileUtils.isLocalFileHidden(file.getAbsolutePath()); } return file.isHidden(); } @Override public boolean isExecutable() { if (NATIVE_FILE_UTILS_AVAILABLE) { return NativeFileUtils.isLocalFileExecutable(file.getAbsolutePath()); } else if (IS_UNIX_BASED) { return !file.isDirectory() && file.canExecute(); } return super.isExecutable(); } @Override public boolean canRead() { return file.canRead(); } /** * Overridden to play nice with platforms that have root drives -- for those, the drive's root (e.g. C:\) * is returned instead of /. */ @Override public AbstractFile getRoot() { if (USES_ROOT_DRIVES) { Matcher matcher = DRIVE_ROOT_PATTERN.matcher(absPath + SEPARATOR); // Test if this file already is the root folder if (matcher.matches()) { return this; } // Extract the drive from the path matcher.reset(); if (matcher.find()) { return FileFactory.getFile(matcher.group()); } } return super.getRoot(); } /** * Overridden to play nice with platforms that have root drives -- for those, true is returned if * this file's path matches the drive root's (e.g. C:\). */ @Override public boolean isRoot() { if (USES_ROOT_DRIVES) { return DRIVE_ROOT_PATTERN.matcher(absPath + SEPARATOR).matches(); } return super.isRoot(); } /** * Overridden to return the local volume on which this file is located. The returned volume is one of the volumes * returned by {@link #getVolumes()}. */ @Override public AbstractFile getVolume() { AbstractFile[] volumes = LocalFile.getVolumes(); // Looks for the volume that best matches this file, i.e. the volume that is the deepest parent of this file. // If this file is itself a volume, return it. int bestDepth = -1; int bestMatch = -1; String thisPath = getAbsolutePath(true); for (int i = 0; i < volumes.length; i++) { AbstractFile volume = volumes[i]; String volumePath = volume.getAbsolutePath(true); if (thisPath.equals(volumePath)) { return this; } else if (thisPath.startsWith(volumePath)) { int depth = PathUtils.getDepth(volumePath, volume.getSeparator()); if (depth > bestDepth) { bestDepth = depth; bestMatch = i; } } } if (bestMatch != -1) { return volumes[bestMatch]; } // If no volume matched this file (shouldn't normally happen), return the root folder return getRoot(); } /////////////////// // Inner classes // /////////////////// /** * LocalRandomAccessInputStream extends RandomAccessInputStream to provide random read access to a LocalFile. * This implementation uses a NIO FileChannel under the hood to benefit from * InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted using * Thread#interrupt(). */ public static class LocalRandomAccessInputStream extends RandomAccessInputStream { private final FileChannel channel; /* Here we use class Buffer but refer to ByteBuffer object. This fixes a problem when running a project compiled for Java 9+ on older versions (Java 8). In Java 8, while calling limit() or position() (and some other) methods of ByteBuffer class, since it has no implementation for this method, so it is actually calling the method from extended class, Buffer. However, in Java 9 and higher, ByteBuffer class has implemented its own methods, and the returning object is changed from Buffer to ByteBuffer */ private final Buffer buffer; LocalRandomAccessInputStream(FileChannel channel) { this.channel = channel; this.buffer = BufferPool.getByteBuffer(); } @Override public int read() throws IOException { synchronized(buffer) { buffer.position(0); buffer.limit(1); int nbRead = channel.read((ByteBuffer) buffer); if (nbRead <= 0) { return nbRead; } return 0xFF & ((ByteBuffer)buffer).get(0); } } @Override public int read(byte[] b, int off, int len) throws IOException { synchronized(buffer) { buffer.position(0); buffer.limit(Math.min(buffer.capacity(), len)); int nbRead = channel.read((ByteBuffer)buffer); if (nbRead <= 0) { return nbRead; } buffer.position(0); ((ByteBuffer)buffer).get(b, off, nbRead); return nbRead; } } @Override public void close() throws IOException { BufferPool.releaseByteBuffer((ByteBuffer)buffer); channel.close(); } public long getOffset() throws IOException { return channel.position(); } public long getLength() throws IOException { return channel.size(); } public void seek(long offset) throws IOException { channel.position(offset); } } /** * A replacement for java.io.FileInputStream that uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * *

    This class simply delegates all its methods to a * {@link com.mucommander.commons.file.impl.local.LocalFile.LocalRandomAccessInputStream} instance. Therefore, this class * does not derive from {@link com.mucommander.commons.io.RandomAccessInputStream}, preventing random-access methods from * being used. * */ static class LocalInputStream extends FilterInputStream { LocalInputStream(FileChannel channel) { super(new LocalRandomAccessInputStream(channel)); } } /** * A replacement for java.io.FileOutputStream that uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * *

    This class simply delegates all its methods to a * {@link com.mucommander.commons.file.impl.local.LocalFile.LocalRandomAccessOutputStream} instance. Therefore, this class * does not derive from {@link com.mucommander.commons.io.RandomAccessOutputStream}, preventing random-access methods from * being used. * */ static class LocalOutputStream extends FilteredOutputStream { LocalOutputStream(FileChannel channel) { super(new LocalRandomAccessOutputStream(channel)); } } /** * LocalRandomAccessOutputStream extends RandomAccessOutputStream to provide random write access to a LocalFile. * This implementation uses a NIO FileChannel under the hood to benefit from * InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted using * Thread#interrupt(). */ public static class LocalRandomAccessOutputStream extends RandomAccessOutputStream { private final FileChannel channel; /* Here we use class Buffer but refer to ByteBuffer object. See comment above. */ private final Buffer buffer; LocalRandomAccessOutputStream(FileChannel channel) { this.channel = channel; this.buffer = BufferPool.getByteBuffer(); } @Override public void write(int i) throws IOException { synchronized(buffer) { buffer.position(0); buffer.limit(1); ((ByteBuffer)buffer).put((byte)i); buffer.position(0); channel.write((ByteBuffer)buffer); } } @Override public void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) throws IOException { synchronized(buffer) { do { buffer.position(0); int nbToWrite = Math.min(buffer.capacity(), len); buffer.limit(nbToWrite); ((ByteBuffer)buffer).put(b, off, nbToWrite); buffer.position(0); nbToWrite = channel.write((ByteBuffer)buffer); len -= nbToWrite; off += nbToWrite; } while(len > 0); } } @Override public void setLength(long newLength) throws IOException { long currentLength = getLength(); if (newLength == currentLength) { return; } long currentPos = channel.position(); if (newLength newLength) { channel.position(newLength); } } else { // Expand the file by positioning the offset at the new EOF and writing a byte, and reposition the // offset to where it was channel.position(newLength-1); // Note: newLength cannot be 0 write(0); channel.position(currentPos); } } @Override public void close() throws IOException { BufferPool.releaseByteBuffer((ByteBuffer)buffer); channel.close(); } public long getOffset() throws IOException { return channel.position(); } public long getLength() throws IOException { return channel.size(); } public void seek(long offset) throws IOException { channel.position(offset); } } /** * A Permissions implementation for LocalFile. */ private static class LocalFilePermissions extends IndividualPermissionBits implements FilePermissions { private final File file; // Permissions are limited to the user access type. Executable permission flag is only available under Java 1.6 // and up. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write), but we return default values. /** Mask for supported permissions under Java 1.6 */ private static final PermissionBits MASK = new GroupedPermissionBits(448); // rwx------ (700 octal) private LocalFilePermissions(java.io.File file) { this.file = file; } public boolean getBitValue(int access, int type) { // Only the 'user' permissions are supported if (access != USER_ACCESS) { return false; } switch (type) { case READ_PERMISSION: return file.canRead(); case WRITE_PERMISSION: return file.canWrite(); case EXECUTE_PERMISSION: return file.canExecute(); } // if (type==READ_PERMISSION) { // return file.canRead(); // }else if(type==WRITE_PERMISSION) // return file.canWrite(); // // Execute permission can only be retrieved under Java 1.6 and up // else if(type==EXECUTE_PERMISSION && JavaVersion.JAVA_1_6.isCurrentOrHigher()) // return file.canExecute(); return false; } /** * Overridden for performance reasons. */ @Override public int getIntValue() { int userPerms = 0; if (getBitValue(USER_ACCESS, READ_PERMISSION)) { userPerms |= READ_PERMISSION; } if (getBitValue(USER_ACCESS, WRITE_PERMISSION)) { userPerms |= WRITE_PERMISSION; } if (getBitValue(USER_ACCESS, EXECUTE_PERMISSION)) { userPerms |= EXECUTE_PERMISSION; } return userPerms<<6; } public PermissionBits getMask() { return MASK; } } /** * Turns a {@link FilenameFilter} into a {@link java.io.FilenameFilter}. */ private static class LocalFilenameFilter implements java.io.FilenameFilter { private final FilenameFilter filter; private LocalFilenameFilter(FilenameFilter filter) { this.filter = filter; } @Override public boolean accept(File dir, String name) { return filter.accept(name); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/local/LocalProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.local; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import com.mucommander.commons.runtime.OsFamily; import java.io.IOException; /** * This class is the provider for the local filesystem implemented by {@link com.mucommander.commons.file.impl.local.LocalFile} * and network path given in UNC format which is implemented by {@link com.mucommander.commons.file.impl.local.UNCFile} * * @author Maxence Bernard, Arik Hadas * @see com.mucommander.commons.file.impl.local.LocalFile * @see com.mucommander.commons.file.impl.local.UNCFile */ public class LocalProtocolProvider implements ProtocolProvider { /** Are we running Windows ? */ private final static boolean IS_WINDOWS = OsFamily.WINDOWS.isCurrent(); public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return isUncFile(url)? (instantiationParams.length==0?new UNCFile(url):new UNCFile(url ,(java.io.File)instantiationParams[0])) :(instantiationParams.length==0?new LocalFile(url):new LocalFile(url, (java.io.File)instantiationParams[0])); } /** * Returns true if the specified {@link FileURL} denotes a Windows UNC file. * * @param fileURL the {@link FileURL} to test * @return true if the specified {@link FileURL} denotes a Windows UNC file. */ private static boolean isUncFile(FileURL fileURL) { return IS_WINDOWS && !FileURL.LOCALHOST.equals(fileURL.getHost()); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/local/SpecialWindowsLocation.java ================================================ package com.mucommander.commons.file.impl.local; import com.mucommander.commons.file.DummyFile; import com.mucommander.commons.file.FileURL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.MalformedURLException; /** * Represents special Windows locations such as 'My Computer', 'Network Neighborhood', 'Recycle Bin', ... as dummy * AbstractFile instances. * *

    This class is totally useless on platforms other than Windows. * * @author Maxence Bernard */ public class SpecialWindowsLocation extends DummyFile { private static final Logger LOGGER = LoggerFactory.getLogger(SpecialWindowsLocation.class); /** Control Panel */ public final static SpecialWindowsLocation CONTROL_PANEL = createLocation("::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\::{21EC2020-3AEA-1069-A2DD-08002B30309D}"); /** My Computer */ public final static SpecialWindowsLocation MY_COMPUTER = createLocation("::{20D04FE0-3AEA-1069-A2D8-08002B30309D}"); /** My Documents */ public final static SpecialWindowsLocation MY_DOCUMENTS = createLocation("::{450D8FBA-AD25-11D0-98A8-0800361B1103}"); /** Network Neighborhood */ public final static SpecialWindowsLocation NETWORK_NEIGHBORHOOD = createLocation("::{208D2C60-3AEA-1069-A2D7-08002B30309D}"); /** Recycle Bin */ public final static SpecialWindowsLocation RECYCLE_BIN = createLocation("::{645FF040-5081-101B-9F08-00AA002F954E}"); /** * Creates a SpecialWindowsLocation using the specified class ID and returns it. * * @param clsid the class ID * @return the new SpecialWindowsLocation */ private static SpecialWindowsLocation createLocation(String clsid) { try { return new SpecialWindowsLocation(clsid); } catch(MalformedURLException e) { LOGGER.warn("Unable to creation location {}", clsid, e); } return null; } /** A class ID */ protected String clsid; /** * Creates a new special Windows location using the given CLSID (Class identifier). * * @param clsid a Windows class identifier * @throws java.net.MalformedURLException should not happen */ public SpecialWindowsLocation(String clsid) throws MalformedURLException { super(FileURL.getFileURL("file:///")); // dummy URL, '/' corresponds to nothing under Windows this.clsid = clsid; } /** * Implementation notes: returns the CLSID (Class identifier) passed to the constructor. */ @Override public String getName() { return clsid; } /** * Implementation notes: returns the CLSID (Class identifier) passed to the constructor. */ @Override public String getAbsolutePath() { return clsid; } /** * Implementation notes: always returns true. */ @Override public boolean isDirectory() { return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/local/UNCFile.java ================================================ package com.mucommander.commons.file.impl.local; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; import java.util.StringTokenizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.GroupedPermissionBits; import com.mucommander.commons.file.IndividualPermissionBits; import com.mucommander.commons.file.PermissionBits; import com.mucommander.commons.file.ProtocolFile; import com.mucommander.commons.file.UnsupportedFileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.local.LocalFile.LocalInputStream; import com.mucommander.commons.file.impl.local.LocalFile.LocalOutputStream; import com.mucommander.commons.file.impl.local.LocalFile.LocalRandomAccessInputStream; import com.mucommander.commons.file.impl.local.LocalFile.LocalRandomAccessOutputStream; import com.mucommander.commons.file.util.Kernel32; import com.mucommander.commons.file.util.Kernel32API; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.runtime.OsVersion; import com.sun.jna.ptr.LongByReference; /** * TODO: update this documentation and LocalFile documentation * * @author Arik Hadas */ public class UNCFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(UNCFile.class); protected File file; private final FilePermissions permissions; /** * Absolute file path, free of trailing separator */ protected String absPath; /** * Caches the parent folder, initially null until getParent() gets called */ protected AbstractFile parent; /** * Indicates whether the parent folder instance has been retrieved and cached or not (parent can be null) */ protected boolean parentValueSet; /** * Underlying Windows's path separator */ public final static String SEPARATOR = "\\"; // Permissions can only be changed under Java 1.6 and up and are limited to 'user' access. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write) and as such can't be changed. /** * Changeable permissions mask for Java 1.6 and up, on Windows OS (any version) */ private static final PermissionBits CHANGEABLE_PERMISSIONS = new GroupedPermissionBits(128); // -w------- (200 octal) /** * Creates a new instance of UNCFile and a corresponding {@link File} instance. */ protected UNCFile(FileURL fileURL) throws IOException { this(fileURL, null); } /** * Creates a new instance of UNCFile, using the given {@link File} if not null, creating a new * {@link File} instance otherwise. * * @throws IOException if an I/O error occurs. */ protected UNCFile(FileURL fileURL, File file) throws IOException { super(fileURL); if (file == null) { absPath = SEPARATOR + SEPARATOR + fileURL.getHost() + fileURL.getPath().replace('/', '\\'); // Replace leading / char by \ // create the java.io.File instance and throw an exception if the path is not absolute. file = new File(absPath); if (!file.isAbsolute()) { throw new IOException(); } } // the java.io.File instance was created by ls(), no need to re-create it or call the costly File#getAbsolutePath() else { absPath = SEPARATOR + SEPARATOR + fileURL.getHost() + fileURL.getPath().replace('/', '\\'); } // Remove the trailing separator if present if (absPath.endsWith(SEPARATOR)) { absPath = absPath.substring(0, absPath.length() - 1); } this.file = file; this.permissions = new UNCFilePermissions(file); } /** * Returns a java.io.File instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } @Override public boolean isSymlink() { // At the moment symlinks under Windows (aka NTFS junction points) are not supported because java.io.File // knows nothing about them and there is no way to discriminate them. So there is no need to waste time // comparing canonical paths, just return false. // Todo: add support for .lnk files (~hard links) return false; } @Override public boolean isSystem() { return false; } @Override public long getLastModifiedDate() { return file.lastModified(); } @Override public void setLastModifiedDate(long lastModified) throws IOException { // java.io.File#setLastModified(long) throws an IllegalArgumentException if time is negative. // If specified time is negative, set it to 0 (01/01/1970). if (lastModified < 0) lastModified = 0; if (!file.setLastModified(lastModified)) throw new IOException(); } @Override public long getSize() { return file.length(); } @Override public AbstractFile getParent() { // Retrieve the parent AbstractFile instance and cache it if (!parentValueSet) { if (!isRoot()) { FileURL parentURL = getURL().getParent(); if (parentURL != null) { parent = FileFactory.getFile(parentURL); } } parentValueSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValueSet = true; } @Override public boolean exists() { return file.exists(); } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { return CHANGEABLE_PERMISSIONS; } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { // Only the 'user' permissions under Java 1.6 are supported if (access != USER_ACCESS) throw new IOException(); boolean success = false; if (permission == READ_PERMISSION) success = file.setReadable(enabled); else if (permission == WRITE_PERMISSION) success = file.setWritable(enabled); else if (permission == EXECUTE_PERMISSION) success = file.setExecutable(enabled); if (!success) throw new IOException(); } /** * Always returns null, this information is not available unfortunately. */ @Override public String getOwner() { return null; } /** * Always returns false, this information is not available unfortunately. */ @Override public boolean canGetOwner() { return false; } /** * Always returns null, this information is not available unfortunately. */ @Override public String getGroup() { return null; } /** * Always returns false, this information is not available unfortunately. */ @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.isDirectory() // if(IS_WINDOWS && guessFloppyDrive()) // return true; return file.isDirectory(); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * * @throws IOException if an I/O error occurs. */ @Override public InputStream getInputStream() throws IOException { return new LocalInputStream(new FileInputStream(file).getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * * @throws IOException if an I/O error occurs. */ @Override public OutputStream getOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, false).getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * * @throws IOException if an I/O error occurs. */ @Override public OutputStream getAppendOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, true).getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * * @throws IOException if an I/O error occurs. */ @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new LocalRandomAccessInputStream(new RandomAccessFile(file, "r").getChannel()); } /** * Implementation notes: the returned InputStream uses a NIO {@link FileChannel} under the hood to * benefit from InterruptibleChannel and allow a thread waiting for an I/O to be gracefully interrupted * using Thread#interrupt(). * * @throws IOException if an I/O error occurs. */ @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return new LocalRandomAccessOutputStream(new RandomAccessFile(file, "rw").getChannel()); } @Override public void delete() throws IOException { if (!file.delete()) { throw new IOException(); } } @Override public AbstractFile[] ls() throws IOException { return ls(null); } @Override public void mkdir() throws IOException { if (!file.mkdir()) throw new IOException(); } @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // Fail in some situations where java.io.File#renameTo() doesn't. // Note that java.io.File#renameTo()'s implementation is system-dependant, so it's always a good idea to // perform all those checks even if some are not necessary on this or that platform. checkRenamePrerequisites(destFile, true, false); // The behavior of java.io.File#renameTo() when the destination file already exists is not consistent // across platforms: // - Under UNIX, it succeeds and return true // - Under Windows, it fails and return false // This ticket goes in great details about the issue: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4017593 // // => Since this method is required to succeed when the destination file exists, the Windows platform needs // special treatment. destFile = destFile.getTopAncestor(); File destJavaIoFile = ((UNCFile) destFile).file; // This check is necessary under Windows because java.io.File#renameTo(java.io.File) does not return false // if the destination file is located on a different drive, contrary for example to Mac OS X where renameTo // returns false in this case. // Not doing this under Windows would mean files would get moved between drives with renameTo, which doesn't // allow the transfer to be monitored. // Note that Windows UNC paths are handled by checkRenamePrerequisites() when comparing hosts for equality. if (!getRoot().equals(destFile.getRoot())) throw new IOException(); // Windows 9x or Windows Me: Kernel32's MoveFileEx function is NOT available if (OsVersion.WINDOWS_ME.isCurrentOrLower()) { // The destination file is deleted before calling java.io.File#renameTo(). // Note that in this case, the atomicity of this method is not guaranteed anymore -- if // java.io.File#renameTo() fails (for whatever reason), the destination file is deleted anyway. if (destFile.exists()) if (!destJavaIoFile.delete()) throw new IOException(); } // Windows NT: Kernel32's MoveFileEx can be used, if the Kernel32 DLL is available. else if (Kernel32.isAvailable()) { // Note: MoveFileEx is always used, even if the destination file does not exist, to avoid having to // call #exists() on the destination file which has a cost. if (!Kernel32.getInstance().MoveFileEx(absPath, destFile.getAbsolutePath(), Kernel32API.MOVEFILE_REPLACE_EXISTING | Kernel32API.MOVEFILE_WRITE_THROUGH)) { String errorMessage = Integer.toString(Kernel32.getInstance().GetLastError()); // TODO: use Kernel32.FormatMessage throw new IOException("Rename using Kernel32 API failed: " + errorMessage); } else { // move successful return; } } // else fall back to java.io.File#renameTo if (!file.renameTo(destJavaIoFile)) { throw new IOException(); } } @Override public long getFreeSpace() throws IOException { return file.getUsableSpace(); } @Override public long getTotalSpace() throws IOException { return file.getTotalSpace(); } // Unsupported file operations /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } //////////////////////// // Overridden methods // /// ///////////////////// @Override public String getName() { // If this file has no parent, return: // - the drive's name under OSes with root drives such as Windows, e.g. "C:" // - "/" under Unix-based systems if (isRoot()) return absPath; return file.getName(); } @Override public String getAbsolutePath() { // Append separator for directories if (isDirectory() && !absPath.endsWith(SEPARATOR)) return absPath + SEPARATOR; return absPath; } @Override public String getCanonicalPath() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.getCanonicalPath() // if(IS_WINDOWS && guessFloppyDrive()) // return absPath; // Note: canonical path must not be cached as its resolution can change over time, for instance // if a file 'Test' is renamed to 'test' in the same folder, its canonical path would still be 'Test' // if it was resolved prior to the renaming and thus be recognized as a symbolic link try { String canonicalPath = file.getCanonicalPath(); // Append separator for directories if (isDirectory() && !canonicalPath.endsWith(SEPARATOR)) canonicalPath = canonicalPath + SEPARATOR; return canonicalPath; } catch (IOException e) { return absPath; } } @Override public String getSeparator() { return SEPARATOR; } @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { File[] files = file.listFiles(filenameFilter == null ? null : new UNCFilenameFilter(filenameFilter)); if (files == null) { throw new IOException(); } int nbFiles = files.length; AbstractFile[] children = new AbstractFile[nbFiles]; for (int i = 0; i < nbFiles; i++) { File file = files[i]; // Clone the FileURL of this file and set the child's path, this is more efficient than creating a new // FileURL instance from scratch. FileURL childURL = (FileURL) fileURL.clone(); childURL.setPath(addTrailingSeparator(fileURL.getPath()) + file.getName()); // Retrieves an AbstractFile (LocalFile or AbstractArchiveFile) instance that's potentially already in // the cache, reuse this file as the file's parent, and the already-created java.io.File instance. children[i] = FileFactory.getFile(childURL, this, file); } return children; } @Override public boolean isHidden() { return file.isHidden(); } /** * TODO */ @Override public AbstractFile getRoot() { String[] splittedBySeparator = absPath.split("\\\\"); return FileFactory.getFile(SEPARATOR + SEPARATOR + splittedBySeparator[2] + SEPARATOR + splittedBySeparator[3]); } /** * TODO */ @Override public boolean isRoot() { return countIndexOf(absPath, "\\\\") <= 3; } private int countIndexOf(String text, String search) { return text.split(search).length - 1; } /** * Overridden to return the local volume on which this file is located. The returned volume is one of the volumes * returned by {@link LocalFile#getVolumes()}. */ @Override public AbstractFile getVolume() { AbstractFile[] volumes = LocalFile.getVolumes(); // Looks for the volume that best matches this file, i.e. the volume that is the deepest parent of this file. // If this file is itself a volume, return it. int bestDepth = -1; int bestMatch = -1; int depth; String thisPath = getAbsolutePath(true); for (int i = 0; i < volumes.length; i++) { AbstractFile volume = volumes[i]; String volumePath = volume.getAbsolutePath(true); if (thisPath.equals(volumePath)) { return this; } else if (thisPath.startsWith(volumePath)) { depth = PathUtils.getDepth(volumePath, volume.getSeparator()); if (depth > bestDepth) { bestDepth = depth; bestMatch = i; } } } if (bestMatch >= 0) { return volumes[bestMatch]; } // If no volume matched this file (shouldn't normally happen), return the root folder return getRoot(); } /** * Returns the total and free space on the volume where this file resides. * *

    Using this method to retrieve both free space and volume space is more efficient than calling * {@link #getFreeSpace()} and {@link #getTotalSpace()} separately -- the underlying method retrieving both * attributes at the same time. * * @return a {totalSpace, freeSpace} long array, both values can be null if the information could not be retrieved * @throws IOException if an I/O error occurred */ public long[] getVolumeInfo() throws IOException { return new long[] { getTotalSpace(), getFreeSpace() }; } /** * Uses platform dependent functions to retrieve the total and free space on the volume where this file resides. * * @return a {totalSpace, freeSpace} long array, both values can be null if the information could not * be retrieved. * @throws IOException if an I/O error occurred */ protected long[] getNativeVolumeInfo() throws IOException { BufferedReader br = null; String absPath = getAbsolutePath(); long[] dfInfo = new long[]{-1, -1}; try { // Use the Kernel32 DLL if it is available if (Kernel32.isAvailable()) { // Retrieves the total and free space information using the GetDiskFreeSpaceEx function of the // Kernel32 API. LongByReference totalSpaceLBR = new LongByReference(); LongByReference freeSpaceLBR = new LongByReference(); if (Kernel32.getInstance().GetDiskFreeSpaceEx(absPath, null, totalSpaceLBR, freeSpaceLBR)) { dfInfo[0] = totalSpaceLBR.getValue(); dfInfo[1] = freeSpaceLBR.getValue(); } else { LOGGER.warn("Call to GetDiskFreeSpaceEx failed, absPath={}", absPath); } } // Otherwise, parse the output of 'dir "filePath"' command to retrieve free space information, if // running Window NT or higher. // Note: no command invocation under Windows 95/98/Me, because it causes a shell window to // appear briefly every time this method is called (See ticket #63). else if (OsVersion.WINDOWS_NT.isCurrentOrHigher()) { // 'dir' command returns free space on the last line Process process = Runtime.getRuntime().exec( (OsVersion.getCurrent().compareTo(OsVersion.WINDOWS_NT) >= 0 ? "cmd /c" : "command.com /c") + " dir \"" + absPath + "\""); // Check that the process was correctly started if (process != null) { br = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; String lastLine = null; // Retrieves last line of dir while ((line = br.readLine()) != null) { if (!line.trim().isEmpty()) lastLine = line; } // Last dir line may look like something this (might vary depending on system's language, below in French): // 6 Rep(s) 14 767 521 792 octets libres if (lastLine != null) { StringTokenizer st = new StringTokenizer(lastLine, " \t\n\r\f,."); // Discard first token st.nextToken(); // Concatenates as many contiguous groups of numbers String token; String freeSpace = ""; while (st.hasMoreTokens()) { token = st.nextToken(); char c = token.charAt(0); if (c >= '0' && c <= '9') { freeSpace += token; } else if (!freeSpace.isEmpty()) { break; } } dfInfo[1] = Long.parseLong(freeSpace); } } } } finally { if (br != null) { try { br.close(); } catch (IOException ignored) { } } } return dfInfo; } /** * A Permissions implementation for LocalFile. */ private static class UNCFilePermissions extends IndividualPermissionBits implements FilePermissions { private final java.io.File file; // Permissions are limited to the user access type. Executable permission flag is only available under Java 1.6 // and up. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write), but we return default values. /** * Mask for supported permissions under Java 1.6 */ private final static PermissionBits MASK = new GroupedPermissionBits(448); // rwx------ (700 octal) private UNCFilePermissions(java.io.File file) { this.file = file; } public boolean getBitValue(int access, int type) { // Only the 'user' permissions are supported if (access != USER_ACCESS) { return false; } if (type == READ_PERMISSION) { return file.canRead(); } else if (type == WRITE_PERMISSION) { return file.canWrite(); } else if (type == EXECUTE_PERMISSION) { return file.canExecute(); } return false; } /** * Overridden for performance reasons. */ @Override public int getIntValue() { int userPerms = 0; if (getBitValue(USER_ACCESS, READ_PERMISSION)) { userPerms |= READ_PERMISSION; } if (getBitValue(USER_ACCESS, WRITE_PERMISSION)) { userPerms |= WRITE_PERMISSION; } if (getBitValue(USER_ACCESS, EXECUTE_PERMISSION)) { userPerms |= EXECUTE_PERMISSION; } return userPerms << 6; } public PermissionBits getMask() { return MASK; } } /** * Turns a {@link FilenameFilter} into a {@link java.io.FilenameFilter}. */ private static class UNCFilenameFilter implements java.io.FilenameFilter { private final FilenameFilter filter; private UNCFilenameFilter(FilenameFilter filter) { this.filter = filter; } public boolean accept(File dir, String name) { return filter.accept(name); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/local/package.html ================================================ Provides an implementation of the local file system. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lst/LstArchiveEntry.java ================================================ package com.mucommander.commons.file.impl.lst; import com.mucommander.commons.file.ArchiveEntry; /** * An LST archive entry. In addition to the common attributes found in {@link ArchiveEntry}, it contains a base * folder which, when concatenated with this entry's path, gives the absolute path to the file referenced by the * LST entry. * * @author Maxence Bernard */ public class LstArchiveEntry extends ArchiveEntry { /** The base folder that when concatenated to this entry's path gives the absolute path to the file referenced * by this entry */ protected String baseFolder; LstArchiveEntry(String path, boolean directory, long date, long size, String baseFolder) { super(path, directory, date, size, true); this.baseFolder = baseFolder; } /** * Returns the base folder which, when concatenated with this entry's path, gives the absolute path to the file * referenced by the LST entry. The returned path should always end with a trailing separator character. * * @return the base folder of this entry */ protected String getBaseFolder() { return baseFolder; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lst/LstArchiveEntryIterator.java ================================================ package com.mucommander.commons.file.impl.lst; import com.mucommander.commons.file.ArchiveEntry; import com.mucommander.commons.file.ArchiveEntryIterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.text.SimpleDateFormat; import java.util.StringTokenizer; /** * An ArchiveEntryIterator that iterates through an LST archive. * * @author Maxence Bernard */ class LstArchiveEntryIterator implements ArchiveEntryIterator { private static final Logger LOGGER = LoggerFactory.getLogger(LstArchiveEntryIterator.class); /** Allows to read the LST archive line by line */ private final BufferedReader br; /** Parses LST-formatted dates */ private final SimpleDateFormat lstDateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm.ss"); /** The next entry to be returned by #nextEntry(), null if there is no more entry */ private ArchiveEntry nextEntry; /** Base folder of all entries */ private final String baseFolder; /** Current directory, used for parsing the LST file */ private String currentDir = ""; /** * Creates a new LstArchiveEntryIterator that parses the given LST InputStream. * The InputStream will be closed by {@link #close()}. * * @param in an LST archive InputStream * @throws IOException if an I/O error occurred while initializing this iterator */ LstArchiveEntryIterator(InputStream in) throws IOException { br = new BufferedReader(new InputStreamReader(in)); // Read the base folder baseFolder = br.readLine(); if(baseFolder==null) throw new IOException(); } /** * Reads the next entry and returns an {@link ArchiveEntry} representing it. * * @return an ArchiveEntry representing the entry * @throws IOException if an error occurred */ ArchiveEntry getNextEntry() throws IOException { String line = br.readLine(); if(line==null) return null; try { StringTokenizer st = new StringTokenizer(line, "\t"); String name = st.nextToken().replace('\\', '/'); long size = Long.parseLong(st.nextToken()); long date = lstDateFormat.parse((st.nextToken()+" "+st.nextToken())).getTime(); String path; boolean isDirectory; if(name.endsWith("/")) { isDirectory = true; currentDir = name; path = currentDir; } else { isDirectory = false; path = currentDir+name; } return new LstArchiveEntry(path, isDirectory, date, size, baseFolder); } catch(Exception e) { // Catches exceptions thrown by StringTokenizer and SimpleDateFormat LOGGER.info("Exception caught while parsing LST file", e); throw new IOException(); } } ///////////////////////////////////////// // ArchiveEntryIterator implementation // ///////////////////////////////////////// public ArchiveEntry nextEntry() throws IOException { // Return the next entry, if any return getNextEntry(); } public void close() throws IOException { br.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lst/LstArchiveFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.lst; import com.mucommander.commons.file.*; import java.io.IOException; import java.io.InputStream; /** * IsoArchiveFile provides read-only access to archives in the LST format made popular by Total Commander. * *

    Entries are parsed from the .lst file and can be read (an InputStream to them can be opened) if the file exists * locally. * *

    For reference, here's a short LST file: *

     * c:\
     * cygwin\	0	2006.10.2	19:35.2
     * cygwin.bat	57	2006.10.2	19:34.58
     * cygwin.ico	7022	2006.10.2	19:40.52
     * cygwin\bin\	0	2006.10.2	19:40.52
     * addftinfo.exe	67072	2002.12.16	10:3.24
     * afmtodit	8544	2002.12.16	10:3.22
     * apropos	1786	2005.5.4	2:12.50
     * ascii.exe	7168	2006.3.20	20:44.24
     * ash.exe	74240	2004.1.27	2:14.20
     * awk.exe	19	2006.10.2	19:34.4
     * 
    * * @see com.mucommander.commons.file.impl.lst.LstFormatProvider * @author Maxence Bernard */ public class LstArchiveFile extends AbstractROArchiveFile { LstArchiveFile(AbstractFile file) { super(file); } @Override public ArchiveEntryIterator getEntryIterator() throws IOException { return new LstArchiveEntryIterator(getInputStream()); } @Override public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException { // Will throw an IOException if the file designated by the entry doesn't exist return FileFactory.getFile(((LstArchiveEntry)entry).getBaseFolder()+entry.getPath(), true).getInputStream(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lst/LstFormatProvider.java ================================================ package com.mucommander.commons.file.impl.lst; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import java.io.IOException; /** * This class is the provider for the 'Lst' archive format implemented by {@link LstArchiveFile}. * * @see com.mucommander.commons.file.impl.lst.LstArchiveFile * @author Nicolas Rinaudo, Maxence Bernard */ public class LstFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".lst"}; /** * Static instance of the filename filter that matches archive filenames */ private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new LstArchiveFile(file); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lst/package.html ================================================ Provides an implementation of the LST archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lzh/LzhFormatProvider.java ================================================ package com.mucommander.commons.file.impl.lzh; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class LzhFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".lzh", ".lha" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); // private final static byte[] SIGNATURE = { 0x2D, 0x6C, 0x68 };//=google but sevenzipjbinding examples 24FB2D private final static byte[] SIGNATURE = { }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.LZH, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/lzma/LzmaFormatProvider.java ================================================ package com.mucommander.commons.file.impl.lzma; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class LzmaFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".lzma" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {}; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.LZMA, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/nfs/NFSFile.java ================================================ package com.mucommander.commons.file.impl.nfs; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.sun.xfile.XFile; import com.sun.xfile.XFileInputStream; import com.sun.xfile.XFileOutputStream; import com.sun.xfile.XRandomAccessFile; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * NFSFile provides access to files located on an NFS/WebNFS server. * *

    The associated {@link FileURL} scheme is {@link FileProtocols#NFS}. The host part of the URL designates the * NFS server. The path separator is '/'. * *

    Here are a few examples of valid NFS URLs: * * nfs://garfield/stuff/
    * nfs://192.168.1.1:2049/stuff/somefile
    *
    * *

    Access to NFS files is provided by the Yanfs library (formerly WebNFS) distributed under the BSD * license. The {@link #getUnderlyingFileObject()} method allows to retrieve a com.sun.xfile.XFile instance * corresponding to this NFSFile. * * @author Maxence Bernard */ public class NFSFile extends ProtocolFile { /** Underlying file instance */ private final XFile file; private String absPath; private final FilePermissions permissions; /** Caches the parent folder, initially null until getParent() gets called */ private AbstractFile parent; /** Indicates whether the parent folder instance has been retrieved and cached or not (parent can be null) */ private boolean parentValueSet; public final static String SEPARATOR = "/"; /** Name of the NFS version property */ public final static String NFS_VERSION_PROPERTY_NAME = "version"; /** NFS version 2 */ public final static String NFS_VERSION_2 = "v2"; /** NFS version 3 */ public final static String NFS_VERSION_3 = "v3"; /** Default NFS version */ public final static String DEFAULT_NFS_VERSION = NFS_VERSION_2; /** Name of the NFS transport protocol property */ public final static String NFS_PROTOCOL_PROPERTY_NAME = "protocol"; /** 'Auto' transport protocol: TCP is tried first and if the connection cannot be established, falls back to UDP */ public final static String NFS_PROTOCOL_AUTO = "Auto"; /** TCP transport protocol */ public final static String NFS_PROTOCOL_TCP = "TCP"; /** UDP transport protocol */ public final static String NFS_PROTOCOL_UDP = "UDP"; /** Default transport protocol */ public final static String DEFAULT_NFS_PROTOCOL = NFS_PROTOCOL_AUTO; /** * Creates a new instance of NFSFile. * * @param fileURL fiel url */ NFSFile(FileURL fileURL) { super(fileURL); // create the NFS URL used by XFile. // The general syntax for NFS URLs is : nfs://:, as specified by RFC 2054 // Additionally, XFile allows some special flags to be used in the port part of the URL to specify connection // properties. Those flags must be placed after the port, and before the colon character delimiting the end of // the port part. // Here's the list of allowed flags (quoted from com.sun.nfs.NfsURL): // vn - NFS version, e.g. "v3" // u - Force UDP - normally TCP is preferred // t - Force TDP - don't fall back to UDP // m - Force Mount protocol. Normally public filehandle is preferred // // Example: nfs://server:123v2um/path : use port 123 with NFS v2 over UDP and Mount protocol // // The 'm' flag must be specified, otherwise regular NFS shares (i.e. non WebNFS-enabled ones) that don't // specify a public filehandle will fail. However, using this flag has two unfortunate consequences: // - the NFS version fails to be properly negotiated as it normally does (try v3 then fall back on v2): the // NFS version must be specified in the URL. // - an extra slash character must be added before the path part, otherwise it is considered as relative to // the public filehandle and will thus fail to resolve. // // These issues might get fixed in Yanfs someday. When that happens, this code might be simplified. // Determines the NFS version (v2 or v3) to be used, based on the version property String nfsVersion = fileURL.getProperty(NFS_VERSION_PROPERTY_NAME); if (nfsVersion == null) { nfsVersion = DEFAULT_NFS_VERSION; } // Determines the NFS transport protocol (Auto, TCP or UDP) to be used, based on the protocol property String nfsProtocol = fileURL.getProperty(NFS_PROTOCOL_PROPERTY_NAME); nfsProtocol = NFS_PROTOCOL_TCP.equals(nfsProtocol)?"t":NFS_PROTOCOL_UDP.equals(nfsProtocol)?"u":""; // Omit port part if none is contained in the FileURL or if it is 2049 int port = fileURL.getPort(); String portString = port < 0 || port == 2049 ? "" : ""+port; // create the XFile instance with the weird NFS url this.file = new XFile("nfs://"+fileURL.getHost()+":"+portString+nfsVersion+nfsProtocol+"m"+"/"+fileURL.getPath()); // Retrieve the absolute path from the FileURL and NOT from the XFile instance which will return those weird flags this.absPath = fileURL.toString(); // Remove trailing separator (if any) this.absPath = absPath.endsWith(SEPARATOR)?absPath.substring(0,absPath.length()-1):absPath; this.permissions = new NFSFilePermissions(file); } @Override public long getLastModifiedDate() { return file.lastModified(); } /** * Implementation notes: always throws {@link UnsupportedFileOperationException}. * * @throws UnsupportedFileOperationException always. */ @Override @UnsupportedFileOperation public void setLastModifiedDate(long lastModified) throws UnsupportedFileOperationException { // XFile has no method for that purpose throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } @Override public long getSize() { return file.length(); } @Override public AbstractFile getParent() { // Retrieve parent AbstractFile and cache it if (!parentValueSet) { FileURL parentURL = getURL().getParent(); if (parentURL != null) { parent = FileFactory.getFile(parentURL); // Note: parent may be null if it can't be resolved } parentValueSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValueSet = true; } @Override public boolean exists() { return file.exists(); } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { // no permission can be changed return PermissionBits.EMPTY_PERMISSION_BITS; } @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException { // XFile has no method for that unfortunately throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } /** * Always returns null, this information is not available unfortunately. */ @Override public String getOwner() { return null; } /** * Always returns false, this information is not available unfortunately. */ @Override public boolean canGetOwner() { return false; } /** * Always returns null, this information is not available unfortunately. */ @Override public String getGroup() { return null; } /** * Always returns false, this information is not available unfortunately. */ @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { return file.isDirectory(); } /** * Always returns false (symlinks are not detected). */ @Override public boolean isSymlink() { // Yanfs is unable to detect symlinks at this time return false; } @Override public boolean isSystem() { return false; } @Override public AbstractFile[] ls() throws IOException { return ls(null); } @Override public void mkdir() throws IOException { if(!new XFile(absPath).mkdir()) throw new IOException(); } @Override public InputStream getInputStream() throws IOException { return new XFileInputStream(file); } @Override public OutputStream getOutputStream() throws IOException { return new XFileOutputStream(absPath, false); } @Override public OutputStream getAppendOutputStream() throws IOException { return new XFileOutputStream(absPath, true); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new NFSRandomAccessInputStream(new XRandomAccessFile(file, "r")); } /** * Warning: the returned {@link com.mucommander.commons.file.impl.nfs.NFSFile.NFSRandomAccessOutputStream} instance * is not fully functional, its {@link com.mucommander.commons.file.impl.nfs.NFSFile.NFSRandomAccessOutputStream#setLength(long)} * method has a limitation. * * @return a RandomAccessOutputStream that is not fully functional * @throws IOException if the file could not be opened for random write access */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return new NFSRandomAccessOutputStream(new XRandomAccessFile(file, "rw")); } @Override public void delete() throws IOException { boolean ret = file.delete(); if (!ret) { throw new IOException(); } } /** * Implementation notes: server-to-server renaming will work if the destination file also uses the 'NFS' scheme * and is located on the same host. */ @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination checkRenamePrerequisites(destFile, true, false); // Rename file if (!file.renameTo(((NFSFile)destFile).file)) { throw new IOException(); } } /** * Returns a com.sun.xfile.XFile instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } // Unsupported file operations /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { // XFile has no method to provide that information throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { // XFile has no method to provide that information throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { String[] names = file.list(); if (names == null) { throw new IOException(); } if (filenameFilter != null) { names = filenameFilter.filter(names); } AbstractFile[] children = new AbstractFile[names.length]; FileURL childURL; String baseURLPath = fileURL.getPath(); if (!baseURLPath.endsWith("/")) { baseURLPath += SEPARATOR; } for (int i = 0; i < names.length; i++) { // Clone this file's URL with the connection properties and set the child file's path childURL = (FileURL)fileURL.clone(); childURL.setPath(baseURLPath+names[i]); // create the child NFSFile using this file as a parent children[i] = FileFactory.getFile(childURL, this); } return children; } /** * NFSRandomAccessInputStream extends RandomAccessInputStream to provide random read access to an NFSFile. */ public static class NFSRandomAccessInputStream extends RandomAccessInputStream { private final XRandomAccessFile raf; NFSRandomAccessInputStream(XRandomAccessFile raf) { this.raf = raf; } @Override public int read() throws IOException { return raf.read(); } @Override public int read(byte[] b, int off, int len) throws IOException { return raf.read(b, off, len); } @Override public void close() throws IOException { raf.close(); } public long getOffset() throws IOException { return raf.getFilePointer(); } public long getLength() throws IOException { return raf.length(); } public void seek(long offset) throws IOException { raf.seek(offset); } } /** * NFSRandomAccessOutputStream extends RandomAccessOutputStream to provide random write access to an NFSFile. * *

    Warning: this RandomAccessOutputStream is not fully functional, the {@link #setLength(long)} has a * limitation. */ public static class NFSRandomAccessOutputStream extends RandomAccessOutputStream { private final XRandomAccessFile raf; NFSRandomAccessOutputStream(XRandomAccessFile raf) { this.raf = raf; } @Override public void write(int i) throws IOException { raf.write(i); } @Override public void write(byte[] b) throws IOException { raf.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { raf.write(b, off, len); } @Override public void close() throws IOException { raf.close(); } public long getOffset() throws IOException { return raf.getFilePointer(); } public long getLength() throws IOException { return raf.length(); } public void seek(long offset) throws IOException { raf.seek(offset); } /** * Warning: this method is only capable of expanding the file, not truncating it. * It will throw an IOException whenever the newLength parameter is greater than * the current length reported by {@link #getLength()}. * * @param newLength the new file's length * @throws IOException If an I/O error occurred while trying to change the file's length */ @Override public void setLength(long newLength) throws IOException { // This operation is supported only if the new length is greater (or equal) than the current length long currentLength = getLength(); if (newLength < currentLength) { throw new IOException(); } if (newLength == currentLength) { return; } // Extend the file's length by seeking to the end and writing a byte seek(newLength-1); write(0); } } /** * A Permissions implementation for NFSFile. */ private static class NFSFilePermissions extends IndividualPermissionBits implements FilePermissions { private final XFile file; private final static PermissionBits MASK = new GroupedPermissionBits(384); // rw------- (300 octal) NFSFilePermissions(XFile file) { this.file = file; } public boolean getBitValue(int access, int type) { if (access != USER_ACCESS) { return false; } if (type == READ_PERMISSION) { return file.canRead(); } else if(type == WRITE_PERMISSION) { return file.canWrite(); } return false; } public PermissionBits getMask() { return MASK; } } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/nfs/NFSProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.nfs; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import java.io.IOException; /** * This class is the provider for the NFS filesystem implemented by {@link com.mucommander.commons.file.impl.nfs.NFSFile}. * * @author Nicolas Rinaudo * @see com.mucommander.commons.file.impl.nfs.NFSFile */ public class NFSProtocolProvider implements ProtocolProvider { public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return new NFSFile(url); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/nfs/package.html ================================================ Provides an implementation of the NFS protocol. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/rar/RarFormatProvider.java ================================================ package com.mucommander.commons.file.impl.rar; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.SevenZipArchiveFormatDetector; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import com.mucommander.commons.file.impl.sevenzip.SevenZipArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; import java.io.IOException; /** * This class is the provider for the 'Rar' archive format implemented by {@link SevenZipArchiveFile}. * * @author Arik Hadas */ public class RarFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".rar", ".cbr"}; private final static byte[] RAR4_SIGNATURE = {0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00}; private final static byte[] RAR5_SIGNATURE = {0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00}; /** * Static instance of the filename filter that matches archive filenames */ private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private static final SevenZipArchiveFormatDetector detector = new SevenZipArchiveFormatDetector(RAR5_SIGNATURE.length) { @Override protected ArchiveFormat detect(byte[] bytes) { if (checkSignature(bytes, RAR4_SIGNATURE)) { return ArchiveFormat.RAR; } else if (checkSignature(bytes, RAR5_SIGNATURE)) { return ArchiveFormat.RAR5; } return null; } }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, detector); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/rpm/RpmFormatProvider.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.rpm; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; import java.io.IOException; public class RpmFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".rpm" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {(byte)0xED, (byte)0xAB, (byte)0xEE, (byte)0xDB, 0x03}; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.RPM, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/s3/S3Bucket.java ================================================ package com.mucommander.commons.file.impl.s3; import com.mucommander.commons.file.*; import com.mucommander.commons.io.RandomAccessInputStream; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * S3Bucket represents an Amazon S3 bucket. * * @author Maxence Bernard */ public class S3Bucket extends S3File { private static final Logger LOGGER = LoggerFactory.getLogger(S3File.class); private final String bucketName; private final S3BucketFileAttributes atts; // TODO: add support for ACL ? (would cost an extra request per bucket) /** Default permissions for S3 buckets */ private final static FilePermissions DEFAULT_PERMISSIONS = new SimpleFilePermissions(448); // rwx------ protected S3Bucket(FileURL url, S3Service service, String bucketName) throws AuthException { super(url, service); this.bucketName = bucketName; atts = new S3BucketFileAttributes(); } protected S3Bucket(FileURL url, S3Service service, org.jets3t.service.model.S3Bucket bucket) { super(url, service); this.bucketName = bucket.getName(); atts = new S3BucketFileAttributes(bucket); } @Override public FileAttributes getFileAttributes() { return atts; } @Override public String getOwner() { return atts.getOwner(); } @Override public boolean canGetOwner() { return true; } @Override public AbstractFile[] ls() throws IOException { return listObjects(bucketName, "", this); } @Override public void delete() throws IOException { try { service.deleteBucket(bucketName); } catch(S3ServiceException e) { throw getIOException(e); } } @Override public void mkdir() throws IOException { try { service.createBucket(bucketName); } catch(S3ServiceException e) { throw getIOException(e); } } // Unsupported operations @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } @Override @UnsupportedFileOperation public void renameTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RENAME); } /** * Always throws an {@link UnsupportedFileOperationException}. */ @Override @UnsupportedFileOperation public InputStream getInputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.READ_FILE); } /** * Always throws an {@link UnsupportedFileOperationException}. */ @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE); } /** * Always throws an {@link UnsupportedFileOperationException}. */ @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_READ_FILE); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } /** * S3BucketFileAttributes provides getters and setters for S3 bucket attributes. By extending * SyncedFileAttributes, this class caches attributes for a certain amount of time * after which fresh values are retrieved from the server. * * @author Maxence Bernard */ private class S3BucketFileAttributes extends SyncedFileAttributes { private final static int TTL = 60000; private S3BucketFileAttributes() throws AuthException { super(TTL, false); // no initial update fetchAttributes(); // throws AuthException if no or bad credentials updateExpirationDate(); // declare the attributes as 'fresh' } private S3BucketFileAttributes(org.jets3t.service.model.S3Bucket bucket) { super(TTL, false); // no initial update setAttributes(bucket); setExists(true); updateExpirationDate(); // declare the attributes as 'fresh' } private void setAttributes(org.jets3t.service.model.S3Bucket bucket) { setDirectory(true); setDate(bucket.getCreationDate().getTime()); setPermissions(DEFAULT_PERMISSIONS); setOwner(bucket.getOwner().getDisplayName()); } private void fetchAttributes() throws AuthException { org.jets3t.service.model.S3Bucket bucket; S3ServiceException e = null; try { // Note: unlike getObjectDetails, getBucket returns null when the bucket does not exist // (that is because the corresponding request is a GET on the root resource, not a HEAD on the bucket). bucket = service.getBucket(bucketName); } catch(S3ServiceException ex) { e = ex; bucket = null; } if (bucket!=null) { // Bucket exists setExists(true); setAttributes(bucket); } else { // Bucket doesn't exist on the server, or could not be retrieved setExists(false); setDirectory(false); setDate(0); setPermissions(FilePermissions.EMPTY_FILE_PERMISSIONS); setOwner(null); if (e != null) { handleAuthException(e, fileURL); } } } @Override public void updateAttributes() { try { fetchAttributes(); } catch(Exception e) { // AuthException LOGGER.info("Failed to update attributes", e); } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/s3/S3File.java ================================================ package com.mucommander.commons.file.impl.s3; import com.mucommander.commons.file.*; import com.mucommander.commons.io.RandomAccessOutputStream; import org.jets3t.service.Constants; import org.jets3t.service.S3ObjectsChunk; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import java.io.IOException; import java.io.OutputStream; import java.util.Date; /** * Super class of {@link S3Root}, {@link S3Bucket} and {@link S3Object}. * * @author Maxence Bernard */ public abstract class S3File extends ProtocolFile { protected org.jets3t.service.S3Service service; protected AbstractFile parent; protected boolean parentSet; protected S3File(FileURL url, S3Service service) { super(url); this.service = service; } protected IOException getIOException(S3ServiceException e) throws IOException { return getIOException(e, fileURL); } protected static IOException getIOException(S3ServiceException e, FileURL fileURL) throws IOException { handleAuthException(e, fileURL); Throwable cause = e.getCause(); if (cause instanceof IOException) return (IOException) cause; return new IOException(e); } protected static void handleAuthException(S3ServiceException e, FileURL fileURL) throws AuthException { int code = e.getResponseCode(); if (code == 401 || code == 403) throw new AuthException(fileURL); } protected AbstractFile[] listObjects(String bucketName, String prefix, S3File parent) throws IOException { try { S3ObjectsChunk chunk = service.listObjectsChunked(bucketName, prefix, "/", Constants.DEFAULT_OBJECT_LIST_CHUNK_SIZE, null, true); org.jets3t.service.model.S3Object[] objects = chunk.getObjects(); String[] commonPrefixes = chunk.getCommonPrefixes(); if (objects.length == 0 && !prefix.isEmpty()) { // This happens only when the directory does not exist throw new IOException(); } AbstractFile[] children = new AbstractFile[objects.length + commonPrefixes.length]; FileURL childURL; int i = 0; String objectKey; for (org.jets3t.service.model.S3Object object : objects) { // Discard the object corresponding to the prefix itself objectKey = object.getKey(); if (objectKey.equals(prefix)) continue; childURL = (FileURL) fileURL.clone(); childURL.setPath(bucketName + "/" + objectKey); children[i] = FileFactory.getFile(childURL, parent, service, object); i++; } org.jets3t.service.model.S3Object directoryObject; for (String commonPrefix : commonPrefixes) { childURL = (FileURL) fileURL.clone(); childURL.setPath(bucketName + "/" + commonPrefix); directoryObject = new org.jets3t.service.model.S3Object(commonPrefix); // Common prefixes are not objects per se, and therefore do not have a date, content-length nor owner. directoryObject.setLastModifiedDate(new Date(System.currentTimeMillis())); directoryObject.setContentLength(0); children[i] = FileFactory.getFile(childURL, parent, service, directoryObject); i++; } // Trim the array if an object was discarded. // Note: Having to recreate an array sucks (puts pressure on the GC), but I haven't found a reliable way // to know in advance whether the prefix will appear in the results or not. if (i < children.length) { AbstractFile[] childrenTrimmed = new AbstractFile[i]; System.arraycopy(children, 0, childrenTrimmed, 0, i); return childrenTrimmed; } return children; } catch (S3ServiceException e) { throw getIOException(e); } } public abstract FileAttributes getFileAttributes(); @Override public AbstractFile getParent() { if (!parentSet) { FileURL parentFileURL = this.fileURL.getParent(); if (parentFileURL != null) { try { parent = FileFactory.getFile(parentFileURL, null, service); } catch (IOException e) { // No parent } } parentSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentSet = true; } // Delegates to FileAttributes @Override public long getLastModifiedDate() { return getFileAttributes().getLastModifiedDate(); } @Override public long getSize() { return getFileAttributes().getSize(); } @Override public boolean exists() { return getFileAttributes().exists(); } @Override public boolean isDirectory() { return getFileAttributes().isDirectory(); } @Override public FilePermissions getPermissions() { return getFileAttributes().getPermissions(); } @Override public Object getUnderlyingFileObject() { return getFileAttributes(); } // Unsupported operations, no matter the kind of resource (object, bucket, service) @Override public boolean isSymlink() { return false; } @Override public boolean isSystem() { return false; } @Override public PermissionBits getChangeablePermissions() { return PermissionBits.EMPTY_PERMISSION_BITS; } @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_PERMISSION); } @Override public String getGroup() { return null; } @Override public boolean canGetGroup() { return false; } @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); } @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } @Override @UnsupportedFileOperation public void setLastModifiedDate(long lastModified) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_DATE); } @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws IOException { throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/s3/S3Object.java ================================================ package com.mucommander.commons.file.impl.s3; import com.mucommander.commons.file.*; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.StreamUtils; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import org.jets3t.service.model.S3Owner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * S3Object represents an Amazon S3 object. * * @author Maxence Bernard */ public class S3Object extends S3File { private static final Logger LOGGER = LoggerFactory.getLogger(S3Object.class); private final String bucketName; private final S3ObjectFileAttributes atts; /** Maximum size of an S3 object (5GB) */ private final static long MAX_OBJECT_SIZE = 5368709120l; // TODO: add support for ACL ? (would cost an extra request per object) /** Default permissions for S3 objects */ private final static FilePermissions DEFAULT_PERMISSIONS = new SimpleFilePermissions(384); // rw------- protected S3Object(FileURL url, S3Service service, String bucketName) throws AuthException { super(url, service); this.bucketName = bucketName; atts = new S3ObjectFileAttributes(); } protected S3Object(FileURL url, S3Service service, String bucketName, org.jets3t.service.model.S3Object object) throws AuthException { super(url, service); this.bucketName = bucketName; atts = new S3ObjectFileAttributes(object); } private String getObjectKey() { String urlPath = fileURL.getPath(); // Strip out the bucket name from the path return urlPath.substring(bucketName.length()+2); } private String getObjectKey(boolean wantTrailingSeparator) { String objectKey = getObjectKey(); return wantTrailingSeparator?addTrailingSeparator(objectKey):removeTrailingSeparator(objectKey); } /** * Uploads the object contained in the given input stream to S3 by performing a 'PUT Object' request. * The input stream is always closed, whether the operation failed or succeeded. * * @param in the stream that contains the object to be uploaded * @param objectLength length of the object * @throws FileTransferException if an error occurred during the transfer */ private void putObject(InputStream in, long objectLength) throws FileTransferException { try { // Init S3 object org.jets3t.service.model.S3Object object = new org.jets3t.service.model.S3Object(getObjectKey(false)); object.setDataInputStream(in); object.setContentLength(objectLength); // Transfer to S3 and update local file attributes atts.setAttributes(service.putObject(bucketName, object)); atts.setExists(true); atts.updateExpirationDate(); } catch(S3ServiceException e) { throw new FileTransferException(FileTransferException.UNKNOWN_REASON); } finally { // Close the InputStream, no matter what try { in.close(); } catch(IOException e) { // Do not re-throw the exception to prevent exceptions caught in the catch block from being replaced } } } @Override public FileAttributes getFileAttributes() { return atts; } @Override public String getOwner() { return atts.getOwner(); } @Override public boolean canGetOwner() { return true; } @Override public AbstractFile[] ls() throws IOException { return listObjects(bucketName, getObjectKey(true), this); } @Override public void mkdir() throws IOException { if(exists()) throw new IOException(); try { atts.setAttributes(service.putObject(bucketName, new org.jets3t.service.model.S3Object(getObjectKey(true)))); atts.setExists(true); atts.updateExpirationDate(); } catch(S3ServiceException e) { throw getIOException(e); } } @Override public void delete() throws IOException { // Note: DELETE on a non-existing resource is a successful request, so we need this check if (!exists()) { throw new IOException(); } try { // Make sure that the directory is empty, abort if not. // Note that we must not count the parent directory (this file). boolean isDirectory = isDirectory(); if(isDirectory && service.listObjectsChunked(bucketName, getObjectKey(true), "/", 2, null, false).getObjects().length>1) throw new IOException("Directory not empty"); service.deleteObject(bucketName, getObjectKey(isDirectory)); // Update file attributes locally atts.setExists(false); atts.setDirectory(false); atts.setSize(0); } catch(S3ServiceException e) { throw getIOException(e); } } @Override public void renameTo(AbstractFile destFile) throws IOException { copyTo(destFile); delete(); } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { checkCopyRemotelyPrerequisites(destFile, true, false); S3Object destObjectFile = destFile.getAncestor(S3Object.class); try { // Let the COPY request fail if both objects are not located in the same region, saves 2 HEAD BUCKET requests. // // Ensure that both objects' bucket are located in the same region (null means US standard) // String sourceBucketLocation = service.getBucket(bucketName).getLocation(); // String destBucketLocation = destObjectFile.service.getBucket(destObjectFile.bucketName).getLocation(); // if((sourceBucketLocation==null && destBucketLocation!=null) // || (sourceBucketLocation!=null && destBucketLocation==null) // || !(sourceBucketLocation!=null && !sourceBucketLocation.equals(destBucketLocation)) // || !destBucketLocation.equals(destBucketLocation)) // throw new IOException(); boolean isDirectory = isDirectory(); org.jets3t.service.model.S3Object destObject = new org.jets3t.service.model.S3Object(destObjectFile.getObjectKey(isDirectory)); destObject.addAllMetadata( service.copyObject(bucketName, getObjectKey(isDirectory), destObjectFile.bucketName, destObject, false) ); // Update destination file attributes destObjectFile.atts.setAttributes(destObject); destObjectFile.atts.setExists(true); } catch(S3ServiceException e) { throw getIOException(e); } } @Override public InputStream getInputStream() throws IOException { return getInputStream(0); } @Override public InputStream getInputStream(long offset) throws IOException { try { // Note: do *not* use S3ObjectRandomAccessInputStream if the object is to be read sequentially, as it would // add unnecessary billing overhead since it reads the object chunk by chunk, each in a separate GET request. return service.getObject(bucketName, getObjectKey(false), null, null, null, null, offset==0?null:offset, null).getDataInputStream(); } catch(S3ServiceException e) { throw getIOException(e); } } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { if(!exists()) throw new IOException(); return new S3ObjectRandomAccessInputStream(); } @Override @UnsupportedFileOperation public OutputStream getOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.WRITE_FILE); // This stream is broken: close has no way to know if the transfer went through entirely. If it didn't // (close was called before the end of the input stream), the partially copied file will still be transferred // to S3, when it shouldn't. // final AbstractFile tempFile = FileFactory.getTemporaryFile(false); // final OutputStream tempOut = tempFile.getOutputStream(); // // // Update local attributes temporarily // atts.setExists(true); // atts.setSize(0); // atts.setDirectory(false); // // // Return an OutputStream to a temporary file that will be copied to the S3 object when the stream is closed. // // The object's length has to be declared in the PUT request's headers and this is the only way to do so. // return new FilteredOutputStream(tempOut) { // @Override // public void close() throws IOException { // tempOut.close(); // // InputStream tempIn = tempFile.getInputStream(); // try { // long tempFileSize = tempFile.getSize(); // // org.jets3t.service.model.S3Object object = new org.jets3t.service.model.S3Object(getObjectKey(false)); // object.setDataInputStream(tempIn); // object.setContentLength(tempFileSize); // // // Transfer to S3 and update local file attributes // atts.setAttributes(service.putObject(bucketName, object)); // atts.setExists(true); // atts.updateExpirationDate(); // } // catch(S3ServiceException e) { // throw getIOException(e); // } // finally { // try { // tempIn.close(); // } // catch(IOException e) { // // Do not re-throw the exception to prevent exceptions caught in the catch block from being replaced // } // // try { // tempFile.delete(); // } // catch(IOException e) { // // Do not re-throw the exception to prevent exceptions caught in the catch block from being replaced // } // } // } // }; } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } @Override public void copyStream(InputStream in, boolean append, long length) throws FileTransferException { if (append) { // throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); throw new FileTransferException(FileTransferException.READING_SOURCE); } // TODO: compute md5 ? // If the length is known, we can upload the object directly without having to go through the tedious process // of copying the stream to a temporary file. if (length>=0) { putObject(in, length); } else { // Copy the stream to a temporary file so that we can know the object's length, which has to be declared // in the PUT request's headers (that is before the transfer is started). final AbstractFile tempFile; final OutputStream tempOut; try { tempFile = FileFactory.getTemporaryFile(false); tempOut = tempFile.getOutputStream(); } catch (IOException e) { throw new FileTransferException(FileTransferException.OPENING_DESTINATION); } try { // Copy the stream to the temporary file try { StreamUtils.copyStream(in, tempOut, IO_BUFFER_SIZE); } finally { // Close the stream even if copyStream() threw an IOException try { tempOut.close(); } catch(IOException e) { // Do not re-throw the exception to prevent swallowing the exception thrown in the try block } } InputStream tempIn; try { tempIn = tempFile.getInputStream(); } catch(IOException e) { throw new FileTransferException(FileTransferException.OPENING_SOURCE); } putObject(tempIn, tempFile.getSize()); } finally { // Delete the temporary file, no matter what. try { tempFile.delete(); } catch(IOException e) { // Do not re-throw the exception to prevent exceptions caught in the catch block from being replaced } } } } /////////////////// // Inner classes // /////////////////// /** * Provides random read access to an S3 object by using GET Range requests with a start offset and no end. * The connection is closed and a new one opened when seeking is required. * *

    * Note: At the time of this writing, a GET request on Amazon S3 costs the equivalent of 6666 bytes of data * transferred ($0.01 per 10,000 GET requests, $0.15 per GB transferred). If the object is being read and a seek is * requested to an offset that is less than 6666 bytes away from the current position going forward, the bytes * separating the current position to the new one are skipped (read and discarded), instead of closing the current * stream and opening a new one (which would cost 1 GET request). Doing so is cheaper (in $$$) and probably faster. */ private class S3ObjectRandomAccessInputStream extends RandomAccessInputStream { /** Length of the S3 object */ private final long length; /** Current offset in the object stream */ private long offset; /** Current object stream */ private InputStream in; /** If the object is being read and a seek is requested to an offset that is less than this amount of bytes away * from the current position going forward, the bytes separating the current position to the new one are * skipped. */ private final static int SKIP_BYTE_SIZE = 6666; protected S3ObjectRandomAccessInputStream() { length = getSize(); } /** * Opens an input stream allowing to read the object and starting at the given offset. The current input stream * (if any) is closed. * * @param offset position at which to start reading the object * @throws IOException on error */ private synchronized void openStream(long offset) throws IOException { // Nothing to do if the requested offset is the current offset if (in != null && this.offset == offset) { return; } // If there is an open connection and the offset to reach is located between the current offset and // SKIP_SIZE, move to the said offset by skipping the difference instead of closing the connection and // opening a new one: if (in != null && offset > this.offset && offset - this.offset < SKIP_BYTE_SIZE) { byte[] skipBuf = BufferPool.getByteArray(SKIP_BYTE_SIZE); // Use a constant buffer size to always reuse the same instance try { StreamUtils.readFully(in, skipBuf, 0, (int)(offset-this.offset)); this.offset = offset; } finally { BufferPool.releaseByteArray(skipBuf); } } // If not, close the current connection else { if (in != null) { try { in.close(); } catch(IOException e) { // Report the error but don't throw the exception LOGGER.info("Failed to close connection", e); } } try { this.in = service.getObject(bucketName, getObjectKey(false), null, null, null, null, offset, null) .getDataInputStream(); this.offset = offset; } catch(S3ServiceException e) { throw getIOException(e); } } } @Override public synchronized int read(byte[] b, int off, int len) throws IOException { if (in == null) { openStream(0); } int nbRead = in.read(b, off, len); if (nbRead > 0) { offset += nbRead; } return nbRead; } @Override public synchronized int read() throws IOException { if (in == null) { openStream(0); } int i = in.read(); if (i != -1) { offset++; } return i; } public long getLength() { return length; } public synchronized long getOffset() { return offset; } public synchronized void seek(long offset) throws IOException { openStream(offset); } @Override public synchronized void close() throws IOException { if (in != null) { try { in.close(); // Let the IOException be thrown } finally { // Further attempts to close the stream will be no-ops in = null; offset = 0; } } } } // /** // * Reads an S3 object block by block. Each block is read by issuing a GET request with a specified Range. // * // *

    Note: A GET request on Amazon S3 costs the equivalent of 6KB of data transferred. Setting the block size too // * low will cause extra requests to be performed. Setting it too high will cause extra data to be transferred. // */ // private class S3ObjectRandomAccessInputStream extends BlockRandomInputStream { // // /** Amount of data returned by each 'GET Object' request */ // private final static int BLOCK_SIZE = 8192; // // /** Length of the S3 object */ // private long length; // // protected S3ObjectRandomAccessInputStream() { // super(BLOCK_SIZE); // // length = getSize(); // } // // // /////////////////////////////////////////// // // BlockRandomInputStream implementation // // /////////////////////////////////////////// // // @Override // protected int readBlock(long fileOffset, byte[] block, int blockLen) throws IOException { // try { // InputStream in = service.getObject(bucketName, getObjectKey(false), null, null, null, null, fileOffset, fileOffset+BLOCK_SIZE) // .getDataInputStream(); // // // Read up to blockLen bytes // try { // int totalRead = 0; // int read; // while(totalReadSyncedFileAttributes, this class caches attributes for a certain amount of time * after which fresh values are retrieved from the server. * * @author Maxence Bernard */ private class S3ObjectFileAttributes extends SyncedFileAttributes { private final static int TTL = 60000; private S3ObjectFileAttributes() throws AuthException { super(TTL, false); // no initial update fetchAttributes(); // throws AuthException if no or bad credentials updateExpirationDate(); // declare the attributes as 'fresh' } private S3ObjectFileAttributes(org.jets3t.service.model.S3Object object) { super(TTL, false); // no initial update setAttributes(object); setExists(true); updateExpirationDate(); // declare the attributes as 'fresh' } private void setAttributes(org.jets3t.service.model.S3Object object) { setDirectory(object.getKey().endsWith("/")); setSize(object.getContentLength()); setDate(object.getLastModifiedDate().getTime()); setPermissions(DEFAULT_PERMISSIONS); // Note: owner is null for common prefix objects S3Owner owner = object.getOwner(); setOwner(owner==null?null:owner.getDisplayName()); } private void fetchAttributes() throws AuthException { try { setAttributes(service.getObjectDetails(bucketName, getObjectKey(), null, null, null, null)); // Object does not exist on the server setExists(true); } catch(S3ServiceException e) { // Object does not exist on the server, or could not be retrieved setExists(false); setDirectory(false); setSize(0); setDate(0); setPermissions(FilePermissions.EMPTY_FILE_PERMISSIONS); setOwner(null); handleAuthException(e, fileURL); } } @Override public void updateAttributes() { try { fetchAttributes(); } catch(Exception e) { // AuthException LOGGER.info("Failed to update attributes", e); } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/s3/S3ProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.s3; import com.mucommander.commons.file.*; import org.jets3t.service.Jets3tProperties; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import org.jets3t.service.impl.rest.httpclient.RestS3Service; import org.jets3t.service.security.AWSCredentials; import java.io.IOException; import java.util.StringTokenizer; /** * A file protocol provider for the Amazon S3 protocol. * * @author Maxence Bernard */ public class S3ProtocolProvider implements ProtocolProvider { public S3ProtocolProvider() { } public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { Credentials credentials = url.getCredentials(); if (credentials == null || credentials.getLogin().isEmpty() || credentials.getPassword().isEmpty()) { throw new AuthException(url); } S3Service service; String bucketName; if (instantiationParams.length == 0) { try { service = new RestS3Service(new AWSCredentials(credentials.getLogin(), credentials.getPassword())); Jets3tProperties props = new Jets3tProperties(); props.setProperty("s3service.s3-endpoint", url.getHost()); } catch(S3ServiceException e) { throw S3File.getIOException(e, url); } } else { service = (S3Service)instantiationParams[0]; } String path = url.getPath(); // Root resource if (("/").equals(path)) { return new S3Root(url, service); } // Fetch the bucket name from the URL StringTokenizer st = new StringTokenizer(path, "/"); bucketName = st.nextToken(); // Object resource if (st.hasMoreTokens()) { if (instantiationParams.length == 2) { return new S3Object(url, service, bucketName, (org.jets3t.service.model.S3Object)instantiationParams[1]); } return new S3Object(url, service, bucketName); } // Bucket resource if (instantiationParams.length == 2) { return new S3Bucket(url, service, (org.jets3t.service.model.S3Bucket)instantiationParams[1]); } return new S3Bucket(url, service, bucketName); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/s3/S3Root.java ================================================ package com.mucommander.commons.file.impl.s3; import com.mucommander.commons.file.*; import com.mucommander.commons.io.RandomAccessInputStream; import org.jets3t.service.S3Service; import org.jets3t.service.S3ServiceException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * S3Root represents the Amazon S3 root resource, also known as 'service'. * * @author Maxence Bernard */ public class S3Root extends S3File { private final SimpleFileAttributes atts; /** Default permissions for the S3 root */ private final static FilePermissions DEFAULT_PERMISSIONS = new SimpleFilePermissions(448); // rwx------ protected S3Root(FileURL url, S3Service service) { super(url, service); atts = new SimpleFileAttributes(); atts.setPath("/"); atts.setExists(true); atts.setDate(0); atts.setSize(0); atts.setDirectory(true); atts.setPermissions(DEFAULT_PERMISSIONS); atts.setOwner(null); atts.setGroup(null); } /////////////////////////// // S3File implementation // /////////////////////////// @Override public FileAttributes getFileAttributes() { return atts; } ///////////////////////////////// // ProtocolFile implementation // ///////////////////////////////// @Override public String getOwner() { return null; } @Override public boolean canGetOwner() { return false; } @Override public AbstractFile[] ls() throws IOException { try { org.jets3t.service.model.S3Bucket[] buckets = service.listAllBuckets(); int nbBuckets = buckets.length; AbstractFile[] bucketFiles = new AbstractFile[nbBuckets]; FileURL bucketURL; for(int i=0; i. */ package com.mucommander.commons.file.impl.sevenzip; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.io.RandomAccessFile; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; import net.sf.sevenzipjbinding.ExtractAskMode; import net.sf.sevenzipjbinding.ExtractOperationResult; import net.sf.sevenzipjbinding.IArchiveExtractCallback; import net.sf.sevenzipjbinding.IArchiveOpenVolumeCallback; import net.sf.sevenzipjbinding.IInArchive; import net.sf.sevenzipjbinding.IInStream; import net.sf.sevenzipjbinding.ISequentialOutStream; import net.sf.sevenzipjbinding.PropID; import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; /** * Created on 23/05/14. * @author Oleg Trifonov */ public class SevenZipArchiveFile extends SevenZipJBindingROArchiveFile { public SevenZipArchiveFile(AbstractFile file, ArchiveFormat sevenZipJBindingFormat, byte[] formatSignature) { super(file, sevenZipJBindingFormat, formatSignature); } private static class ArchiveOpenVolumeCallback implements IArchiveOpenVolumeCallback { /** * Cache for opened file streams */ private final Map openedRandomAccessFileList = new HashMap<>(); /** * This method doesn't needed, if using with VolumedArchiveInStream * and pass the name of the first archive in constructor. * (Use two argument constructor) * * @see IArchiveOpenVolumeCallback#getProperty(PropID) */ public Object getProperty(PropID propID) { return null; } /** * * The name of the required volume will be calculated out of the * name of the first volume and volume index. If you need * need volume index (integer) you will have to parse filename * and extract index. * *

             * int index = filename.substring(filename.length() - 3,
             *         filename.length());
             * 
    * */ public IInStream getStream(String filename) { try { // We use caching of opened streams, so check cache first RandomAccessFile randomAccessFile = openedRandomAccessFileList.get(filename); if (randomAccessFile != null) { // Cache hit. // Move the file pointer back to the beginning // in order to emulating new stream randomAccessFile.seek(0); return new RandomAccessFileInStream(randomAccessFile); } // Nothing useful in cache. Open required volume. randomAccessFile = new RandomAccessFile(filename, "r"); // Put new stream in the cache openedRandomAccessFileList.put(filename, randomAccessFile); return new RandomAccessFileInStream(randomAccessFile); } catch (FileNotFoundException fileNotFoundException) { // Required volume doesn't exist. This happens if the volume: // 1. never exists. 7-Zip doesn't know how many volumes should // exist, so it have to try each volume. // 2. should be there, but doesn't. This is an error case. // Since normal and error cases are possible, // we can't throw an error message return null; // We return always null in this case } catch (Exception e) { throw new RuntimeException(e); } } /** * Close all opened streams */ void close() throws IOException { for (RandomAccessFile file : openedRandomAccessFileList.values()) { file.close(); } } } public static class ExtractCallback implements IArchiveExtractCallback { private int hash = 0; private long size = 0; private int index; private boolean skipExtraction; private IInArchive inArchive; private OutputStream os; public ExtractCallback(IInArchive inArchive, OutputStream os) { this.inArchive = inArchive; this.os = os; } public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException { this.index = index; skipExtraction = (Boolean) inArchive.getProperty(index, PropID.IS_FOLDER); if (skipExtraction || extractAskMode != ExtractAskMode.EXTRACT) { return null; } return data -> { hash ^= Arrays.hashCode(data); size += data.length; try { os.write(data); } catch (IOException e) { throw new SevenZipException(e); } return data.length; // Return amount of proceed data }; } public void prepareOperation(ExtractAskMode extractAskMode) { //System.out.println("prepare " + index); } public void setOperationResult(ExtractOperationResult extractOperationResult) { if (skipExtraction) { return; } if (extractOperationResult != ExtractOperationResult.OK) { System.err.println("Extraction error = " + extractOperationResult); } else { //System.out.println(String.format("%9X | %10s | %s", hash, size, inArchive.getProperty(index, PropID.PATH))); hash = 0; size = 0; } } public void setCompleted(long completeValue) { //System.out.println("completed " + completeValue); } public void setTotal(long total) { //System.out.println("total " + index + " " + total); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/SevenZipFormatProvider.java ================================================ package com.mucommander.commons.file.impl.sevenzip; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import net.sf.sevenzipjbinding.ArchiveFormat; /** * This class is the provider for the '7z' archive format implemented by {@link SevenZipArchiveFile}. * * @author Arik Hadas */ public class SevenZipFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".7z", ".cb7"}; /** * Static instance of the filename filter that matches archive filenames * */ private final static ExtensionFilenameFilter filenameFilter = new ExtensionFilenameFilter(EXTENSIONS); private static final byte[] SIGNATURE = { 0x37, 0x7A, (byte) 0xBC, (byte) 0xAF, 0x27, 0x1C }; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipArchiveFile(file, ArchiveFormat.SEVEN_ZIP, SIGNATURE); } public FilenameFilter getFilenameFilter() { return filenameFilter; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/SignatureCheckedRandomAccessFile.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.sevenzip; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.StreamUtils; import net.sf.sevenzipjbinding.IInStream; import net.sf.sevenzipjbinding.ISequentialInStream; import net.sf.sevenzipjbinding.SevenZipException; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.trolsoft.utils.StrUtils; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; /** * @author Oleg Trifonov */ public class SignatureCheckedRandomAccessFile implements IInStream, ISequentialInStream { private static final Logger LOGGER = LoggerFactory.getLogger(SignatureCheckedRandomAccessFile.class); private final AbstractFile file; private InputStream stream; private long position; public SignatureCheckedRandomAccessFile(AbstractFile file, byte[] signature) throws UnsupportedFileOperationException { super(); this.position = 0; this.file = file; try { this.stream = openStreamAndCheckSignature(file, signature); } catch (IOException e) { e.printStackTrace(); LOGGER.trace("Error", e); throw new UnsupportedFileOperationException(FileOperation.READ_FILE); } } @Override public synchronized long seek(long offset, int seekOrigin) throws SevenZipException { try { if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { seekOnRandomAccessFile(offset, seekOrigin); } else { seekOnSequentialFile(offset, seekOrigin); } } catch (IOException e) { throw new SevenZipException(e); } return position; } private void seekOnRandomAccessFile(long offset, int seekOrigin) throws IOException { RandomAccessInputStream randomAccessInputStream = (RandomAccessInputStream) stream; switch (seekOrigin) { case SEEK_SET: position = offset; break; case SEEK_CUR: position += offset; break; case SEEK_END: position = randomAccessInputStream.getLength() + offset; break; } randomAccessInputStream.seek(position); } /** * @param offset * @param seekOrigin * @throws IOException */ private void seekOnSequentialFile(long offset, int seekOrigin) throws IOException { switch (seekOrigin) { case SEEK_SET: if (position != offset) { stream.close(); stream = file.getInputStream(); skip(offset); position = offset; } break; case SEEK_CUR: skip(offset); position += offset; break; case SEEK_END: long size = file.getSize(); if (size == -1) { throw new IOException("can't seek from file end without knowing it's size"); } long newPosition = size + (offset > 0 ? offset : 0); if (position != newPosition) { position = newPosition; stream.close(); stream = file.getInputStream(); stream.skip(position); } break; } } /** * @param skip * @throws IOException */ private void skip(long skip) throws IOException { if (skip <= 0) { return; } long skipped = stream.skip(skip); if (skipped < 0) { throw new IOException("non reasonable number of bytes skipped"); } position += skipped; while (skipped < skip) { int skipNow = (int) Long.min(skip - skipped, 1024); byte[] skipBuffer = new byte[skipNow]; int read = stream.read(skipBuffer, 0, skipBuffer.length); if (read == -1) { break; } else { position += read; skipped += read; } } } @Override public synchronized int read(byte[] bytes) throws SevenZipException { if (bytes.length == 0) { return 0; } try { int read = stream.read(bytes); if (read != -1) { position += read; return read; } return 0; } catch (IOException e) { throw new SevenZipException(e); } } private InputStream openStreamAndCheckSignature(AbstractFile file, byte[] signature) throws IOException { byte[] buf = new byte[signature.length]; InputStream is = null; int read = 0; try { if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { RandomAccessInputStream raiStream = file.getRandomAccessInputStream(); is = raiStream; if (buf.length > 0) { raiStream.seek(0); read = StreamUtils.readUpTo(raiStream, buf); raiStream.seek(0); } } else { PushbackInputStream pushbackInputStream = null; if (buf.length > 0) { pushbackInputStream = file.getPushBackInputStream(buf.length); is = pushbackInputStream; read = StreamUtils.readUpTo(pushbackInputStream, buf); } else { is = file.getInputStream(); } // TODO sometimes reading from pushbackInputStream returns 0 if (read <= 0 && file.getSize() > 0) { return file.getInputStream(); } pushbackInputStream.unread(buf, 0, read); } if (signature != null && !checkSignature(buf, signature)) { throw new IOException("Wrong file signature was " + StrUtils.bytesToHexStr(buf, 0, read) + " but should be " + StrUtils.bytesToHexStr(signature, 0, signature.length)); } } catch (IOException e) { IOUtils.closeQuietly(is); throw e; } return is; } private static boolean checkSignature(byte[] data, byte[] signature) { for (int i = 0; i < signature.length; i++) { if (data[i] != signature[i]) { return false; } } return true; } @Override public synchronized void close() throws IOException { if (stream != null) { stream.close(); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/multivolume/InArchiveWrapper.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.sevenzip.multivolume; import java.io.Closeable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sf.sevenzipjbinding.ArchiveFormat; import net.sf.sevenzipjbinding.ExtractOperationResult; import net.sf.sevenzipjbinding.IArchiveExtractCallback; import net.sf.sevenzipjbinding.IInArchive; import net.sf.sevenzipjbinding.IOutItemAllFormats; import net.sf.sevenzipjbinding.IOutUpdateArchive; import net.sf.sevenzipjbinding.IOutUpdateArchive7z; import net.sf.sevenzipjbinding.IOutUpdateArchiveBZip2; import net.sf.sevenzipjbinding.IOutUpdateArchiveGZip; import net.sf.sevenzipjbinding.IOutUpdateArchiveTar; import net.sf.sevenzipjbinding.IOutUpdateArchiveZip; import net.sf.sevenzipjbinding.ISequentialOutStream; import net.sf.sevenzipjbinding.PropID; import net.sf.sevenzipjbinding.PropertyInfo; import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.simple.ISimpleInArchive; public class InArchiveWrapper implements IInArchive { private static final Logger LOGGER = LoggerFactory.getLogger(InArchiveWrapper.class); private final IInArchive archive; private final Closeable[] closeables; public InArchiveWrapper(IInArchive archive, Closeable... closeables) { this.archive = archive; this.closeables = closeables; } @Override public int getNumberOfItems() throws SevenZipException { return getMainArchive().getNumberOfItems(); } @Override public Object getProperty(int index, PropID propID) throws SevenZipException { return getMainArchive().getProperty(index, propID); } @Override public String getStringProperty(int index, PropID propID) throws SevenZipException { return getMainArchive().getStringProperty(index, propID); } @Override public void extract(int[] indices, boolean testMode, IArchiveExtractCallback extractCallback) throws SevenZipException { getMainArchive().extract(indices, testMode, extractCallback); } @Override public ExtractOperationResult extractSlow(int index, ISequentialOutStream outStream) throws SevenZipException { return getMainArchive().extractSlow(index, outStream); } @Override public ExtractOperationResult extractSlow(int index, ISequentialOutStream outStream, String password) throws SevenZipException { return getMainArchive().extractSlow(index, outStream, password); } @Override public Object getArchiveProperty(PropID propID) throws SevenZipException { return getMainArchive().getArchiveProperty(propID); } @Override public String getStringArchiveProperty(PropID propID) throws SevenZipException { return getMainArchive().getStringArchiveProperty(propID); } @Override public int getNumberOfProperties() throws SevenZipException { return getMainArchive().getNumberOfProperties(); } @Override public PropertyInfo getPropertyInfo(int index) throws SevenZipException { return getMainArchive().getPropertyInfo(index); } @Override public int getNumberOfArchiveProperties() throws SevenZipException { return getMainArchive().getNumberOfArchiveProperties(); } @Override public PropertyInfo getArchivePropertyInfo(int index) throws SevenZipException { return getMainArchive().getArchivePropertyInfo(index); } @Override public ISimpleInArchive getSimpleInterface() { return getMainArchive().getSimpleInterface(); } @Override public ArchiveFormat getArchiveFormat() { return getMainArchive().getArchiveFormat(); } @Override public IOutUpdateArchive getConnectedOutArchive() throws SevenZipException { return getMainArchive().getConnectedOutArchive(); } @Override public IOutUpdateArchive7z getConnectedOutArchive7z() throws SevenZipException { return getMainArchive().getConnectedOutArchive7z(); } @Override public IOutUpdateArchiveZip getConnectedOutArchiveZip() throws SevenZipException { return getMainArchive().getConnectedOutArchiveZip(); } @Override public IOutUpdateArchiveTar getConnectedOutArchiveTar() throws SevenZipException { return getMainArchive().getConnectedOutArchiveTar(); } @Override public IOutUpdateArchiveGZip getConnectedOutArchiveGZip() throws SevenZipException { return getMainArchive().getConnectedOutArchiveGZip(); } @Override public IOutUpdateArchiveBZip2 getConnectedOutArchiveBZip2() throws SevenZipException { return getMainArchive().getConnectedOutArchiveBZip2(); } @Override public void close() throws SevenZipException { getMainArchive().close(); if (closeables != null) { for (Closeable c : closeables) { try { c.close(); } catch (Exception e) { LOGGER.warn("Error closing: {}", c, e); } } } } private IInArchive getMainArchive() { return archive; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/multivolume/SevenZipMultiVolumeCallbackHandler.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.sevenzip.multivolume; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.RandomAccessFile; import java.util.HashMap; import java.util.Map; import com.mucommander.commons.file.impl.sevenzip.SignatureCheckedRandomAccessFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import net.sf.sevenzipjbinding.IArchiveOpenVolumeCallback; import net.sf.sevenzipjbinding.ICryptoGetTextPassword; import net.sf.sevenzipjbinding.IInStream; import net.sf.sevenzipjbinding.ISeekableStream; import net.sf.sevenzipjbinding.PropID; import net.sf.sevenzipjbinding.SevenZipException; import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; public class SevenZipMultiVolumeCallbackHandler implements IArchiveOpenVolumeCallback, ICryptoGetTextPassword, Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(SevenZipMultiVolumeCallbackHandler.class); private final Map fileCache = new HashMap<>(); private final AbstractFile firstFile; private final byte[] signature; private final String password; private boolean firstVolume = true; public SevenZipMultiVolumeCallbackHandler(byte[] signature, AbstractFile firstFile, String password) { this.signature = signature; this.firstFile = firstFile; this.password = password; } @Override public Object getProperty(PropID propID) { switch (propID) { case NAME: return firstFile.getAbsolutePath(); } return null; } @Override public IInStream getStream(String filename) { try { IInStream stream = fileCache.get(filename); if (stream != null) { stream.seek(0, ISeekableStream.SEEK_SET); } else { if (firstVolume) { // Only first file starts with magic number AbstractFile abstractFile = FileFactory.getFile(filename); stream = new SignatureCheckedRandomAccessFile(abstractFile, signature); firstVolume = false; } else { stream = new RandomAccessFileInStream(new RandomAccessFile(filename, "r")); } fileCache.put(filename, stream); } return stream; } catch (FileNotFoundException e) { // There is no way to know ahead of time if we reached the last file, // So it is safe to ignore this Exception LOGGER.debug("Multi volume 7z file not found - This is expected after reading the final volume [filename = {}]", filename, e); return null; } catch (Exception e) { throw new RuntimeException(e); } } @Override public void close() { for (IInStream f : fileCache.values()) { try { f.close(); } catch (Exception e) { LOGGER.error("Error closing IInStream", e); } } } @Override public String cryptoGetTextPassword() throws SevenZipException { if (password == null) { throw new SevenZipException("No password was provided for opening protected archive."); } return password; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/multivolume/SevenZipRarMultiVolumeCallbackHandler.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.sevenzip.multivolume; import java.io.Closeable; import java.io.IOException; import java.util.HashMap; import java.util.Map; import com.mucommander.commons.file.impl.sevenzip.SignatureCheckedRandomAccessFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import net.sf.sevenzipjbinding.IArchiveOpenCallback; import net.sf.sevenzipjbinding.IArchiveOpenVolumeCallback; import net.sf.sevenzipjbinding.ICryptoGetTextPassword; import net.sf.sevenzipjbinding.IInStream; import net.sf.sevenzipjbinding.ISeekableStream; import net.sf.sevenzipjbinding.PropID; import net.sf.sevenzipjbinding.SevenZipException; public class SevenZipRarMultiVolumeCallbackHandler implements IArchiveOpenVolumeCallback, IArchiveOpenCallback, ICryptoGetTextPassword, Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(SevenZipRarMultiVolumeCallbackHandler.class); private Map fileCache = new HashMap<>(); private String lastFileName; private final byte[] signature; private final String password; public SevenZipRarMultiVolumeCallbackHandler(byte[] signature, String password) { this.signature = signature; this.password = password; } @Override public void setTotal(Long files, Long bytes) throws SevenZipException { // NO-OP } @Override public void setCompleted(Long files, Long bytes) throws SevenZipException { // NO-OP } @Override public Object getProperty(PropID propID) throws SevenZipException { switch (propID) { case NAME: return lastFileName; } return null; } @Override public IInStream getStream(String filename) throws SevenZipException { try { SignatureCheckedRandomAccessFile stream = fileCache.get(filename); if (stream != null) { stream.seek(0, ISeekableStream.SEEK_SET); } else { AbstractFile abstractFile = FileFactory.getFile(filename); stream = new SignatureCheckedRandomAccessFile(abstractFile, signature); fileCache.put(filename, stream); } lastFileName = filename; return stream; } catch (Exception e) { throw new RuntimeException(e); } } @Override public String cryptoGetTextPassword() throws SevenZipException { if (password == null) { throw new SevenZipException("No password was provided for opening protected archive."); } return password; } @Override public void close() throws IOException { for (SignatureCheckedRandomAccessFile f : fileCache.values()) { try { f.close(); } catch (Exception e) { LOGGER.error("Error closing SignatureCheckedRandomAccessFile", e); } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/BoolVector.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class BoolVector { protected boolean[] data = new boolean[10]; int capacityIncr = 10; int elt = 0; public BoolVector() { } public int size() { return elt; } private void ensureCapacity(int minCapacity) { int oldCapacity = data.length; if (minCapacity > oldCapacity) { boolean [] oldData = data; int newCapacity = oldCapacity + capacityIncr; if (newCapacity < minCapacity) { newCapacity = minCapacity; } data = new boolean[newCapacity]; System.arraycopy(oldData, 0, data, 0, elt); } } public boolean get(int index) { if (index >= elt) throw new ArrayIndexOutOfBoundsException(index); return data[index]; } public void Reserve(int s) { ensureCapacity(s); } public void add(boolean b) { ensureCapacity(elt + 1); data[elt++] = b; } public void clear() { elt = 0; } public boolean isEmpty() { return elt == 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/ByteBuffer.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class ByteBuffer { int _capacity; byte [] _items; public ByteBuffer() { _capacity = 0; _items = null; } public byte [] data() { return _items; } public int GetCapacity() { return _capacity; } public void SetCapacity(int newCapacity) { if (newCapacity == _capacity) return; byte [] newBuffer; if (newCapacity > 0) { newBuffer = new byte[newCapacity]; if(_capacity > 0) { int len = _capacity; if (newCapacity < len) len = newCapacity; System.arraycopy(_items,0,newBuffer,0,len); // for (int i = 0 ; i < len ; i++) newBuffer[i] = _items[i]; } } else newBuffer = null; // delete []_items; _items = newBuffer; _capacity = newCapacity; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/CRC.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class CRC { static public int[] Table = new int[256]; static { for (int i = 0; i < 256; i++) { int r = i; for (int j = 0; j < 8; j++) if ((r & 1) != 0) r = (r >>> 1) ^ 0xEDB88320; else r >>>= 1; Table[i] = r; } } int _value = -1; public void Init() { _value = -1; } public void updateByte(int b) { _value = Table[(_value ^ b) & 0xFF] ^ (_value >>> 8); } public void updateUInt32(int v) { for (int i = 0; i < 4; i++) updateByte((v >> (8 * i)) & 0xFF); } public void updateUInt64(long v) { for (int i = 0; i < 8; i++) updateByte((int) ((v >> (8 * i))) & 0xFF); } public int getDigest() { return ~_value; } public void Update(byte[] data, int size) { for (int i = 0; i < size; i++) _value = Table[(_value ^ data[i]) & 0xFF] ^ (_value >>> 8); } public void Update(byte[] data) { for (byte aData : data) _value = Table[(_value ^ aData) & 0xFF] ^ (_value >>> 8); } public void Update(byte[] data, int offset, int size) { for (int i = 0; i < size; i++) _value = Table[(_value ^ data[offset + i]) & 0xFF] ^ (_value >>> 8); } public static int calculateDigest(byte[] data, int size) { CRC crc = new CRC(); crc.Update(data, size); return crc.getDigest(); } static public boolean verifyDigest(int digest, byte[] data, int size) { return (calculateDigest(data, size) == digest); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/IntVector.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; // TODO remove it! public class IntVector { protected int[] data = new int[10]; int capacityIncr = 10; int elt = 0; public IntVector() { } public int size() { return elt; } private void ensureCapacity(int minCapacity) { int oldCapacity = data.length; if (minCapacity > oldCapacity) { int [] oldData = data; int newCapacity = oldCapacity + capacityIncr; if (newCapacity < minCapacity) { newCapacity = minCapacity; } data = new int[newCapacity]; System.arraycopy(oldData, 0, data, 0, elt); } } public int get(int index) { if (index >= elt) throw new ArrayIndexOutOfBoundsException(index); return data[index]; } public void Reserve(int s) { ensureCapacity(s); } public void add(int b) { ensureCapacity(elt + 1); data[elt++] = b; } public void clear() { elt = 0; } public boolean isEmpty() { return elt == 0; } public int remove(int index) { if (index >= elt) throw new ArrayIndexOutOfBoundsException(index); int oldValue = data[index]; int numMoved = elt - index - 1; if (numMoved > 0) System.arraycopy(elt, index+1, elt, index,numMoved); // TODO WTF ?! // data[--elt] = null; // Let gc do its work return oldValue; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/LimitedSequentialInStream.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class LimitedSequentialInStream extends java.io.InputStream { java.io.InputStream _stream; // ISequentialInStream long _size; long _pos; boolean _wasFinished; public LimitedSequentialInStream() { } public void SetStream(java.io.InputStream stream) { // ISequentialInStream _stream = stream; } public void Init(long streamSize) { _size = streamSize; _pos = 0; _wasFinished = false; } public int read() throws java.io.IOException { int ret = _stream.read(); if (ret == -1) _wasFinished = true; return ret; } public int read(byte [] data,int off, int size) throws java.io.IOException { long sizeToRead2 = (_size - _pos); if (size < sizeToRead2) sizeToRead2 = size; int sizeToRead = (int)sizeToRead2; if (sizeToRead > 0) { int realProcessedSize = _stream.read(data, off, sizeToRead); if (realProcessedSize == -1) { _wasFinished = true; return -1; } _pos += realProcessedSize; return realProcessedSize; } return -1; // EOF } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/LockedInStream.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IInStream; public class LockedInStream { IInStream _stream; public LockedInStream() { } public void Init(IInStream stream) { _stream = stream; } /* really too slow, don't use ! public synchronized int read(long startPos) throws java.io.IOException { // NWindows::NSynchronization::CCriticalSectionLock lock(_criticalSection); _stream.Seek(startPos, IInStream.STREAM_SEEK_SET); return _stream.read(); } */ public synchronized int read(long startPos, byte [] data, int size) throws java.io.IOException { // NWindows::NSynchronization::CCriticalSectionLock lock(_criticalSection); _stream.Seek(startPos, IInStream.STREAM_SEEK_SET); return _stream.read(data,0, size); } public synchronized int read(long startPos, byte [] data, int off, int size) throws java.io.IOException { // NWindows::NSynchronization::CCriticalSectionLock lock(_criticalSection); _stream.Seek(startPos, IInStream.STREAM_SEEK_SET); return _stream.read(data,off, size); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/LockedSequentialInStreamImp.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class LockedSequentialInStreamImp extends java.io.InputStream { LockedInStream _lockedInStream; long _pos; public LockedSequentialInStreamImp() { } public void Init(LockedInStream lockedInStream, long startPos) { _lockedInStream = lockedInStream; _pos = startPos; } public int read() throws java.io.IOException { throw new java.io.IOException("LockedSequentialInStreamImp : read() not implemented"); /* int ret = _lockedInStream.read(_pos); if (ret == -1) return -1; // EOF _pos += 1; return ret; */ } public int read(byte [] data, int off, int size) throws java.io.IOException { int realProcessedSize = _lockedInStream.read(_pos, data,off, size); if (realProcessedSize == -1) return -1; // EOF _pos += realProcessedSize; return realProcessedSize; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/LongVector.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class LongVector { protected long[] data = new long[10]; int capacityIncr = 10; int elt = 0; public LongVector() { } public int size() { return elt; } private void ensureCapacity(int minCapacity) { int oldCapacity = data.length; if (minCapacity > oldCapacity) { long [] oldData = data; int newCapacity = oldCapacity + capacityIncr; if (newCapacity < minCapacity) { newCapacity = minCapacity; } data = new long[newCapacity]; System.arraycopy(oldData, 0, data, 0, elt); } } public long get(int index) { if (index >= elt) throw new ArrayIndexOutOfBoundsException(index); return data[index]; } public void Reserve(int s) { ensureCapacity(s); } public void add(long b) { ensureCapacity(elt + 1); data[elt++] = b; } public void clear() { elt = 0; } public boolean isEmpty() { return elt == 0; } public long Back() { if (elt < 1) throw new ArrayIndexOutOfBoundsException(0); return data[elt-1]; } public long Front() { if (elt < 1) throw new ArrayIndexOutOfBoundsException(0); return data[0]; } public void DeleteBack() { // Delete(_size - 1); remove(elt-1); } public long remove(int index) { if (index >= elt) throw new ArrayIndexOutOfBoundsException(index); long oldValue = data[index]; int numMoved = elt - index - 1; if (numMoved > 0) System.arraycopy(elt, index+1, elt, index,numMoved); // TODO WTF ?! // data[--elt] = null; // Let gc do its work return oldValue; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/ObjectVector.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class ObjectVector extends java.util.Vector { public ObjectVector() { super(); } public void Reserve(int s) { ensureCapacity(s); } public E Back() { return get(elementCount-1); } public E Front() { return get(0); } public void DeleteBack() { remove(elementCount-1); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/Common/RecordVector.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.Common; public class RecordVector extends java.util.Vector { public RecordVector() { super(); } public void Reserve(int s) { ensureCapacity(s); } public E Back() { return get(elementCount-1); } public E Front() { return get(0); } public void DeleteBack() { remove(elementCount-1); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/BindInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; import com.mucommander.commons.file.impl.sevenzip.provider.Common.IntVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; public class BindInfo { public RecordVector Coders = new RecordVector<>(); public RecordVector BindPairs = new RecordVector<>(); public IntVector InStreams = new IntVector(); public IntVector OutStreams = new IntVector(); public void Clear() { Coders.clear(); BindPairs.clear(); // InStreams.clear(); // OutStreams.clear(); } public int FindBinderForInStream(int inStream) // const { for (int i = 0; i < BindPairs.size(); i++) if (BindPairs.get(i).InIndex == inStream) return i; return -1; } public int FindBinderForOutStream(int outStream) // const { for (int i = 0; i < BindPairs.size(); i++) if (BindPairs.get(i).OutIndex == outStream) return i; return -1; } public int GetCoderInStreamIndex(int coderIndex) // const { int streamIndex = 0; for (int i = 0; i < coderIndex; i++) streamIndex += Coders.get(i).NumInStreams; return streamIndex; } public int GetCoderOutStreamIndex(int coderIndex) // const { int streamIndex = 0; for (int i = 0; i < coderIndex; i++) streamIndex += Coders.get(i).NumOutStreams; return streamIndex; } public void FindInStream(int streamIndex, int [] coderIndex, // UInt32 &coderIndex int [] coderStreamIndex // UInt32 &coderStreamIndex ) { for (coderIndex[0] = 0; coderIndex[0] < Coders.size(); coderIndex[0]++) { int curSize = Coders.get(coderIndex[0]).NumInStreams; if (streamIndex < curSize) { coderStreamIndex[0] = streamIndex; return; } streamIndex -= curSize; } throw new UnknownError("1"); } public void FindOutStream(int streamIndex, int [] coderIndex, // UInt32 &coderIndex, int [] coderStreamIndex /* , UInt32 &coderStreamIndex */ ) { for (coderIndex[0] = 0; coderIndex[0] < Coders.size(); coderIndex[0]++) { int curSize = Coders.get(coderIndex[0]).NumOutStreams; if (streamIndex < curSize) { coderStreamIndex[0] = streamIndex; return; } streamIndex -= curSize; } throw new UnknownError("1"); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/BindPair.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; public class BindPair { public int InIndex; public int OutIndex; public BindPair() { InIndex = 0; OutIndex = 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/CoderInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder2; public class CoderInfo { ICompressCoder Coder; ICompressCoder2 Coder2; int NumInStreams; int NumOutStreams; LongVector InSizes = new LongVector(); LongVector OutSizes = new LongVector(); LongVector InSizePointers = new LongVector(); LongVector OutSizePointers = new LongVector(); public CoderInfo(int numInStreams, int numOutStreams) { NumInStreams = numInStreams; NumOutStreams = numOutStreams; InSizes.Reserve(NumInStreams); InSizePointers.Reserve(NumInStreams); OutSizePointers.Reserve(NumOutStreams); OutSizePointers.Reserve(NumOutStreams); } static public void SetSizes( LongVector srcSizes, LongVector sizes, LongVector sizePointers, int numItems) { sizes.clear(); sizePointers.clear(); for(int i = 0; i < numItems; i++) { if (srcSizes == null || srcSizes.get(i) == -1) { // TBD null => -1 sizes.add((long) 0); sizePointers.add(-1); } else { sizes.add(srcSizes.get(i)); // sizes.Add(*srcSizes[i]); sizePointers.add(sizes.Back()); // sizePointers.Add(&sizes.Back()); } } } public void SetCoderInfo( LongVector inSizes, // const UInt64 **inSizes, LongVector outSizes) //const UInt64 **outSizes) { SetSizes(inSizes, InSizes, InSizePointers, NumInStreams); SetSizes(outSizes, OutSizes, OutSizePointers, NumOutStreams); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/CoderMixer2.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; public interface CoderMixer2 { void ReInit(); void SetBindInfo(BindInfo bindInfo); void SetCoderInfo(int coderIndex,LongVector inSizes, LongVector outSizes); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/CoderMixer2ST.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder2; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetInStream; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetOutStream; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetOutStreamSize; public class CoderMixer2ST implements ICompressCoder2 , CoderMixer2 { BindInfo _bindInfo = new BindInfo(); ObjectVector _coders = new ObjectVector<>(); public CoderMixer2ST() { } public void SetBindInfo(BindInfo bindInfo) { _bindInfo = bindInfo; } public void AddCoderCommon(boolean isMain) { CoderStreamsInfo csi = _bindInfo.Coders.get(_coders.size()); _coders.add(new STCoderInfo(csi.NumInStreams, csi.NumOutStreams, isMain)); } public void AddCoder2(ICompressCoder2 coder, boolean isMain) { AddCoderCommon(isMain); _coders.Back().Coder2 = coder; } public void AddCoder(ICompressCoder coder, boolean isMain) { AddCoderCommon(isMain); _coders.Back().Coder = coder; } public void ReInit() { } public void SetCoderInfo(int coderIndex,LongVector inSizes, LongVector outSizes) { // _coders[coderIndex].SetCoderInfo(inSizes, outSizes); _coders.get(coderIndex).SetCoderInfo(inSizes, outSizes); } public int GetInStream( RecordVector inStreams, Object useless_inSizes, // const UInt64 **inSizes, int streamIndex, InputStream [] inStreamRes) { InputStream seqInStream; for(int i = 0; i < _bindInfo.InStreams.size(); i++) { if (_bindInfo.InStreams.get(i) == streamIndex) { seqInStream = inStreams.get(i); inStreamRes[0] = seqInStream; // seqInStream.Detach(); return HRESULT.S_OK; } } int binderIndex = _bindInfo.FindBinderForInStream(streamIndex); if (binderIndex < 0) return HRESULT.E_INVALIDARG; int tmp1 [] = new int[1]; // TBD int tmp2 [] = new int[1]; // TBD _bindInfo.FindOutStream(_bindInfo.BindPairs.get(binderIndex).OutIndex, tmp1 /* coderIndex */ , tmp2 /* coderStreamIndex */ ); int coderIndex = tmp1[0], coderStreamIndex = tmp2[0]; CoderInfo coder = _coders.get(coderIndex); if (coder.Coder == null) return HRESULT.E_NOTIMPL; seqInStream = (InputStream)coder.Coder; // coder.Coder.QueryInterface(IID_ISequentialInStream, &seqInStream); int startIndex = _bindInfo.GetCoderInStreamIndex(coderIndex); if (coder.Coder == null) return HRESULT.E_NOTIMPL; ICompressSetInStream setInStream = (ICompressSetInStream)coder.Coder; // coder.Coder.QueryInterface(IID_ICompressSetInStream, &setInStream); if (setInStream == null) return HRESULT.E_NOTIMPL; if (coder.NumInStreams > 1) return HRESULT.E_NOTIMPL; for (int i = 0; i < coder.NumInStreams; i++) { InputStream [] tmp = new InputStream[1]; int res = GetInStream(inStreams, useless_inSizes, startIndex + i, tmp /* &seqInStream2 */ ); if (res != HRESULT.S_OK) return res; InputStream seqInStream2 = tmp[0]; res = setInStream.SetInStream(seqInStream2); if (res != HRESULT.S_OK) return res; } inStreamRes[0] = seqInStream; // seqInStream.Detach(); return HRESULT.S_OK; } public int GetOutStream( RecordVector outStreams, Object useless_outSizes, // const UInt64 **outSizes, int streamIndex, OutputStream [] outStreamRes) { OutputStream seqOutStream; for(int i = 0; i < _bindInfo.OutStreams.size(); i++) if (_bindInfo.OutStreams.get(i) == streamIndex) { seqOutStream = outStreams.get(i); outStreamRes[0] = seqOutStream; // seqOutStream.Detach(); return HRESULT.S_OK; } int binderIndex = _bindInfo.FindBinderForOutStream(streamIndex); if (binderIndex < 0) return HRESULT.E_INVALIDARG; int tmp1[] = new int[1]; int tmp2[] = new int[1]; _bindInfo.FindInStream(_bindInfo.BindPairs.get(binderIndex).InIndex, tmp1 /* coderIndex*/ , tmp2 /* coderStreamIndex */ ); int coderIndex = tmp1[0]; CoderInfo coder = _coders.get(coderIndex); if (coder.Coder == null) return HRESULT.E_NOTIMPL; try { seqOutStream = (OutputStream)coder.Coder; // coder.Coder.QueryInterface(IID_ISequentialOutStream, &seqOutStream); } catch (java.lang.ClassCastException e) { return HRESULT.E_NOTIMPL; } int startIndex = _bindInfo.GetCoderOutStreamIndex(coderIndex); if (coder.Coder == null) return HRESULT.E_NOTIMPL; ICompressSetOutStream setOutStream; try { setOutStream = (ICompressSetOutStream)coder.Coder; // coder.Coder.QueryInterface(IID_ICompressSetOutStream, &setOutStream); } catch (java.lang.ClassCastException e) { return HRESULT.E_NOTIMPL; } if (coder.NumOutStreams > 1) return HRESULT.E_NOTIMPL; for (int i = 0; i < coder.NumOutStreams; i++) { OutputStream [] tmp = new OutputStream[1]; int res = GetOutStream(outStreams, useless_outSizes, startIndex + i, tmp /* &seqOutStream2 */ ); if (res != HRESULT.S_OK) return res; OutputStream seqOutStream2 = tmp[0]; res = setOutStream.SetOutStream(seqOutStream2); if (res != HRESULT.S_OK) return res; } outStreamRes[0] = seqOutStream; // seqOutStream.Detach(); return HRESULT.S_OK; } public int Code( RecordVector inStreams, Object useless_inSizes, // const UInt64 ** inSizes , int numInStreams, RecordVector outStreams, Object useless_outSizes, // const UInt64 ** /* outSizes */, int numOutStreams, ICompressProgressInfo progress) throws IOException { if (numInStreams != _bindInfo.InStreams.size() || numOutStreams != _bindInfo.OutStreams.size()) return HRESULT.E_INVALIDARG; // Find main coder int _mainCoderIndex = -1; int i; for (i = 0; i < _coders.size(); i++) if (_coders.get(i).IsMain) { _mainCoderIndex = i; break; } if (_mainCoderIndex < 0) for (i = 0; i < _coders.size(); i++) if (_coders.get(i).NumInStreams > 1) { if (_mainCoderIndex >= 0) return HRESULT.E_NOTIMPL; _mainCoderIndex = i; } if (_mainCoderIndex < 0) _mainCoderIndex = 0; // _mainCoderIndex = 0; // _mainCoderIndex = _coders.Size() - 1; CoderInfo mainCoder = _coders.get(_mainCoderIndex); ObjectVector seqInStreams = new ObjectVector<>(); // CObjectVector< CMyComPtr > ObjectVector seqOutStreams = new ObjectVector<>(); // CObjectVector< CMyComPtr > int startInIndex = _bindInfo.GetCoderInStreamIndex(_mainCoderIndex); int startOutIndex = _bindInfo.GetCoderOutStreamIndex(_mainCoderIndex); for (i = 0; i < mainCoder.NumInStreams; i++) { InputStream tmp [] = new InputStream[1]; int res = GetInStream(inStreams, useless_inSizes, startInIndex + i, tmp /* &seqInStream */ ); if (res != HRESULT.S_OK) return res; InputStream seqInStream = tmp[0]; seqInStreams.add(seqInStream); } for (i = 0; i < mainCoder.NumOutStreams; i++) { OutputStream tmp [] = new OutputStream[1]; int res = GetOutStream(outStreams, useless_outSizes, startOutIndex + i, tmp); if (res != HRESULT.S_OK) return res; OutputStream seqOutStream = tmp[0]; seqOutStreams.add(seqOutStream); } RecordVector seqInStreamsSpec = new RecordVector<>(); RecordVector seqOutStreamsSpec = new RecordVector<>(); for (i = 0; i < mainCoder.NumInStreams; i++) seqInStreamsSpec.add(seqInStreams.get(i)); for (i = 0; i < mainCoder.NumOutStreams; i++) seqOutStreamsSpec.add(seqOutStreams.get(i)); for (i = 0; i < _coders.size(); i++) { if (i == _mainCoderIndex) continue; CoderInfo coder = _coders.get(i); try { ICompressSetOutStreamSize setOutStreamSize = (ICompressSetOutStreamSize)coder.Coder; int res = setOutStreamSize.SetOutStreamSize(coder.OutSizePointers.get(0)); if (res != HRESULT.S_OK) return res; } catch (java.lang.ClassCastException e) { // nothing to do } } if (mainCoder.Coder != null) { int res = mainCoder.Coder.Code( seqInStreamsSpec.get(0), seqOutStreamsSpec.get(0), // TBD mainCoder.InSizePointers.get(0), mainCoder.OutSizePointers.get(0), progress); if (res != HRESULT.S_OK) return res; } else { int res = mainCoder.Coder2.Code( seqInStreamsSpec, // &seqInStreamsSpec.Front( mainCoder.InSizePointers.Front(), // &mainCoder.InSizePointers.Front() mainCoder.NumInStreams, seqOutStreamsSpec, // &seqOutStreamsSpec.Front() mainCoder.OutSizePointers.Front(), // &mainCoder.OutSizePointers.Front() mainCoder.NumOutStreams, progress); if (res != HRESULT.S_OK) return res; } OutputStream stream = seqOutStreams.Front(); stream.flush(); return HRESULT.S_OK; } public void close() { } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/CoderStreamsInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; public class CoderStreamsInfo { public int NumInStreams; public int NumOutStreams; public CoderStreamsInfo() { NumInStreams = 0; NumOutStreams = 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/FilterCoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; import java.io.IOException; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetOutStream; /* // #ifdef _ST_MODE public ICompressSetInStream, public ISequentialInStream, public ICompressSetOutStream, public ISequentialOutStream, public IOutStreamFlush, // #endif */ public class FilterCoder extends java.io.OutputStream implements ICompressCoder , ICompressSetOutStream { public com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressFilter Filter = null; java.io.OutputStream _outStream = null; int _bufferPos; // UInt32 boolean _outSizeIsDefined; long _outSize; long _nowPos64; int Init() // HRESULT { _nowPos64 = 0; _outSizeIsDefined = false; return Filter.Init(); } // ICompressCoder public int Code( java.io.InputStream inStream, // , ISequentialInStream java.io.OutputStream outStream, // ISequentialOutStream long outSize, com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo progress) throws java.io.IOException { throw new java.io.IOException("Not implemented"); } // java.io.OutputStream public void write(int b) { throw new UnknownError("FilterCoder write"); } public void write(byte b[], int off, int size) throws IOException { if (b == null) { throw new NullPointerException(); } else if ((off < 0) || (off > b.length) || (size < 0) || ((off + size) > b.length) || ((off + size) < 0)) { throw new IndexOutOfBoundsException(); } else if (size == 0) { return; } if (off != 0) throw new IOException("FilterCoder - off <> 0"); byte [] cur_data = b; int cur_off = 0; while(size > 0) { int sizeMax = kBufferSize - _bufferPos; int sizeTemp = size; if (sizeTemp > sizeMax) sizeTemp = sizeMax; System.arraycopy(cur_data, cur_off, _buffer, _bufferPos , sizeTemp); // memmove(_buffer + _bufferPos, data, sizeTemp); size -= sizeTemp; cur_off = cur_off + sizeTemp; int endPos = _bufferPos + sizeTemp; _bufferPos = Filter.Filter(_buffer, endPos); if (_bufferPos == 0) { _bufferPos = endPos; break; } if (_bufferPos > endPos) { if (size != 0) throw new IOException("FilterCoder - write() : size <> 0"); // return HRESULT.E_FAIL; break; } WriteWithLimit(_outStream, _bufferPos); int i = 0; while(_bufferPos < endPos) _buffer[i++] = _buffer[_bufferPos++]; _bufferPos = i; } // return HRESULT.S_OK; } void WriteWithLimit(java.io.OutputStream outStream, int size) throws IOException { if (_outSizeIsDefined) { long remSize = _outSize - _nowPos64; if (size > remSize) size = (int)remSize; } outStream.write(_buffer,0,size); _nowPos64 += size; } byte [] _buffer; static final int kBufferSize = 1 << 17; public FilterCoder() { _buffer = new byte[kBufferSize]; } // ICompressSetOutStream public int SetOutStream(java.io.OutputStream outStream) { _bufferPos = 0; _outStream = outStream; return Init(); } public int ReleaseOutStream() throws IOException { if (_outStream != null) _outStream.close(); // Release() _outStream = null; return HRESULT.S_OK; } public void flush() throws IOException { if (_bufferPos != 0) { int endPos = Filter.Filter(_buffer, _bufferPos); if (endPos > _bufferPos) { for (; _bufferPos < endPos; _bufferPos++) _buffer[_bufferPos] = 0; if (Filter.Filter(_buffer, endPos) != endPos) throw new IOException("FilterCoder - flush() : E_FAIL"); // return HRESULT.E_FAIL; } _outStream.write(_buffer,0,_bufferPos); _bufferPos = 0; } _outStream.flush(); } public void close() throws IOException { if (_outStream != null) _outStream.close(); // Release() _outStream = null; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/OutStreamWithCRC.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; import com.mucommander.commons.file.impl.sevenzip.provider.Common.CRC; public class OutStreamWithCRC extends java.io.OutputStream { java.io.OutputStream _stream; long _size; CRC _crc = new CRC(); boolean _calculateCrc; public void write(int b) throws java.io.IOException { throw new java.io.IOException("OutStreamWithCRC - write() not implemented"); } public void write(byte [] data,int off, int size) throws java.io.IOException { if(_stream != null) { if (size == 0) { throw new java.io.IOException("size = 0"); } else { _stream.write(data, off,size); } } if (_calculateCrc) _crc.Update(data,off, size); _size += size; } public void SetStream(java.io.OutputStream stream) { _stream = stream; } public void Init() { Init(true); } public void Init(boolean calculateCrc) { _size = 0; _calculateCrc = calculateCrc; _crc.Init(); } public void ReleaseStream() throws java.io.IOException { // _stream.Release(); if (_stream != null) _stream.close(); _stream = null; } public long GetSize() { return _size; } public int GetCRC() { return _crc.getDigest(); } public void InitCRC() { _crc.Init(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/Common/STCoderInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common; public class STCoderInfo extends CoderInfo { boolean IsMain; public STCoderInfo(int numInStreams, int numOutStreams, boolean isMain) { super(numInStreams, numOutStreams); this.IsMain = isMain; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/IArchiveExtractCallback.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive; public interface IArchiveExtractCallback extends com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IProgress { // GetStream OUT: S_OK - OK, S_FALSE - skip this file int GetStream(int index, java.io.OutputStream [] outStream, int askExtractMode) throws java.io.IOException; int PrepareOperation(int askExtractMode); int SetOperationResult(int resultEOperationResult) throws java.io.IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/IInArchive.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive; import java.io.IOException; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IInStream; public interface IInArchive { int NExtract_NAskMode_kExtract = 0; int NExtract_NAskMode_kTest = 1; int NExtract_NAskMode_kSkip = 2; int NExtract_NOperationResult_kOK = 0; int NExtract_NOperationResult_kUnSupportedMethod = 1; int NExtract_NOperationResult_kDataError = 2; int NExtract_NOperationResult_kCRCError = 3; // Static-SFX (for Linux) can be big. long kMaxCheckStartPosition = 1 << 22; SevenZipEntry getEntry(int index); int size(); int close() throws IOException ; int Extract(int [] indices, int numItems, int testModeSpec, IArchiveExtractCallback extractCallbackSpec) throws java.io.IOException; int Open(IInStream stream) throws IOException; int Open( IInStream stream, // InStream *stream long maxCheckStartPosition // const UInt64 *maxCheckStartPosition, // IArchiveOpenCallback *openArchiveCallback */ ) throws java.io.IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/AltCoderInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ByteBuffer; class AltCoderInfo { public MethodID MethodID; public ByteBuffer Properties; public AltCoderInfo() { MethodID = new MethodID(); Properties = new ByteBuffer(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/ArchiveDatabase.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.BoolVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.IntVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector; class ArchiveDatabase { public LongVector PackSizes = new LongVector(); public BoolVector PackCRCsDefined = new BoolVector(); public IntVector PackCRCs = new IntVector(); public ObjectVector Folders = new ObjectVector<>(); public IntVector NumUnPackStreamsVector = new IntVector(); public ObjectVector Files = new ObjectVector<>(); void Clear() { PackSizes.clear(); PackCRCsDefined.clear(); // PackCRCs.clear(); Folders.clear(); // NumUnPackStreamsVector.clear(); Files.clear(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/ArchiveDatabaseEx.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.IntVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; public class ArchiveDatabaseEx extends ArchiveDatabase { InArchiveInfo ArchiveInfo = new InArchiveInfo(); LongVector packStreamStartPositions = new LongVector(); IntVector folderStartPackStreamIndex = new IntVector(); IntVector folderStartFileIndex = new IntVector(); IntVector fileIndexToFolderIndexMap = new IntVector(); void Clear() { super.Clear(); ArchiveInfo.Clear(); packStreamStartPositions.clear(); folderStartPackStreamIndex.clear(); folderStartFileIndex.clear(); fileIndexToFolderIndexMap.clear(); } void FillFolderStartPackStream() { folderStartPackStreamIndex.clear(); folderStartPackStreamIndex.Reserve(Folders.size()); int startPos = 0; for (Folder Folder : Folders) { folderStartPackStreamIndex.add(startPos); startPos += Folder.PackStreams.size(); } } void FillStartPos() { packStreamStartPositions.clear(); packStreamStartPositions.Reserve(PackSizes.size()); long startPos = 0; for(int i = 0; i < PackSizes.size(); i++) { packStreamStartPositions.add(startPos); startPos += PackSizes.get(i); } } public void Fill() throws java.io.IOException { FillFolderStartPackStream(); FillStartPos(); FillFolderStartFileIndex(); } public long GetFolderFullPackSize(int folderIndex) { int packStreamIndex = folderStartPackStreamIndex.get(folderIndex); Folder folder = Folders.get(folderIndex); long size = 0; for (int i = 0; i < folder.PackStreams.size(); i++) size += PackSizes.get(packStreamIndex + i); return size; } void FillFolderStartFileIndex() throws java.io.IOException { folderStartFileIndex.clear(); folderStartFileIndex.Reserve(Folders.size()); fileIndexToFolderIndexMap.clear(); fileIndexToFolderIndexMap.Reserve(Files.size()); int folderIndex = 0; int indexInFolder = 0; for (int i = 0; i < Files.size(); i++) { FileItem file = Files.get(i); boolean emptyStream = !file.HasStream; if (emptyStream && indexInFolder == 0) { fileIndexToFolderIndexMap.add(InArchive.kNumNoIndex); continue; } if (indexInFolder == 0) { // v3.13 incorrectly worked with empty folders // v4.07: Loop for skipping empty folders for (;;) { if (folderIndex >= Folders.size()) throw new java.io.IOException("Incorrect Header"); // CInArchiveException(CInArchiveException::kIncorrectHeader); folderStartFileIndex.add(i); // check it if (NumUnPackStreamsVector.get(folderIndex) != 0) break; folderIndex++; } } fileIndexToFolderIndexMap.add(folderIndex); if (emptyStream) continue; indexInFolder++; if (indexInFolder >= NumUnPackStreamsVector.get(folderIndex)) { folderIndex++; indexInFolder = 0; } } } public long GetFolderStreamPos(int folderIndex, int indexInFolder) { return ArchiveInfo.DataStartPosition + packStreamStartPositions.get(folderStartPackStreamIndex.get(folderIndex) + indexInFolder); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/BindInfoEx.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.BindInfo; class BindInfoEx extends BindInfo { RecordVector CoderMethodIDs = new RecordVector<>(); public void Clear() { super.Clear(); // CBindInfo::Clear(); CoderMethodIDs.clear(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/CoderInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector; class CoderInfo { int NumInStreams; int NumOutStreams; public ObjectVector AltCoders = new com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector<>(); boolean IsSimpleCoder() { return (NumInStreams == 1) && (NumOutStreams == 1); } public CoderInfo() { NumInStreams = 0; NumOutStreams = 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/Decoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ByteBuffer; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LimitedSequentialInStream; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LockedInStream; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LockedSequentialInStreamImp; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder2; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressFilter; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetDecoderProperties2; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.BindPair; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.CoderMixer2; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.CoderMixer2ST; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.CoderStreamsInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.FilterCoder; // import SevenZip.Archive.Common.CoderMixer2MT; class Decoder { boolean _bindInfoExPrevIsDefined; BindInfoEx _bindInfoExPrev; boolean _multiThread; // CoderMixer2MT _mixerCoderMTSpec; CoderMixer2ST _mixerCoderSTSpec; CoderMixer2 _mixerCoderCommon; ICompressCoder2 _mixerCoder; ObjectVector _decoders; public Decoder(boolean multiThread) { _multiThread = multiThread; _bindInfoExPrevIsDefined = false; _bindInfoExPrev = new BindInfoEx(); _mixerCoder = null; _decoders = new ObjectVector<>(); // #ifndef EXCLUDE_COM -- LoadMethodMap(); } static void ConvertFolderItemInfoToBindInfo(Folder folder,BindInfoEx bindInfo) { bindInfo.Clear(); for (int i = 0; i < folder.BindPairs.size(); i++) { BindPair bindPair = new BindPair(); bindPair.InIndex = folder.BindPairs.get(i).InIndex; bindPair.OutIndex = folder.BindPairs.get(i).OutIndex; bindInfo.BindPairs.add(bindPair); } int outStreamIndex = 0; for (int i = 0; i < folder.Coders.size(); i++) { CoderStreamsInfo coderStreamsInfo = new CoderStreamsInfo(); CoderInfo coderInfo = folder.Coders.get(i); coderStreamsInfo.NumInStreams = coderInfo.NumInStreams; coderStreamsInfo.NumOutStreams = coderInfo.NumOutStreams; bindInfo.Coders.add(coderStreamsInfo); AltCoderInfo altCoderInfo = coderInfo.AltCoders.Front(); bindInfo.CoderMethodIDs.add(altCoderInfo.MethodID); for (int j = 0; j < coderStreamsInfo.NumOutStreams; j++, outStreamIndex++) if (folder.FindBindPairForOutStream(outStreamIndex) < 0) bindInfo.OutStreams.add(outStreamIndex); } for (int i = 0; i < folder.PackStreams.size(); i++) bindInfo.InStreams.add(folder.PackStreams.get(i)); } static boolean AreCodersEqual(CoderStreamsInfo a1, CoderStreamsInfo a2) { return (a1.NumInStreams == a2.NumInStreams) && (a1.NumOutStreams == a2.NumOutStreams); } static boolean AreBindPairsEqual(BindPair a1, BindPair a2) { return (a1.InIndex == a2.InIndex) && (a1.OutIndex == a2.OutIndex); } static boolean AreBindInfoExEqual(BindInfoEx a1, BindInfoEx a2) { if (a1.Coders.size() != a2.Coders.size()) return false; int i; for (i = 0; i < a1.Coders.size(); i++) if (!AreCodersEqual(a1.Coders.get(i), a2.Coders.get(i))) return false; if (a1.BindPairs.size() != a2.BindPairs.size()) return false; for (i = 0; i < a1.BindPairs.size(); i++) if (!AreBindPairsEqual(a1.BindPairs.get(i), a2.BindPairs.get(i))) return false; for (i = 0; i < a1.CoderMethodIDs.size(); i++) if (a1.CoderMethodIDs.get(i) != a2.CoderMethodIDs.get(i)) return false; return a1.InStreams.size() == a2.InStreams.size() && a1.OutStreams.size() == a2.OutStreams.size(); } int Decode(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IInStream inStream, long startPos, LongVector packSizes, int packSizesOffset, // const UInt64 *packSizes, Folder folderInfo, OutputStream outStream, com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo compressProgress // , // ICompressProgressInfo *compressProgress, // _ST_MODE boolean mtMode, int numThreads ) throws IOException { ObjectVector inStreams = new ObjectVector<>(); // CObjectVector< CMyComPtr > LockedInStream lockedInStream = new LockedInStream(); lockedInStream.Init(inStream); for (int j = 0; j < folderInfo.PackStreams.size(); j++) { LockedSequentialInStreamImp lockedStreamImpSpec = new LockedSequentialInStreamImp(); lockedStreamImpSpec.Init(lockedInStream, startPos); startPos += packSizes.get(j+packSizesOffset); LimitedSequentialInStream streamSpec = new LimitedSequentialInStream(); streamSpec.SetStream(lockedStreamImpSpec); streamSpec.Init(packSizes.get(j+packSizesOffset)); inStreams.add(streamSpec); } int numCoders = folderInfo.Coders.size(); BindInfoEx bindInfo = new BindInfoEx(); ConvertFolderItemInfoToBindInfo(folderInfo, bindInfo); boolean createNewCoders; createNewCoders = !_bindInfoExPrevIsDefined || !AreBindInfoExEqual(bindInfo, _bindInfoExPrev); if (createNewCoders) { int i; _decoders.clear(); if (_mixerCoder != null) _mixerCoder.close(); // _mixerCoder.Release(); if (_multiThread) { /* _mixerCoderMTSpec = new CoderMixer2MT(); _mixerCoder = _mixerCoderMTSpec; _mixerCoderCommon = _mixerCoderMTSpec; */ throw new IOException("multithreaded decoder not implemented"); } else { _mixerCoderSTSpec = new CoderMixer2ST(); _mixerCoder = _mixerCoderSTSpec; _mixerCoderCommon = _mixerCoderSTSpec; } _mixerCoderCommon.SetBindInfo(bindInfo); for (i = 0; i < numCoders; i++) { CoderInfo coderInfo = folderInfo.Coders.get(i); AltCoderInfo altCoderInfo = coderInfo.AltCoders.Front(); /* #ifndef EXCLUDE_COM CMethodInfo methodInfo; if (!GetMethodInfo(altCoderInfo.MethodID, methodInfo)) return E_NOTIMPL; #endif */ if (coderInfo.IsSimpleCoder()) { ICompressCoder decoder = null; ICompressFilter filter = null; // #ifdef COMPRESS_LZMA if (altCoderInfo.MethodID.equals(MethodID.k_LZMA)) decoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Decoder(); // NCompress::NLZMA::CDecoder; if (altCoderInfo.MethodID.equals(MethodID.k_PPMD)) System.out.println("PPMD not implemented"); // decoder = new NCompress::NPPMD::CDecoder; if (altCoderInfo.MethodID.equals(MethodID.k_BCJ_X86)) filter = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.Branch.BCJ_x86_Decoder(); if (altCoderInfo.MethodID.equals(MethodID.k_Deflate)) System.out.println("DEFLATE not implemented"); // decoder = new NCompress::NDeflate::NDecoder::CCOMCoder; if (altCoderInfo.MethodID.equals(MethodID.k_BZip2)) System.out.println("BZIP2 not implemented"); // decoder = new NCompress::NBZip2::CDecoder; if (altCoderInfo.MethodID.equals(MethodID.k_Copy)) decoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.Copy.Decoder(); // decoder = new NCompress::CCopyCoder; if (altCoderInfo.MethodID.equals(MethodID.k_7zAES)) throw new IOException("k_7zAES not implemented"); // filter = new NCrypto::NSevenZ::CDecoder; if (filter != null) { FilterCoder coderSpec = new FilterCoder(); decoder = coderSpec; coderSpec.Filter = filter; } /* #ifndef EXCLUDE_COM if (decoder == 0) { RINOK(_libraries.CreateCoderSpec(methodInfo.FilePath, methodInfo.Decoder, &decoder)); } #endif */ if (decoder == null) return HRESULT.E_NOTIMPL; _decoders.add(decoder); if (_multiThread) { // _mixerCoderMTSpec.AddCoder(decoder); throw new IOException("Multithreaded decoder is not implemented"); } else { _mixerCoderSTSpec.AddCoder(decoder, false); } } else { ICompressCoder2 decoder = null; if (altCoderInfo.MethodID.equals(MethodID.k_BCJ2)) { decoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.Branch.BCJ2_x86_Decoder(); } if (decoder == null) return HRESULT.E_NOTIMPL; _decoders.add(decoder); if (_multiThread) { // _mixerCoderMTSpec.AddCoder2(decoder); throw new IOException("Multithreaded decoder is not implemented"); } else { _mixerCoderSTSpec.AddCoder2(decoder, false); } } } _bindInfoExPrev = bindInfo; _bindInfoExPrevIsDefined = true; } int i; _mixerCoderCommon.ReInit(); int packStreamIndex = 0, unPackStreamIndex = 0; int coderIndex = 0; for (i = 0; i < numCoders; i++) { CoderInfo coderInfo = folderInfo.Coders.get(i); AltCoderInfo altCoderInfo = coderInfo.AltCoders.Front(); Object decoder = _decoders.get(coderIndex); // CMyComPtr &decoder = _decoders[coderIndex]; { try { ICompressSetDecoderProperties2 setDecoderProperties = (ICompressSetDecoderProperties2)decoder; ByteBuffer properties = altCoderInfo.Properties; int size = properties.GetCapacity(); if (size == -1) // (size > 0xFFFFFFFF) return HRESULT.E_NOTIMPL; if (size > 0) { boolean ret = setDecoderProperties.SetDecoderProperties2(properties.data() /* , size */ ); if (!ret) return HRESULT.E_FAIL; } } catch (ClassCastException e) { // nothing to do } } /* #ifdef COMPRESS_MT if (mtMode) { CMyComPtr setCoderMt; decoder.QueryInterface(IID_ICompressSetCoderMt, &setCoderMt); if (setCoderMt) { RINOK(setCoderMt->SetNumberOfThreads(numThreads)); } } #endif #ifndef _NO_CRYPTO { CMyComPtr cryptoSetPassword; decoder.QueryInterface(IID_ICryptoSetPassword, &cryptoSetPassword); if (cryptoSetPassword) { if (getTextPassword == 0) return E_FAIL; CMyComBSTR password; RINOK(getTextPassword->CryptoGetTextPassword(&password)); CByteBuffer buffer; UString unicodePassword(password); const UInt32 sizeInBytes = unicodePassword.Length() * 2; buffer.SetCapacity(sizeInBytes); for (int i = 0; i < unicodePassword.Length(); i++) { wchar_t c = unicodePassword[i]; ((Byte *)buffer)[i * 2] = (Byte)c; ((Byte *)buffer)[i * 2 + 1] = (Byte)(c >> 8); } RINOK(cryptoSetPassword->CryptoSetPassword( (const Byte *)buffer, sizeInBytes)); } } #endif */ coderIndex++; int numInStreams = coderInfo.NumInStreams; int numOutStreams = coderInfo.NumOutStreams; LongVector packSizesPointers = new LongVector(); // CRecordVector LongVector unPackSizesPointers = new LongVector(); // CRecordVector packSizesPointers.Reserve(numInStreams); unPackSizesPointers.Reserve(numOutStreams); int j; for (j = 0; j < numOutStreams; j++, unPackStreamIndex++) unPackSizesPointers.add(folderInfo.UnPackSizes.get(unPackStreamIndex)); for (j = 0; j < numInStreams; j++, packStreamIndex++) { int bindPairIndex = folderInfo.FindBindPairForInStream(packStreamIndex); if (bindPairIndex >= 0) packSizesPointers.add( folderInfo.UnPackSizes.get(folderInfo.BindPairs.get(bindPairIndex).OutIndex)); else { int index = folderInfo.FindPackStreamArrayIndex(packStreamIndex); if (index < 0) return HRESULT.E_FAIL; packSizesPointers.add(packSizes.get(index)); } } _mixerCoderCommon.SetCoderInfo(i, packSizesPointers, // &packSizesPointers.Front(), unPackSizesPointers // &unPackSizesPointers.Front() ); } int [] temp_useless = new int [1]; // TBD int [] tmp1 = new int[1]; bindInfo.FindOutStream(bindInfo.OutStreams.get(0), tmp1 /* mainCoder */ , temp_useless /* temp */); if (_multiThread) { // _mixerCoderMTSpec.SetProgressCoderIndex(mainCoder); throw new IOException("Multithreaded decoder is not implemented"); } if (numCoders == 0) return 0; RecordVector inStreamPointers = new RecordVector<>(); // CRecordVector inStreamPointers.Reserve(inStreams.size()); for (i = 0; i < inStreams.size(); i++) inStreamPointers.add(inStreams.get(i)); RecordVector outStreamPointer = new RecordVector<>(); outStreamPointer.add(outStream); return _mixerCoder.Code( inStreamPointers, //&inStreamPointers.Front(), null, inStreams.size(), outStreamPointer, // &outStreamPointer, null, 1, compressProgress); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/ExtractFolderInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.BoolVector; class ExtractFolderInfo { /* #ifdef _7Z_VOL int VolumeIndex; #endif */ public int FileIndex; public int FolderIndex; public BoolVector ExtractStatuses = new BoolVector(); public long UnPackSize; public ExtractFolderInfo( /* #ifdef _7Z_VOL int volumeIndex, #endif */ int fileIndex, int folderIndex) // CNum fileIndex, CNum folderIndex { /* #ifdef _7Z_VOL VolumeIndex(volumeIndex), #endif */ FileIndex = fileIndex; FolderIndex = folderIndex; UnPackSize = 0; if (fileIndex != InArchive.kNumNoIndex) { ExtractStatuses.Reserve(1); ExtractStatuses.add(true); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/FileItem.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; public class FileItem { public long CreationTime; public long LastWriteTime; public long LastAccessTime; public long UnPackSize; public long StartPos; public int Attributes; public int FileCRC; public boolean IsDirectory; public boolean IsAnti; public boolean IsFileCRCDefined; public boolean AreAttributesDefined; public boolean HasStream; // public boolean IsCreationTimeDefined; replace by (CreationTime != 0) // public boolean IsLastWriteTimeDefined; replace by (LastWriteTime != 0) // public boolean IsLastAccessTimeDefined; replace by (LastAccessTime != 0) public boolean IsStartPosDefined; public String name; public FileItem() { HasStream = true; IsDirectory = false; IsAnti = false; IsFileCRCDefined = false; AreAttributesDefined = false; CreationTime = 0; // IsCreationTimeDefined = false; LastWriteTime = 0; // IsLastWriteTimeDefined = false; LastAccessTime = 0; // IsLastAccessTimeDefined = false; IsStartPosDefined = false; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/Folder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import java.io.IOException; import com.mucommander.commons.file.impl.sevenzip.provider.Common.IntVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.BindPair; class Folder { public RecordVector Coders = new RecordVector<>(); RecordVector BindPairs = new RecordVector<>(); IntVector PackStreams = new IntVector(); LongVector UnPackSizes = new LongVector(); int UnPackCRC; boolean UnPackCRCDefined; Folder() { UnPackCRCDefined = false; } long GetUnPackSize() throws IOException { if (UnPackSizes.isEmpty()) return 0; for (int i = UnPackSizes.size() - 1; i >= 0; i--) if (FindBindPairForOutStream(i) < 0) return UnPackSizes.get(i); throw new IOException("1"); // throw 1 // TBD } int FindBindPairForInStream(int inStreamIndex) { for(int i = 0; i < BindPairs.size(); i++) if (BindPairs.get(i).InIndex == inStreamIndex) return i; return -1; } int FindBindPairForOutStream(int outStreamIndex) { for(int i = 0; i < BindPairs.size(); i++) if (BindPairs.get(i).OutIndex == outStreamIndex) return i; return -1; } int FindPackStreamArrayIndex(int inStreamIndex) { for(int i = 0; i < PackStreams.size(); i++) if (PackStreams.get(i) == inStreamIndex) return i; return -1; } int GetNumOutStreams() { int result = 0; for (CoderInfo Coder : Coders) result += Coder.NumOutStreams; return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/FolderOutStream.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.BoolVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IArchiveExtractCallback; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.OutStreamWithCRC; class FolderOutStream extends java.io.OutputStream { OutStreamWithCRC _outStreamWithHashSpec; java.io.OutputStream _outStreamWithHash; ArchiveDatabaseEx _archiveDatabase; BoolVector _extractStatuses; int _startIndex; int _ref2Offset; IArchiveExtractCallback _extractCallback; boolean _testMode; int _currentIndex; boolean _fileIsOpen; long _filePos; public FolderOutStream() { _outStreamWithHashSpec = new OutStreamWithCRC(); _outStreamWithHash = _outStreamWithHashSpec; } public int Init( ArchiveDatabaseEx archiveDatabase, int ref2Offset, int startIndex, BoolVector extractStatuses, IArchiveExtractCallback extractCallback, boolean testMode) throws java.io.IOException { _archiveDatabase = archiveDatabase; _ref2Offset = ref2Offset; _startIndex = startIndex; _extractStatuses = extractStatuses; _extractCallback = extractCallback; _testMode = testMode; _currentIndex = 0; _fileIsOpen = false; return WriteEmptyFiles(); } int OpenFile() throws java.io.IOException { int askMode; if(_extractStatuses.get(_currentIndex)) askMode = _testMode ? IInArchive.NExtract_NAskMode_kTest : IInArchive.NExtract_NAskMode_kExtract; else askMode = IInArchive.NExtract_NAskMode_kSkip; int index = _startIndex + _currentIndex; // RINOK(_extractCallback->GetStream(_ref2Offset + index, &realOutStream, askMode)); java.io.OutputStream [] realOutStream2 = new java.io.OutputStream[1]; // TBD int ret = _extractCallback.GetStream(_ref2Offset + index, realOutStream2, askMode); if (ret != HRESULT.S_OK) return ret; java.io.OutputStream realOutStream = realOutStream2[0]; _outStreamWithHashSpec.SetStream(realOutStream); _outStreamWithHashSpec.Init(); if (askMode == IInArchive.NExtract_NAskMode_kExtract && (realOutStream == null)) { FileItem fileInfo = _archiveDatabase.Files.get(index); if (!fileInfo.IsAnti && !fileInfo.IsDirectory) askMode = IInArchive.NExtract_NAskMode_kSkip; } return _extractCallback.PrepareOperation(askMode); } int WriteEmptyFiles() throws java.io.IOException { for(;_currentIndex < _extractStatuses.size(); _currentIndex++) { int index = _startIndex + _currentIndex; FileItem fileInfo = _archiveDatabase.Files.get(index); if (!fileInfo.IsAnti && !fileInfo.IsDirectory && fileInfo.UnPackSize != 0) return HRESULT.S_OK; int res = OpenFile(); if (res != HRESULT.S_OK) return res; res = _extractCallback.SetOperationResult( IInArchive.NExtract_NOperationResult_kOK); if (res != HRESULT.S_OK) return res; _outStreamWithHashSpec.ReleaseStream(); } return HRESULT.S_OK; } public void write(int b) throws java.io.IOException { throw new java.io.IOException("FolderOutStream - write() not implemented"); } public void write(byte [] data,int off, int size) throws java.io.IOException // UInt32 *processedSize { int realProcessedSize = 0; while(_currentIndex < _extractStatuses.size()) { if (_fileIsOpen) { int index = _startIndex + _currentIndex; FileItem fileInfo = _archiveDatabase.Files.get(index); long fileSize = fileInfo.UnPackSize; long numBytesToWrite2 = (int)(fileSize - _filePos); int tmp = size - realProcessedSize; if (tmp < numBytesToWrite2) numBytesToWrite2 = tmp; int numBytesToWrite = (int)numBytesToWrite2; int processedSizeLocal; // int res = _outStreamWithHash.Write((const Byte *)data + realProcessedSize,numBytesToWrite, &processedSizeLocal)); // if (res != HRESULT.S_OK) throw new java.io.IOException("_outStreamWithHash.Write : " + res); // return res; processedSizeLocal = numBytesToWrite; _outStreamWithHash.write(data,realProcessedSize + off,numBytesToWrite); _filePos += processedSizeLocal; realProcessedSize += processedSizeLocal; if (_filePos == fileSize) { boolean digestsAreEqual; digestsAreEqual = !fileInfo.IsFileCRCDefined || (fileInfo.FileCRC == _outStreamWithHashSpec.GetCRC()); int res = _extractCallback.SetOperationResult( digestsAreEqual ? IInArchive.NExtract_NOperationResult_kOK : IInArchive.NExtract_NOperationResult_kCRCError); if (res != HRESULT.S_OK) throw new java.io.IOException("_extractCallback.SetOperationResult : " + res); // return res; _outStreamWithHashSpec.ReleaseStream(); _fileIsOpen = false; _currentIndex++; } if (realProcessedSize == size) { int res = WriteEmptyFiles(); if (res != HRESULT.S_OK) throw new java.io.IOException("WriteEmptyFiles : " + res); // return res; return ;// return realProcessedSize; } } else { int res = OpenFile(); if (res != HRESULT.S_OK) throw new java.io.IOException("OpenFile : " + res); // return res; _fileIsOpen = true; _filePos = 0; } } // return size; } public int FlushCorrupted(int resultEOperationResult) throws java.io.IOException { while(_currentIndex < _extractStatuses.size()) { if (_fileIsOpen) { int res = _extractCallback.SetOperationResult(resultEOperationResult); if (res != HRESULT.S_OK) return res; _outStreamWithHashSpec.ReleaseStream(); _fileIsOpen = false; _currentIndex++; } else { int res = OpenFile(); if (res != HRESULT.S_OK) return res; _fileIsOpen = true; } } return HRESULT.S_OK; } public int WasWritingFinished() { int val = _extractStatuses.size(); if (_currentIndex == val) return HRESULT.S_OK; return HRESULT.E_FAIL; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/Handler.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import java.io.IOException; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IInStream; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IArchiveExtractCallback; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZipEntry; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.LocalCompressProgressInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.LocalProgress; public class Handler implements com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive { // useless LongVector _fileInfoPopIDs = new LongVector(); IInStream _inStream; ArchiveDatabaseEx _database; int _numThreads; public Handler() { _numThreads = 1; // TBD _database = new ArchiveDatabaseEx(); } public int Open(IInStream stream) throws IOException { return Open(stream,kMaxCheckStartPosition); } public int Open( IInStream stream, // InStream *stream long maxCheckStartPosition // const UInt64 *maxCheckStartPosition, // IArchiveOpenCallback *openArchiveCallback */ ) throws IOException { close(); // useless _fileInfoPopIDs.clear(); // TBD try { // TBD CMyComPtr openArchiveCallbackTemp = openArchiveCallback; /* TBD CMyComPtr getTextPassword; if (openArchiveCallback) { openArchiveCallbackTemp.QueryInterface( IID_ICryptoGetTextPassword, &getTextPassword); } */ InArchive archive = new InArchive(); int ret = archive.Open(stream, maxCheckStartPosition); if (ret != HRESULT.S_OK) return ret; ret = archive.ReadDatabase(_database); // getTextPassword if (ret != HRESULT.S_OK) return ret; _database.Fill(); _inStream = stream; } // FillPopIDs(); // useless _fileInfoPopIDs return HRESULT.S_OK; } public int Extract(int [] indices, int numItems, int testModeSpec, IArchiveExtractCallback extractCallbackSpec) throws IOException { boolean testMode = (testModeSpec != 0); long importantTotalUnPacked = 0; boolean allFilesMode = (numItems == -1); if (allFilesMode) numItems = // #ifdef _7Z_VOL // _refs.Size(); // #else _database.Files.size(); // #endif if(numItems == 0) return HRESULT.S_OK; ObjectVector extractFolderInfoVector = new ObjectVector<>(); for(int ii = 0; ii < numItems; ii++) { int ref2Index = allFilesMode ? ii : indices[ii]; { /* #ifdef _7Z_VOL // const CRef &ref = ref2.Refs[ri]; const CRef &ref = _refs[ref2Index]; int volumeIndex = ref.VolumeIndex; const CVolume &volume = _volumes[volumeIndex]; const CArchiveDatabaseEx &database = volume.Database; UInt32 fileIndex = ref.ItemIndex; #else */ ArchiveDatabaseEx database = _database; //#endif int folderIndex = database.fileIndexToFolderIndexMap.get(ref2Index); if (folderIndex == com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip.InArchive.kNumNoIndex) { extractFolderInfoVector.add( new ExtractFolderInfo( // #ifdef _7Z_VOL // volumeIndex, // #endif ref2Index, com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip.InArchive.kNumNoIndex)); continue; } if (extractFolderInfoVector.isEmpty() || folderIndex != extractFolderInfoVector.Back().FolderIndex /* #ifdef _7Z_VOL || volumeIndex != extractFolderInfoVector.Back().VolumeIndex #endif */ ) { extractFolderInfoVector.add( new ExtractFolderInfo( /* #ifdef _7Z_VOL volumeIndex, #endif */ com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip.InArchive.kNumNoIndex, folderIndex)); Folder folderInfo = database.Folders.get(folderIndex); long unPackSize = folderInfo.GetUnPackSize(); importantTotalUnPacked += unPackSize; extractFolderInfoVector.Back().UnPackSize = unPackSize; } ExtractFolderInfo efi = extractFolderInfoVector.Back(); // const CFolderInfo &folderInfo = m_dam_Folders[folderIndex]; int startIndex = database.folderStartFileIndex.get(folderIndex); // CNum for (int index = efi.ExtractStatuses.size(); index <= ref2Index - startIndex; index++) { // UInt64 unPackSize = _database.Files[startIndex + index].UnPackSize; // Count partial_folder_size // efi.UnPackSize += unPackSize; // importantTotalUnPacked += unPackSize; efi.ExtractStatuses.add(index == ref2Index - startIndex); } } } extractCallbackSpec.SetTotal(importantTotalUnPacked); Decoder decoder = new Decoder( // #ifdef _ST_MODE false // #else // true // #endif ); long currentImportantTotalUnPacked = 0; long totalFolderUnPacked; for(int i = 0; i < extractFolderInfoVector.size(); i++, currentImportantTotalUnPacked += totalFolderUnPacked) { ExtractFolderInfo efi = extractFolderInfoVector.get(i); totalFolderUnPacked = efi.UnPackSize; int res = extractCallbackSpec.SetCompleted(currentImportantTotalUnPacked); if (res != HRESULT.S_OK) return res; FolderOutStream folderOutStream = new FolderOutStream(); // #ifdef _7Z_VOL // const CVolume &volume = _volumes[efi.VolumeIndex]; // const CArchiveDatabaseEx &database = volume.Database; // #else ArchiveDatabaseEx database = _database; //#endif int startIndex; // CNum if (efi.FileIndex != InArchive.kNumNoIndex) startIndex = efi.FileIndex; else startIndex = database.folderStartFileIndex.get(efi.FolderIndex); int result = folderOutStream.Init(database, // #ifdef _7Z_VOL // volume.StartRef2Index, // #else 0, // #endif startIndex, efi.ExtractStatuses, extractCallbackSpec, testMode); if (result != HRESULT.S_OK) return result; if (efi.FileIndex != InArchive.kNumNoIndex) continue; int folderIndex = efi.FolderIndex; // CNum Folder folderInfo = database.Folders.get(folderIndex); LocalProgress localProgressSpec = new LocalProgress(); localProgressSpec.Init(extractCallbackSpec, false); LocalCompressProgressInfo localCompressProgressSpec = new LocalCompressProgressInfo(); localCompressProgressSpec.Init(localProgressSpec, ICompressProgressInfo.INVALID , currentImportantTotalUnPacked); int packStreamIndex = database.folderStartPackStreamIndex.get(folderIndex); // CNum long folderStartPackPos = database.GetFolderStreamPos(folderIndex, 0); /* #ifndef _NO_CRYPTO CMyComPtr getTextPassword; if (extractCallback) extractCallback.QueryInterface(IID_ICryptoGetTextPassword, &getTextPassword); #endif */ try { result = decoder.Decode( // #ifdef _7Z_VOL // volume.Stream, // #else _inStream, // #endif folderStartPackPos, database.PackSizes, // database.PackSizes.get(packStreamIndex), packStreamIndex, folderInfo, folderOutStream, localCompressProgressSpec // #ifndef _NO_CRYPTO // , getTextPassword // #endif // #ifdef COMPRESS_MT // , true, _numThreads // #endif ); if (result == HRESULT.S_FALSE) { result = folderOutStream.FlushCorrupted(IInArchive.NExtract_NOperationResult_kDataError); if (result != HRESULT.S_OK) return result; continue; } if (result == HRESULT.E_NOTIMPL) { result = folderOutStream.FlushCorrupted(IInArchive.NExtract_NOperationResult_kUnSupportedMethod); if (result != HRESULT.S_OK) return result; continue; } if (result != HRESULT.S_OK) return result; if (folderOutStream.WasWritingFinished() != HRESULT.S_OK) { result = folderOutStream.FlushCorrupted(IInArchive.NExtract_NOperationResult_kDataError); if (result != HRESULT.S_OK) return result; } } catch(Exception e) { System.out.println("IOException : " + e); e.printStackTrace(); result = folderOutStream.FlushCorrupted(IInArchive.NExtract_NOperationResult_kDataError); if (result != HRESULT.S_OK) return result; } } return HRESULT.S_OK; } public int close() throws IOException { /* #ifdef _7Z_VOL _volumes.Clear(); _refs.Clear(); #else _inStream.Release(); _database.Clear(); #endif return S_OK; */ if (_inStream != null) _inStream.close(); // _inStream.Release(); _inStream = null; _database.Clear(); return 0; } public int size() { return _database.Files.size(); } long getPackSize(int index2) { long packSize = 0; int folderIndex = _database.fileIndexToFolderIndexMap.get(index2); if (folderIndex != InArchive.kNumNoIndex) { if (_database.folderStartFileIndex.get(folderIndex) == index2) packSize = _database.GetFolderFullPackSize(folderIndex); } return packSize; } static int GetUInt32FromMemLE(byte [] p , int off) { return p[off] | (((int)p[off + 1]) << 8) | (((int)p[off + 2]) << 16) | (((int)p[off +3]) << 24); } static String GetStringForSizeValue(int value) { for (int i = 31; i >= 0; i--) { if ((1 << i) == value) { return "" + i; } } String result = ""; if (value % (1 << 20) == 0) { result += "" + (value >> 20); result += "m"; } else if (value % (1 << 10) == 0) { result += "" + (value >> 10); result += "k"; } else { result += "" + (value); result += "b"; } return result; } String getMethods(int index2) { int folderIndex = _database.fileIndexToFolderIndexMap.get(index2); if (folderIndex != InArchive.kNumNoIndex) { Folder folderInfo = _database.Folders.get(folderIndex); StringBuilder methodsString = new StringBuilder(); for (int i = folderInfo.Coders.size() - 1; i >= 0; i--) { CoderInfo coderInfo = folderInfo.Coders.get(i); if (methodsString.length() != 0) { methodsString.append(' '); } // MethodInfo methodInfo; for (int j = 0; j < coderInfo.AltCoders.size(); j++) { if (j > 0) { methodsString.append("|"); } AltCoderInfo altCoderInfo = coderInfo.AltCoders.get(j); String methodName = getMethodName(altCoderInfo); if (methodName != null) { methodsString.append(methodName); if (altCoderInfo.MethodID.equals(MethodID.k_LZMA)) { if (altCoderInfo.Properties.GetCapacity() >= 5) { methodsString.append(":"); int dicSize = GetUInt32FromMemLE(altCoderInfo.Properties.data(),1); methodsString.append(GetStringForSizeValue(dicSize)); } } /* else if (altCoderInfo.MethodID == k_PPMD) { if (altCoderInfo.Properties.GetCapacity() >= 5) { Byte order = *(const Byte *)altCoderInfo.Properties; methodsString += ":o"; methodsString += ConvertUInt32ToString(order); methodsString += ":mem"; UInt32 dicSize = GetUInt32FromMemLE( ((const Byte *)altCoderInfo.Properties + 1)); methodsString += GetStringForSizeValue(dicSize); } } else if (altCoderInfo.MethodID == k_AES) { if (altCoderInfo.Properties.GetCapacity() >= 1) { methodsString += ":"; const Byte *data = (const Byte *)altCoderInfo.Properties; Byte firstByte = *data++; UInt32 numCyclesPower = firstByte & 0x3F; methodsString += ConvertUInt32ToString(numCyclesPower); } } else { if (altCoderInfo.Properties.GetCapacity() > 0) { methodsString += ":["; for (size_t bi = 0; bi < altCoderInfo.Properties.GetCapacity(); bi++) { if (bi > 5 && bi + 1 < altCoderInfo.Properties.GetCapacity()) { methodsString += ".."; break; } else methodsString += GetHex2(altCoderInfo.Properties[bi]); } methodsString += "]"; } } */ } else { // TBD methodsString += altCoderInfo.MethodID.ConvertToString(); } } } return methodsString.toString(); } return ""; } private String getMethodName(AltCoderInfo altCoderInfo) { if (altCoderInfo.MethodID.equals(MethodID.k_Copy)) { return "Copy"; } else if (altCoderInfo.MethodID.equals(MethodID.k_LZMA)) { return "LZMA"; } else if (altCoderInfo.MethodID.equals(MethodID.k_BCJ)) { return "BCJ"; } else if (altCoderInfo.MethodID.equals(MethodID.k_BCJ2)) { return "BCJ2"; } else if (altCoderInfo.MethodID.equals(MethodID.k_PPMD)) { return "PPMD"; } else if (altCoderInfo.MethodID.equals(MethodID.k_Deflate)) { return "Deflate"; } else if (altCoderInfo.MethodID.equals(MethodID.k_Deflate64)) { return "Deflate64"; } else if (altCoderInfo.MethodID.equals(MethodID.k_BZip2)) { return "BZip2"; } else if (altCoderInfo.MethodID.equals(MethodID.k_7zAES)) { return "7zAES"; } else { return null; } } public SevenZipEntry getEntry(int index) { com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip.FileItem item = _database.Files.get(index); long crc = -1; if (item.IsFileCRCDefined) { crc = item.FileCRC & 0xFFFFFFFFL; } long position = -1; if (item.IsStartPosDefined) position = item.StartPos; return new SevenZipEntry( item.name, getPackSize(index), item.UnPackSize, crc, item.LastWriteTime, position, item.IsDirectory, item.Attributes, getMethods(index) ); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/Header.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; class Header { public static final int kSignatureSize = 6; public static byte [] kSignature= {'7', 'z', (byte)0xBC, (byte)0xAF, 0x27, 0x1C}; public static byte kMajorVersion = 0; public class NID { public static final int kEnd = 0; public static final int kHeader = 1; public static final int kArchiveProperties = 2; public static final int kAdditionalStreamsInfo = 3; public static final int kMainStreamsInfo = 4; public static final int kFilesInfo = 5; public static final int kPackInfo = 6; public static final int kUnPackInfo = 7; public static final int kSubStreamsInfo = 8; public static final int kSize = 9; public static final int kCRC = 10; public static final int kFolder = 11; public static final int kCodersUnPackSize = 12; public static final int kNumUnPackStream = 13; public static final int kEmptyStream = 14; public static final int kEmptyFile = 15; public static final int kAnti = 16; public static final int kName = 17; public static final int kCreationTime = 18; public static final int kLastAccessTime = 19; public static final int kLastWriteTime = 20; public static final int kWinAttributes = 21; public static final int kComment = 22; public static final int kEncodedHeader = 23; public static final int kStartPos = 24; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/InArchive.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.*; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.Common.BindPair; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.SequentialOutStreamImp2; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.StreamUtils; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IInStream; import java.io.IOException; import java.io.OutputStream; class InArchive extends Header { // CNum static public final int kNumMax = 0x7FFFFFFF; static public final int kNumNoIndex = 0xFFFFFFFF; IInStream _stream; // CMyComPtr _stream; ObjectVector _inByteVector; InByte2 _inByteBack; long _arhiveBeginStreamPosition; long _position; public InArchive() { _inByteVector = new ObjectVector<>(); _inByteBack = new InByte2(); } public void AddByteStream(byte [] buffer, int size) { _inByteVector.add(new InByte2()); _inByteBack = _inByteVector.Back(); _inByteBack.Init(buffer, size); } void DeleteByteStream() { _inByteVector.DeleteBack(); if (!_inByteVector.isEmpty()) _inByteBack = _inByteVector.Back(); } static boolean TestSignatureCandidate(byte [] testBytes, int off) { for (int i = 0; i < kSignatureSize; i++) { // System.out.println(" " + i + ":" + testBytes[i] + " " + kSignature[i]); if (testBytes[i + off] != kSignature[i]) return false; } return true; } int ReadDirect(IInStream stream, // IInStream *stream, byte [] data, // void *data, int off, int size // UInt32 size, ) throws IOException { int realProcessedSize = StreamUtils.ReadStream(stream, data, off, size); if (realProcessedSize != -1) _position += realProcessedSize; return realProcessedSize; } int ReadDirect(byte [] data, int size) throws IOException { return ReadDirect(_stream, data, 0, size); } int SafeReadDirectUInt32() throws IOException { int val = 0; byte [] b = new byte[4]; int realProcessedSize = ReadDirect(b, 4); if (realProcessedSize != 4) throw new IOException("Unexpected End Of Archive"); // throw CInArchiveException(CInArchiveException::kUnexpectedEndOfArchive); for (int i = 0; i < 4; i++) { val |= ((b[i] & 0xff) << (8 * i)); } return val; } int ReadUInt32() throws IOException { int value = 0; for (int i = 0; i < 4; i++) { int b = ReadByte(); value |= ((b) << (8 * i)); } return value; } long ReadUInt64() throws IOException { long value = 0; for (int i = 0; i < 8; i++) { int b = ReadByte(); value |= (((long)b) << (8 * i)); } return value; } int ReadBytes(byte data[], int size) { if (!_inByteBack.ReadBytes(data, size)) return HRESULT.E_FAIL; return HRESULT.S_OK; } int ReadByte() throws IOException { return _inByteBack.ReadByte(); } long SafeReadDirectUInt64() throws IOException { long val = 0; byte [] b = new byte[8]; int realProcessedSize = ReadDirect(b, 8); if (realProcessedSize != 8) throw new IOException("Unexpected End Of Archive"); // throw CInArchiveException(CInArchiveException::kUnexpectedEndOfArchive); for (int i = 0; i < 8; i++) { val |= ((long)(b[i] & 0xFF) << (8 * i)); } return val; } char ReadWideCharLE() throws IOException { int b1 = _inByteBack.ReadByte(); int b2 = _inByteBack.ReadByte(); char c = (char)(((char)(b2) << 8) + b1); return c; } long ReadNumber() throws IOException { int firstByte = ReadByte(); int mask = 0x80; long value = 0; for (int i = 0; i < 8; i++) { if ((firstByte & mask) == 0) { long highPart = firstByte & (mask - 1); value += (highPart << (i * 8)); return value; } int b = ReadByte(); if (b < 0) throw new IOException("ReadNumber - Can't read stream"); value |= (((long)b) << (8 * i)); mask >>= 1; } return value; } int ReadNum() throws IOException { // CNum long value64 = ReadNumber(); if (value64 > InArchive.kNumMax) throw new IOException("ReadNum - value > CNum.kNumMax"); // return E_FAIL; return (int)value64; } long ReadID() throws IOException { return ReadNumber(); } int FindAndReadSignature( IInStream stream, // IInStream *stream, long searchHeaderSizeLimit // const UInt64 *searchHeaderSizeLimit ) throws IOException { _position = _arhiveBeginStreamPosition; stream.Seek(_arhiveBeginStreamPosition,IInStream.STREAM_SEEK_SET); byte [] signature = new byte[kSignatureSize]; int processedSize = ReadDirect(stream, signature, 0, kSignatureSize); if(processedSize != kSignatureSize) return HRESULT.S_FALSE; if (TestSignatureCandidate(signature,0)) return HRESULT.S_OK; // SFX support ByteBuffer byteBuffer = new ByteBuffer(); final int kBufferSize = (1 << 16); byteBuffer.SetCapacity(kBufferSize); byte [] buffer = byteBuffer.data(); int numPrevBytes = kSignatureSize - 1; System.arraycopy(signature,1,buffer,0,numPrevBytes); long curTestPos = _arhiveBeginStreamPosition + 1; for (;;) { if (searchHeaderSizeLimit != -1) if (curTestPos - _arhiveBeginStreamPosition > searchHeaderSizeLimit) break; int numReadBytes = kBufferSize - numPrevBytes; // RINOK(ReadDirect(stream, buffer + numPrevBytes, numReadBytes, &processedSize)); processedSize = ReadDirect(stream, buffer,numPrevBytes, numReadBytes); if (processedSize == -1) return HRESULT.S_FALSE; int numBytesInBuffer = numPrevBytes + processedSize; if (numBytesInBuffer < kSignatureSize) break; int numTests = numBytesInBuffer - kSignatureSize + 1; for(int pos = 0; pos < numTests; pos++, curTestPos++) { if (TestSignatureCandidate(buffer , pos)) { _arhiveBeginStreamPosition = curTestPos; _position = curTestPos + kSignatureSize; stream.Seek(_position, IInStream.STREAM_SEEK_SET); return HRESULT.S_OK; } } numPrevBytes = numBytesInBuffer - numTests; System.arraycopy(buffer,numTests,buffer,0,numPrevBytes); } return HRESULT.S_FALSE; } int SkeepData(long size) throws IOException { for (long i = 0; i < size; i++) { int temp = ReadByte(); } return HRESULT.S_OK; } int SkeepData() throws IOException { long size = ReadNumber(); return SkeepData(size); } int ReadArchiveProperties(InArchiveInfo archiveInfo) throws IOException { for (;;) { long type = ReadID(); if (type == NID.kEnd) break; SkeepData(); } return HRESULT.S_OK; } int GetNextFolderItem(Folder folder) throws IOException { int numCoders = ReadNum(); folder.Coders.clear(); folder.Coders.Reserve(numCoders); int numInStreams = 0; int numOutStreams = 0; int i; for (i = 0; i < numCoders; i++) { folder.Coders.add(new CoderInfo()); CoderInfo coder = folder.Coders.Back(); for (;;) { coder.AltCoders.add(new AltCoderInfo()); AltCoderInfo altCoder = coder.AltCoders.Back(); int mainByte = ReadByte(); altCoder.MethodID.IDSize = (byte)(mainByte & 0xF); int ret = ReadBytes(altCoder.MethodID.ID, altCoder.MethodID.IDSize); if (ret != HRESULT.S_OK) return ret; if ((mainByte & 0x10) != 0) { coder.NumInStreams = ReadNum(); coder.NumOutStreams = ReadNum(); } else { coder.NumInStreams = 1; coder.NumOutStreams = 1; } if ((mainByte & 0x20) != 0) { int propertiesSize = ReadNum(); altCoder.Properties.SetCapacity(propertiesSize); // RINOK(ReadBytes((Byte *)altCoder.Properties, (size_t)propertiesSize)); ret = ReadBytes(altCoder.Properties.data(), propertiesSize); } if ((mainByte & 0x80) == 0) break; } numInStreams += coder.NumInStreams; numOutStreams += coder.NumOutStreams; } // RINOK(ReadNumber(numBindPairs)); int numBindPairs = numOutStreams - 1; folder.BindPairs.clear(); folder.BindPairs.Reserve(numBindPairs); for (i = 0; i < numBindPairs; i++) { BindPair bindPair = new BindPair(); bindPair.InIndex = ReadNum(); bindPair.OutIndex = ReadNum(); folder.BindPairs.add(bindPair); } int numPackedStreams = numInStreams - numBindPairs; folder.PackStreams.Reserve(numPackedStreams); if (numPackedStreams == 1) { for (int j = 0; j < numInStreams; j++) if (folder.FindBindPairForInStream(j) < 0) { folder.PackStreams.add(j); break; } } else for(i = 0; i < numPackedStreams; i++) { int packStreamInfo = ReadNum(); folder.PackStreams.add(packStreamInfo); } return HRESULT.S_OK; } int WaitAttribute(long attribute) throws IOException { for (;;) { long type = ReadID(); if (type == attribute) return HRESULT.S_OK; if (type == NID.kEnd) return HRESULT.S_FALSE; int ret = SkeepData(); if (ret != HRESULT.S_OK) return ret; } } int Open( IInStream stream, // IInStream *stream long searchHeaderSizeLimit // const UInt64 *searchHeaderSizeLimit ) throws IOException { Close(); _arhiveBeginStreamPosition = stream.Seek(0, IInStream.STREAM_SEEK_CUR); _position = _arhiveBeginStreamPosition; int ret = FindAndReadSignature(stream, searchHeaderSizeLimit); if (ret != HRESULT.S_OK) return ret; _stream = stream; return HRESULT.S_OK; } void Close() throws IOException { if (_stream != null) _stream.close(); // _stream.Release(); _stream = null; } int ReadStreamsInfo( ObjectVector dataVector, long [] dataOffset, LongVector packSizes, BoolVector packCRCsDefined, IntVector packCRCs, ObjectVector folders, IntVector numUnPackStreamsInFolders, LongVector unPackSizes, BoolVector digestsDefined, IntVector digests) throws IOException { for (;;) { long type = ReadID(); switch((int)type) { case NID.kEnd: return HRESULT.S_OK; case NID.kPackInfo: { int result = ReadPackInfo(dataOffset, packSizes, packCRCsDefined, packCRCs); if (result != HRESULT.S_OK) return result; break; } case NID.kUnPackInfo: { int result = ReadUnPackInfo(dataVector, folders); if (result != HRESULT.S_OK) return result; break; } case NID.kSubStreamsInfo: { int result = ReadSubStreamsInfo(folders, numUnPackStreamsInFolders, unPackSizes, digestsDefined, digests); if (result != HRESULT.S_OK) return result; break; } } } } int ReadFileNames(ObjectVector files) throws IOException { for (FileItem file : files) { String name = ""; for (; ; ) { char c = ReadWideCharLE(); if (c == '\0') break; name += c; } file.name = name; } return HRESULT.S_OK; } int ReadBoolVector(int numItems, BoolVector v) throws IOException { v.clear(); v.Reserve(numItems); int b = 0; int mask = 0; for(int i = 0; i < numItems; i++) { if (mask == 0) { b = ReadByte(); mask = 0x80; } v.add((b & mask) != 0); mask >>= 1; } return HRESULT.S_OK; } int ReadBoolVector2(int numItems, BoolVector v) throws IOException { // CBoolVector int allAreDefined = ReadByte(); if (allAreDefined == 0) return ReadBoolVector(numItems, v); v.clear(); v.Reserve(numItems); for (int i = 0; i < numItems; i++) v.add(true); return HRESULT.S_OK; } int ReadHashDigests(int numItems, BoolVector digestsDefined, IntVector digests) throws IOException { int ret = ReadBoolVector2(numItems, digestsDefined); if (ret != HRESULT.S_OK) return ret; digests.clear(); digests.Reserve(numItems); for(int i = 0; i < numItems; i++) { int crc = 0; if (digestsDefined.get(i)) crc = ReadUInt32(); digests.add(crc); } return HRESULT.S_OK; } int ReadPackInfo( long [] dataOffset, // UInt64 &dataOffset, LongVector packSizes, // CRecordVector &packSizes, BoolVector packCRCsDefined, // CRecordVector &packCRCsDefined, IntVector packCRCs) // CRecordVector &packCRCs) throws IOException { dataOffset[0] = ReadNumber(); int numPackStreams = ReadNum(); int ret = WaitAttribute(NID.kSize); if (ret != HRESULT.S_OK) return ret; packSizes.clear(); packSizes.Reserve(numPackStreams); for(int i = 0; i < numPackStreams; i++) // CNum i { long size = ReadNumber(); packSizes.add(size); } long type; for (;;) { type = ReadID(); if (type == NID.kEnd) break; if (type == NID.kCRC) { ret = ReadHashDigests(numPackStreams, packCRCsDefined, packCRCs); if (ret != HRESULT.S_OK) return ret; continue; } ret = SkeepData(); if (ret != HRESULT.S_OK) return ret; } if (packCRCsDefined.isEmpty()) { packCRCsDefined.Reserve(numPackStreams); packCRCsDefined.clear(); packCRCs.Reserve(numPackStreams); packCRCs.clear(); for(int i = 0; i < numPackStreams; i++) { packCRCsDefined.add(false); packCRCs.add(0); } } return HRESULT.S_OK; } int ReadUnPackInfo( ObjectVector dataVector, ObjectVector folders) throws IOException { int ret = WaitAttribute(NID.kFolder); if (ret != HRESULT.S_OK) return ret; int numFolders = ReadNum(); { StreamSwitch streamSwitch = new StreamSwitch(); ret = streamSwitch.Set(this, dataVector); if (ret != HRESULT.S_OK) return ret; folders.clear(); folders.Reserve(numFolders); for(int i = 0; i < numFolders; i++) { folders.add(new Folder()); ret = GetNextFolderItem(folders.Back()); if (ret != HRESULT.S_OK) { streamSwitch.close(); return ret; } } streamSwitch.close(); } ret = WaitAttribute(NID.kCodersUnPackSize); if (ret != HRESULT.S_OK) return ret; for(int i = 0; i < numFolders; i++) { Folder folder = folders.get(i); int numOutStreams = folder.GetNumOutStreams(); folder.UnPackSizes.Reserve(numOutStreams); for(int j = 0; j < numOutStreams; j++) { long unPackSize = ReadNumber(); folder.UnPackSizes.add(unPackSize); } } for (;;) { long type = ReadID(); if (type == NID.kEnd) return HRESULT.S_OK; if (type == NID.kCRC) { BoolVector crcsDefined = new BoolVector(); IntVector crcs = new IntVector(); ret = ReadHashDigests(numFolders, crcsDefined, crcs); if (ret != HRESULT.S_OK) return ret; for(int i = 0; i < numFolders; i++) { Folder folder = folders.get(i); folder.UnPackCRCDefined = crcsDefined.get(i); folder.UnPackCRC = crcs.get(i); } continue; } ret = SkeepData(); if (ret != HRESULT.S_OK) return ret; } } int ReadSubStreamsInfo( ObjectVector folders, IntVector numUnPackStreamsInFolders, LongVector unPackSizes, BoolVector digestsDefined, IntVector digests) throws IOException { numUnPackStreamsInFolders.clear(); numUnPackStreamsInFolders.Reserve(folders.size()); long type; for (;;) { type = ReadID(); if (type == NID.kNumUnPackStream) { for(int i = 0; i < folders.size(); i++) { int value = ReadNum(); numUnPackStreamsInFolders.add(value); } continue; } if (type == NID.kCRC || type == NID.kSize) break; if (type == NID.kEnd) break; int ret = SkeepData(); if (ret != HRESULT.S_OK) return ret; } if (numUnPackStreamsInFolders.isEmpty()) for(int i = 0; i < folders.size(); i++) numUnPackStreamsInFolders.add(1); for(int i = 0; i < numUnPackStreamsInFolders.size(); i++) { // v3.13 incorrectly worked with empty folders // v4.07: we check that folder is empty int numSubstreams = numUnPackStreamsInFolders.get(i); if (numSubstreams == 0) continue; long sum = 0; for (int j = 1; j < numSubstreams; j++) { long size; if (type == NID.kSize) { size = ReadNumber(); unPackSizes.add(size); sum += size; } } unPackSizes.add(folders.get(i).GetUnPackSize() - sum); } if (type == NID.kSize) { type = ReadID(); } int numDigests = 0; int numDigestsTotal = 0; for(int i = 0; i < folders.size(); i++) { int numSubstreams = numUnPackStreamsInFolders.get(i); if (numSubstreams != 1 || !folders.get(i).UnPackCRCDefined) numDigests += numSubstreams; numDigestsTotal += numSubstreams; } for (;;) { if (type == NID.kCRC) { BoolVector digestsDefined2 = new BoolVector(); IntVector digests2 = new IntVector(); int ret = ReadHashDigests(numDigests, digestsDefined2, digests2); if (ret != HRESULT.S_OK) return ret; int digestIndex = 0; for (int i = 0; i < folders.size(); i++) { int numSubstreams = numUnPackStreamsInFolders.get(i); Folder folder = folders.get(i); if (numSubstreams == 1 && folder.UnPackCRCDefined) { digestsDefined.add(true); digests.add(folder.UnPackCRC); } else for (int j = 0; j < numSubstreams; j++, digestIndex++) { digestsDefined.add(digestsDefined2.get(digestIndex)); digests.add(digests2.get(digestIndex)); } } } else if (type == NID.kEnd) { if (digestsDefined.isEmpty()) { digestsDefined.clear(); digests.clear(); for (int i = 0; i < numDigestsTotal; i++) { digestsDefined.add(false); digests.add(0); } } return HRESULT.S_OK; } else { int ret = SkeepData(); if (ret != HRESULT.S_OK) return ret; } type = ReadID(); } } static final long SECS_BETWEEN_EPOCHS = 11644473600L; static final long SECS_TO_100NS = 10000000L; /* 10^7 */ static long FileTimeToLong(int dwHighDateTime, int dwLowDateTime) { // The FILETIME structure is a 64-bit value representing the number of 100-nanosecond intervals since January 1 long tm = dwHighDateTime; tm <<=32; tm |= (dwLowDateTime & 0xFFFFFFFFL); return (tm - (SECS_BETWEEN_EPOCHS * SECS_TO_100NS)) / (10000L); /* now convert to milliseconds */ } int ReadTime(ObjectVector dataVector, ObjectVector files, long type) throws IOException { BoolVector boolVector = new BoolVector(); int ret = ReadBoolVector2(files.size(), boolVector); if (ret != HRESULT.S_OK) return ret; StreamSwitch streamSwitch = new StreamSwitch(); ret = streamSwitch.Set(this, dataVector); if (ret != HRESULT.S_OK) { streamSwitch.close(); return ret; } for(int i = 0; i < files.size(); i++) { FileItem file = files.get(i); int low = 0; int high = 0; boolean defined = boolVector.get(i); if (defined) { low = ReadUInt32(); high = ReadUInt32(); } switch((int)type) { case NID.kCreationTime: // file.IsCreationTimeDefined = defined; if (defined) file.CreationTime = FileTimeToLong(high,low); break; case NID.kLastWriteTime: // file.IsLastWriteTimeDefined = defined; if (defined) file.LastWriteTime = FileTimeToLong(high,low); break; case NID.kLastAccessTime: // file.IsLastAccessTimeDefined = defined; if (defined) file.LastAccessTime = FileTimeToLong(high,low); break; } } streamSwitch.close(); return HRESULT.S_OK; } int ReadAndDecodePackedStreams(long baseOffset, long [] dataOffset, ObjectVector dataVector // CObjectVector &dataVector ) throws IOException { LongVector packSizes = new LongVector(); // CRecordVector packSizes; BoolVector packCRCsDefined = new BoolVector(); // CRecordVector packCRCsDefined; IntVector packCRCs = new IntVector(); // CRecordVector packCRCs; ObjectVector folders = new ObjectVector<>(); IntVector numUnPackStreamsInFolders = new IntVector(); LongVector unPackSizes = new LongVector(); BoolVector digestsDefined = new BoolVector(); IntVector digests = new IntVector(); int ret = ReadStreamsInfo(null, dataOffset, packSizes, packCRCsDefined, packCRCs, folders, numUnPackStreamsInFolders, unPackSizes, digestsDefined, digests); // database.ArchiveInfo.DataStartPosition2 += database.ArchiveInfo.StartPositionAfterHeader; int packIndex = 0; Decoder decoder = new Decoder(false); // _ST_MODE long dataStartPos = baseOffset + dataOffset[0]; for (Folder folder : folders) { dataVector.add(new ByteBuffer()); ByteBuffer data = dataVector.Back(); long unPackSize = folder.GetUnPackSize(); if (unPackSize > InArchive.kNumMax) return HRESULT.E_FAIL; if (unPackSize > 0xFFFFFFFFL) return HRESULT.E_FAIL; data.SetCapacity((int) unPackSize); SequentialOutStreamImp2 outStreamSpec = new SequentialOutStreamImp2(); OutputStream outStream = outStreamSpec; outStreamSpec.Init(data.data(), (int) unPackSize); int result = decoder.Decode(_stream, dataStartPos, packSizes, packIndex, // &packSizes[packIndex] folder, outStream, null // _ST_MODE , false, 1 ); if (result != HRESULT.S_OK) return result; if (folder.UnPackCRCDefined) if (!CRC.verifyDigest(folder.UnPackCRC, data.data(), (int) unPackSize)) throw new IOException("Incorrect Header"); // CInArchiveException(CInArchiveException::kIncorrectHeader); for (int j = 0; j < folder.PackStreams.size(); j++) dataStartPos += packSizes.get(packIndex++); } return HRESULT.S_OK; } int ReadDatabase(ArchiveDatabaseEx database) throws IOException { database.Clear(); database.ArchiveInfo.StartPosition = _arhiveBeginStreamPosition; byte [] btmp = new byte[2]; int realProcessedSize = ReadDirect(btmp, 2); if (realProcessedSize != 2) throw new IOException("Unexpected End Of Archive"); // throw CInArchiveException(CInArchiveException::kUnexpectedEndOfArchive); database.ArchiveInfo.ArchiveVersion_Major = btmp[0]; database.ArchiveInfo.ArchiveVersion_Minor = btmp[1]; if (database.ArchiveInfo.ArchiveVersion_Major != kMajorVersion) throw new IOException("Unsupported Version"); CRC crc = new CRC(); int crcFromArchive = SafeReadDirectUInt32(); long nextHeaderOffset = SafeReadDirectUInt64(); long nextHeaderSize = SafeReadDirectUInt64(); int nextHeaderCRC = SafeReadDirectUInt32(); /* #ifdef FORMAT_7Z_RECOVERY ... #endif */ crc.updateUInt64(nextHeaderOffset); crc.updateUInt64(nextHeaderSize); crc.updateUInt32(nextHeaderCRC); database.ArchiveInfo.StartPositionAfterHeader = _position; if (crc.getDigest() != crcFromArchive) throw new IOException("Incorrect Header"); // CInArchiveException(CInArchiveException::kIncorrectHeader); if (nextHeaderSize == 0) return HRESULT.S_OK; if (nextHeaderSize >= 0xFFFFFFFFL) return HRESULT.E_FAIL; _position = _stream.Seek(nextHeaderOffset,IInStream.STREAM_SEEK_CUR); ByteBuffer buffer2 = new ByteBuffer(); buffer2.SetCapacity((int)nextHeaderSize); // SafeReadDirect(buffer2.data(), (int)nextHeaderSize); realProcessedSize = ReadDirect(buffer2.data(), (int)nextHeaderSize); if (realProcessedSize != (int)nextHeaderSize) throw new IOException("Unexpected End Of Archive"); // throw CInArchiveException(CInArchiveException::kUnexpectedEndOfArchive); if (!CRC.verifyDigest(nextHeaderCRC, buffer2.data(), (int) nextHeaderSize)) throw new IOException("Incorrect Header"); // CInArchiveException(CInArchiveException::kIncorrectHeader); StreamSwitch streamSwitch = new StreamSwitch(); streamSwitch.Set(this, buffer2); ObjectVector dataVector = new ObjectVector<>(); // CObjectVector dataVector; for (;;) { long type = ReadID(); if (type == NID.kHeader) break; if (type != NID.kEncodedHeader) throw new IOException("Incorrect Header"); // CInArchiveException(CInArchiveException::kIncorrectHeader); long [] ltmp = new long[1]; ltmp[0] = database.ArchiveInfo.DataStartPosition2; int result = ReadAndDecodePackedStreams( database.ArchiveInfo.StartPositionAfterHeader, ltmp, // database.ArchiveInfo.DataStartPosition2, dataVector); if (result != HRESULT.S_OK) return result; database.ArchiveInfo.DataStartPosition2 = ltmp[0]; if (dataVector.size() == 0) return HRESULT.S_OK; if (dataVector.size() > 1) throw new IOException("Incorrect Header"); // CInArchiveException(CInArchiveException::kIncorrectHeader); streamSwitch.Remove(); streamSwitch.Set(this, dataVector.get(0)); // dataVector.Front() } streamSwitch.close(); return ReadHeader(database); } int ReadHeader(ArchiveDatabaseEx database) throws IOException { long type = ReadID(); if (type == NID.kArchiveProperties) { int ret = ReadArchiveProperties(database.ArchiveInfo); if (ret != HRESULT.S_OK) return ret; type = ReadID(); } ObjectVector dataVector = new ObjectVector<>(); if (type == NID.kAdditionalStreamsInfo) { long [] ltmp = new long[1]; ltmp[0] = database.ArchiveInfo.DataStartPosition2; int result = ReadAndDecodePackedStreams( database.ArchiveInfo.StartPositionAfterHeader, ltmp, // database.ArchiveInfo.DataStartPosition2, dataVector); if (result != HRESULT.S_OK) return result; database.ArchiveInfo.DataStartPosition2 = ltmp[0]; database.ArchiveInfo.DataStartPosition2 += database.ArchiveInfo.StartPositionAfterHeader; type = ReadID(); } LongVector unPackSizes = new LongVector(); BoolVector digestsDefined = new BoolVector(); IntVector digests = new IntVector(); if (type == NID.kMainStreamsInfo) { long [] ltmp = new long[1]; ltmp[0] = database.ArchiveInfo.DataStartPosition; int result = ReadStreamsInfo(dataVector, ltmp, // database.ArchiveInfo.DataStartPosition, database.PackSizes, database.PackCRCsDefined, database.PackCRCs, database.Folders, database.NumUnPackStreamsVector, unPackSizes, digestsDefined, digests); if (result != HRESULT.S_OK) return result; database.ArchiveInfo.DataStartPosition = ltmp[0]; database.ArchiveInfo.DataStartPosition += database.ArchiveInfo.StartPositionAfterHeader; type = ReadID(); } else { for(int i = 0; i < database.Folders.size(); i++) { database.NumUnPackStreamsVector.add(1); Folder folder = database.Folders.get(i); unPackSizes.add(folder.GetUnPackSize()); digestsDefined.add(folder.UnPackCRCDefined); digests.add(folder.UnPackCRC); } } database.Files.clear(); if (type == NID.kEnd) return HRESULT.S_OK; if (type != NID.kFilesInfo) throw new IOException("Incorrect Header"); // throw CInArchiveException(CInArchiveException::kIncorrectHeader); int numFiles = ReadNum(); database.Files.Reserve(numFiles); for(int i = 0; i < numFiles; i++) database.Files.add(new FileItem()); database.ArchiveInfo.FileInfoPopIDs.add(NID.kSize); if (!database.PackSizes.isEmpty()) database.ArchiveInfo.FileInfoPopIDs.add(NID.kPackInfo); if (numFiles > 0 && !digests.isEmpty()) database.ArchiveInfo.FileInfoPopIDs.add(NID.kCRC); BoolVector emptyStreamVector = new BoolVector(); emptyStreamVector.Reserve(numFiles); for(int i = 0; i < numFiles; i++) emptyStreamVector.add(false); BoolVector emptyFileVector = new BoolVector(); BoolVector antiFileVector = new BoolVector(); int numEmptyStreams = 0; // int sizePrev = -1; // int posPrev = 0; for (;;) { type = ReadID(); if (type == NID.kEnd) break; long size = ReadNumber(); // sizePrev = size; // posPrev = _inByteBack->GetProcessedSize(); database.ArchiveInfo.FileInfoPopIDs.add(type); switch((int)type) { case NID.kName: { StreamSwitch streamSwitch = new StreamSwitch(); int result = streamSwitch.Set(this, dataVector); if (result != HRESULT.S_OK) return result; result = ReadFileNames(database.Files); streamSwitch.close(); if (result != HRESULT.S_OK) return result; break; } case NID.kWinAttributes: { BoolVector boolVector = new BoolVector(); int result = ReadBoolVector2(database.Files.size(), boolVector); if (result != HRESULT.S_OK) return result; StreamSwitch streamSwitch = new StreamSwitch(); result = streamSwitch.Set(this, dataVector); if (result != HRESULT.S_OK) return result; for(int i = 0; i < numFiles; i++) { FileItem file = database.Files.get(i); file.AreAttributesDefined = boolVector.get(i); if (file.AreAttributesDefined) { file.Attributes = ReadUInt32(); } } streamSwitch.close(); break; } case NID.kStartPos: { BoolVector boolVector = new BoolVector(); int result = ReadBoolVector2(database.Files.size(), boolVector); if (result != HRESULT.S_OK) return result; StreamSwitch streamSwitch = new StreamSwitch(); result = streamSwitch.Set(this, dataVector); if (result != HRESULT.S_OK) return result; for(int i = 0; i < numFiles; i++) { FileItem file = database.Files.get(i); file.IsStartPosDefined = boolVector.get(i); if (file.IsStartPosDefined) { file.StartPos = ReadUInt64(); } } streamSwitch.close(); break; } case NID.kEmptyStream: { int result = ReadBoolVector(numFiles, emptyStreamVector); if (result != HRESULT.S_OK) return result; for (int i = 0; i < emptyStreamVector.size(); i++) if (emptyStreamVector.get(i)) numEmptyStreams++; emptyFileVector.Reserve(numEmptyStreams); antiFileVector.Reserve(numEmptyStreams); for (int i = 0; i < numEmptyStreams; i++) { emptyFileVector.add(false); antiFileVector.add(false); } break; } case NID.kEmptyFile: { int result = ReadBoolVector(numEmptyStreams, emptyFileVector); if (result != HRESULT.S_OK) return result; break; } case NID.kAnti: { int result = ReadBoolVector(numEmptyStreams, antiFileVector); if (result != HRESULT.S_OK) return result; break; } case NID.kCreationTime: case NID.kLastWriteTime: case NID.kLastAccessTime: { int result = ReadTime(dataVector, database.Files, type); if (result != HRESULT.S_OK) return result; break; } default: { database.ArchiveInfo.FileInfoPopIDs.DeleteBack(); int result = SkeepData(size); if (result != HRESULT.S_OK) return result; } } } int emptyFileIndex = 0; int sizeIndex = 0; for(int i = 0; i < numFiles; i++) { FileItem file = database.Files.get(i); file.HasStream = !emptyStreamVector.get(i); if(file.HasStream) { file.IsDirectory = false; file.IsAnti = false; file.UnPackSize = unPackSizes.get(sizeIndex); file.FileCRC = digests.get(sizeIndex); file.IsFileCRCDefined = digestsDefined.get(sizeIndex); sizeIndex++; } else { file.IsDirectory = !emptyFileVector.get(emptyFileIndex); file.IsAnti = antiFileVector.get(emptyFileIndex); emptyFileIndex++; file.UnPackSize = 0; file.IsFileCRCDefined = false; } } return HRESULT.S_OK; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/InArchiveInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.LongVector; class InArchiveInfo { public byte ArchiveVersion_Major; public byte ArchiveVersion_Minor; public long StartPosition; public long StartPositionAfterHeader; public long DataStartPosition; public long DataStartPosition2; LongVector FileInfoPopIDs = new LongVector(); void Clear() { FileInfoPopIDs.clear(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/InByte2.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import java.io.IOException; class InByte2 { byte [] _buffer; int _size; int _pos; public void Init(byte [] buffer, int size) { _buffer = buffer; _size = size; _pos = 0; } public int ReadByte() throws IOException { if(_pos >= _size) throw new IOException("CInByte2 - Can't read stream"); return (_buffer[_pos++] & 0xFF); } int ReadBytes2(byte [] data, int size) { int processedSize; for(processedSize = 0; processedSize < size && _pos < _size; processedSize++) data[processedSize] = _buffer[_pos++]; return processedSize; } boolean ReadBytes(byte [] data, int size) { int processedSize = ReadBytes2(data, size); return (processedSize == size); } int GetProcessedSize() { return _pos; } InByte2() { } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/MethodID.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; class MethodID { static public final MethodID k_LZMA = new MethodID(0x3, 0x1, 0x1); static public final MethodID k_PPMD = new MethodID(0x3, 0x4, 0x1); static public final MethodID k_BCJ_X86 = new MethodID(0x3, 0x3, 0x1, 0x3); static public final MethodID k_BCJ = new MethodID(0x3, 0x3, 0x1, 0x3); static public final MethodID k_BCJ2 = new MethodID(0x3, 0x3, 0x1, 0x1B); static public final MethodID k_Deflate = new MethodID(0x4, 0x1, 0x8); static public final MethodID k_Deflate64 = new MethodID(0x4, 0x1, 0x9); static public final MethodID k_BZip2 = new MethodID(0x4, 0x2, 0x2); static public final MethodID k_Copy = new MethodID(0x0); static public final MethodID k_7zAES = new MethodID(0x6, 0xF1, 0x07, 0x01); static final int kMethodIDSize = 15; byte [] ID; byte IDSize; public MethodID() { ID = new byte[kMethodIDSize]; IDSize = 0; } public MethodID(int a) { int size = 1; ID = new byte[size]; IDSize = (byte)size; ID[0] = (byte)a; } public MethodID(int a, int b ,int c) { int size = 3; ID = new byte[size]; IDSize = (byte)size; ID[0] = (byte)a; ID[1] = (byte)b; ID[2] = (byte)c; } public MethodID(int a, int b ,int c, int d) { int size = 4; ID = new byte[size]; IDSize = (byte)size; ID[0] = (byte)a; ID[1] = (byte)b; ID[2] = (byte)c; ID[3] = (byte)d; } public boolean equals(MethodID anObject) { if (IDSize != anObject.IDSize) return false; for(int i = 0; i < IDSize ; i++) { if (ID[i] != anObject.ID[i]) return false; } return true; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZip/StreamSwitch.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ByteBuffer; import com.mucommander.commons.file.impl.sevenzip.provider.Common.ObjectVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; class StreamSwitch { InArchive _archive; boolean _needRemove; public StreamSwitch() { _needRemove = false; } public void close() { Remove(); } void Remove() { if (_needRemove) { _archive.DeleteByteStream(); _needRemove = false; } } void Set(InArchive archive, ByteBuffer byteBuffer) { Set(archive, byteBuffer.data(), byteBuffer.GetCapacity()); } void Set(InArchive archive, byte [] data, int size) { Remove(); _archive = archive; _archive.AddByteStream(data, size); _needRemove = true; } int Set(InArchive archive, ObjectVector dataVector) throws java.io.IOException { Remove(); int external = archive.ReadByte(); if (external != 0) { int dataIndex = archive.ReadNum(); Set(archive, dataVector.get(dataIndex)); } return HRESULT.S_OK; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Archive/SevenZipEntry.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive; public class SevenZipEntry { long LastWriteTime; long UnPackSize; long PackSize; int Attributes; long FileCRC; boolean IsDirectory; String Name; String Methods; long Position; public SevenZipEntry( String name, long packSize, long unPackSize, long crc, long lastWriteTime, long position, boolean isDir, int att, String methods) { this.Name = name; this.PackSize = packSize; this.UnPackSize = unPackSize; this.FileCRC = crc; this.LastWriteTime = lastWriteTime; this.Position = position; this.IsDirectory = isDir; this.Attributes = att; this.Methods = methods; } public long getCompressedSize() { return PackSize; } public long getSize() { return UnPackSize; } public long getCrc() { return FileCRC; } public String getName() { return Name; } public long getTime() { return LastWriteTime; } public long getPosition() { return Position; } public boolean isDirectory() { return IsDirectory; } static final String kEmptyAttributeChar = "."; static final String kDirectoryAttributeChar = "D"; static final String kReadonlyAttributeChar = "R"; static final String kHiddenAttributeChar = "H"; static final String kSystemAttributeChar = "S"; static final String kArchiveAttributeChar = "A"; static public final int FILE_ATTRIBUTE_READONLY = 0x00000001 ; static public final int FILE_ATTRIBUTE_HIDDEN = 0x00000002 ; static public final int FILE_ATTRIBUTE_SYSTEM = 0x00000004 ; static public final int FILE_ATTRIBUTE_DIRECTORY = 0x00000010; static public final int FILE_ATTRIBUTE_ARCHIVE = 0x00000020 ; public String getAttributesString() { String ret = ""; ret += ((Attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 || IsDirectory) ? kDirectoryAttributeChar: kEmptyAttributeChar; ret += ((Attributes & FILE_ATTRIBUTE_READONLY) != 0)? kReadonlyAttributeChar: kEmptyAttributeChar; ret += ((Attributes & FILE_ATTRIBUTE_HIDDEN) != 0) ? kHiddenAttributeChar: kEmptyAttributeChar; ret += ((Attributes & FILE_ATTRIBUTE_SYSTEM) != 0) ? kSystemAttributeChar: kEmptyAttributeChar; ret += ((Attributes & FILE_ATTRIBUTE_ARCHIVE) != 0) ? kArchiveAttributeChar: kEmptyAttributeChar; return ret; } public String getMethods() { return Methods; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ArchiveExtractCallback.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; import java.io.File; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IArchiveExtractCallback; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZipEntry; public class ArchiveExtractCallback implements IArchiveExtractCallback // , ICryptoGetTextPassword, { static class OutputStream extends java.io.OutputStream { java.io.RandomAccessFile file; public OutputStream(java.io.RandomAccessFile f) { file = f; } public void close() throws java.io.IOException { file.close(); file = null; } /* public void flush() throws java.io.IOException { file.flush(); } */ public void write(byte[] b) throws java.io.IOException { file.write(b); } public void write(byte[] b, int off, int len) throws java.io.IOException { file.write(b,off,len); } public void write(int b) throws java.io.IOException { file.write(b); } } public int SetTotal(long size) { return HRESULT.S_OK; } public int SetCompleted(long completeValue) { return HRESULT.S_OK; } public void PrintString(String str) { System.out.print(str); } public void PrintNewLine() { System.out.println(); } public int PrepareOperation(int askExtractMode) { System.out.println("askExtractMode = " + askExtractMode); _extractMode = false; switch (askExtractMode) { case IInArchive.NExtract_NAskMode_kExtract: _extractMode = true; } System.out.println("here1"); switch (askExtractMode) { case IInArchive.NExtract_NAskMode_kExtract: PrintString("Extracting "); break; case IInArchive.NExtract_NAskMode_kTest: PrintString("Testing "); break; case IInArchive.NExtract_NAskMode_kSkip: PrintString("Skipping "); break; } System.out.println("here2"); PrintString(_filePath); return HRESULT.S_OK; } public int SetOperationResult(int operationResult) throws java.io.IOException { switch(operationResult) { case IInArchive.NExtract_NOperationResult_kOK: break; default: { NumErrors++; PrintString(" "); switch(operationResult) { case IInArchive.NExtract_NOperationResult_kUnSupportedMethod: PrintString("Unsupported Method"); break; case IInArchive.NExtract_NOperationResult_kCRCError: PrintString("CRC Failed"); break; case IInArchive.NExtract_NOperationResult_kDataError: PrintString("Data Error"); break; default: PrintString("Unknown Error"); } } } /* if(_outFileStream != null && _processedFileInfo.UTCLastWriteTimeIsDefined) _outFileStreamSpec->File.SetLastWriteTime(&_processedFileInfo.UTCLastWriteTime); */ if (_outFileStream != null) _outFileStream.close(); // _outFileStream.Release(); /* if (_extractMode && _processedFileInfo.AttributesAreDefined) NFile::NDirectory::MySetFileAttributes(_diskFilePath, _processedFileInfo.Attributes); */ PrintNewLine(); return HRESULT.S_OK; } java.io.OutputStream _outFileStream; public int GetStream(int index, java.io.OutputStream [] outStream, int askExtractMode) { outStream[0] = null; SevenZipEntry item = _archiveHandler.getEntry(index); _filePath = item.getName(); File file = new File(_filePath); switch (askExtractMode) { case IInArchive.NExtract_NAskMode_kTest: return HRESULT.S_OK; case IInArchive.NExtract_NAskMode_kExtract: try { isDirectory = item.isDirectory(); if (isDirectory) { if (file.isDirectory()) { return HRESULT.S_OK; } if (file.mkdirs()) return HRESULT.S_OK; else return HRESULT.S_FALSE; } File dirs = file.getParentFile(); if (dirs != null) { if (!dirs.isDirectory()) if (!dirs.mkdirs()) return HRESULT.S_FALSE; } long pos = item.getPosition(); if (pos == -1) { file.delete(); } java.io.RandomAccessFile outStr = new java.io.RandomAccessFile(_filePath,"rw"); if (pos != -1) { outStr.seek(pos); } outStream[0] = new OutputStream(outStr); } catch (java.io.IOException e) { return HRESULT.S_FALSE; } return HRESULT.S_OK; } // other case : skip ... return HRESULT.S_OK; } com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive _archiveHandler; // IInArchive String _filePath; // name inside arcvhive String _diskFilePath; // full path to file on disk public long NumErrors; boolean PasswordIsDefined; String Password; boolean _extractMode; boolean isDirectory; public ArchiveExtractCallback() { PasswordIsDefined = false; } public void Init(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive archiveHandler) { NumErrors = 0; _archiveHandler = archiveHandler; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Common/InBuffer.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common; public class InBuffer { int _bufferPos; int _bufferLimit; byte [] _bufferBase; java.io.InputStream _stream = null; // CMyComPtr long _processedSize; int _bufferSize; boolean _wasFinished; public InBuffer() { } // ~CInBuffer() { Free(); } public void Create(int bufferSize) { final int kMinBlockSize = 1; if (bufferSize < kMinBlockSize) bufferSize = kMinBlockSize; if (_bufferBase != null && _bufferSize == bufferSize) return ; Free(); _bufferSize = bufferSize; _bufferBase = new byte[bufferSize]; } void Free() { _bufferBase = null; } public void SetStream(java.io.InputStream stream) { // ISequentialInStream _stream = stream; } public void Init() { _processedSize = 0; _bufferPos = 0; // = _bufferBase; _bufferLimit = 0; // _buffer; _wasFinished = false; } public void ReleaseStream() throws java.io.IOException { if (_stream != null) _stream.close(); // _stream.Release(); _stream = null; } public int read() throws java.io.IOException { if(_bufferPos >= _bufferLimit) return ReadBlock2(); return _bufferBase[_bufferPos++] & 0xFF; } public boolean ReadBlock() throws java.io.IOException { if (_wasFinished) return false; _processedSize += _bufferPos; // (_buffer - _bufferBase); int numProcessedBytes = _stream.read(_bufferBase, 0,_bufferSize); if (numProcessedBytes == -1) numProcessedBytes = 0; // EOF _bufferPos = 0; // _bufferBase; _bufferLimit = numProcessedBytes; // _buffer + numProcessedBytes; _wasFinished = (numProcessedBytes == 0); return (!_wasFinished); } public int ReadBlock2() throws java.io.IOException { if(!ReadBlock()) return -1; // 0xFF; return _bufferBase[_bufferPos++] & 0xFF; } public long GetProcessedSize() { return _processedSize + (_bufferPos); } public boolean WasFinished() { return _wasFinished; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Common/LocalCompressProgressInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; public class LocalCompressProgressInfo implements ICompressProgressInfo { ICompressProgressInfo _progress; boolean _inStartValueIsAssigned; boolean _outStartValueIsAssigned; long _inStartValue; long _outStartValue; public void Init(ICompressProgressInfo progress, long inStartValue, long outStartValue) { _progress = progress; _inStartValueIsAssigned = (inStartValue != ICompressProgressInfo.INVALID); if (_inStartValueIsAssigned) _inStartValue = inStartValue; _outStartValueIsAssigned = (outStartValue != ICompressProgressInfo.INVALID); if (_outStartValueIsAssigned) _outStartValue = outStartValue; } public int SetRatioInfo(long inSize, long outSize) { long inSizeNew, outSizeNew; long inSizeNewPointer; long outSizeNewPointer; if (_inStartValueIsAssigned && inSize != ICompressProgressInfo.INVALID) { inSizeNew = _inStartValue + (inSize); // *inSize inSizeNewPointer = inSizeNew; } else inSizeNewPointer = ICompressProgressInfo.INVALID; if (_outStartValueIsAssigned && outSize != ICompressProgressInfo.INVALID) { outSizeNew = _outStartValue + (outSize); outSizeNewPointer = outSizeNew; } else outSizeNewPointer = ICompressProgressInfo.INVALID; return _progress.SetRatioInfo(inSizeNewPointer, outSizeNewPointer); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Common/LocalProgress.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IProgress; public class LocalProgress implements ICompressProgressInfo { IProgress _progress; boolean _inSizeIsMain; public void Init(IProgress progress, boolean inSizeIsMain) { _progress = progress; _inSizeIsMain = inSizeIsMain; } public int SetRatioInfo(long inSize, long outSize) { return _progress.SetCompleted(_inSizeIsMain ? inSize : outSize); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Common/SequentialOutStreamImp2.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common; public class SequentialOutStreamImp2 extends java.io.OutputStream { byte []_buffer; int _size; int _pos; public void Init(byte [] buffer, int size) { _buffer = buffer; _pos = 0; _size = size; } public void write(int b) throws java.io.IOException { throw new java.io.IOException("SequentialOutStreamImp2 - write() not implemented"); } public void write(byte [] data,int off, int size) throws java.io.IOException { for(int i = 0 ; i < size ; i++) { if (_pos < _size) { _buffer[_pos++] = data[off + i]; } else { throw new java.io.IOException("SequentialOutStreamImp2 - can't write"); } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Common/StreamUtils.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common; import java.io.IOException; public class StreamUtils { static public int ReadStream(java.io.InputStream stream, byte [] data,int off, int size) throws IOException { int processedSize = 0; while(size != 0) { int processedSizeLoc = stream.read(data,off + processedSize,size); if (processedSizeLoc > 0) { processedSize += processedSizeLoc; size -= processedSizeLoc; } if (processedSizeLoc == -1) { if (processedSize > 0) return processedSize; return -1; // EOF } } return processedSize; } // HRESULT WriteStream(ISequentialOutStream *stream, const void *data, UInt32 size, UInt32 *processedSize); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/Branch/BCJ2_x86_Decoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.Branch; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder2; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.InBuffer; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZ.OutWindow; public class BCJ2_x86_Decoder implements ICompressCoder2 { public static final int kNumMoveBits = 5; InBuffer _mainInStream = new InBuffer(); InBuffer _callStream = new InBuffer(); InBuffer _jumpStream = new InBuffer(); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder _statusE8Decoder[] = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder[256]; com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder _statusE9Decoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder(kNumMoveBits); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder _statusJccDecoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder(kNumMoveBits); OutWindow _outStream = new OutWindow(); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder _rangeDecoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder(); // static final boolean IsJcc(int b0, int b1) { // return ((b0 == 0x0F) && ((b1 & 0xF0) == 0x80)); // } int CodeReal( RecordVector inStreams, Object useless1, // const UInt64 ** /* inSizes */, int numInStreams, RecordVector outStreams, Object useless2, // const UInt64 ** /* outSizes */, int numOutStreams, ICompressProgressInfo progress) throws java.io.IOException { if (numInStreams != 4 || numOutStreams != 1) return HRESULT.E_INVALIDARG; _mainInStream.Create(1 << 16); _callStream.Create(1 << 20); _jumpStream.Create(1 << 16); _rangeDecoder.Create(1 << 20); _outStream.Create(1 << 16); _mainInStream.SetStream(inStreams.get(0)); _callStream.SetStream(inStreams.get(1)); _jumpStream.SetStream(inStreams.get(2)); _rangeDecoder.SetStream(inStreams.get(3)); _outStream.SetStream(outStreams.get(0)); _mainInStream.Init(); _callStream.Init(); _jumpStream.Init(); _rangeDecoder.Init(); _outStream.Init(); for (int i = 0; i < 256; i++) { _statusE8Decoder[i] = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitDecoder(kNumMoveBits); _statusE8Decoder[i].Init(); } _statusE9Decoder.Init(); _statusJccDecoder.Init(); int prevByte = 0; int processedBytes = 0; for (;;) { if (processedBytes > (1 << 20) && progress != null) { long nowPos64 = _outStream.GetProcessedSize(); int res = progress.SetRatioInfo(ICompressProgressInfo.INVALID, nowPos64); if (res != HRESULT.S_OK) return res; processedBytes = 0; } processedBytes++; int b = _mainInStream.read(); if (b == -1) return Flush(); _outStream.WriteByte(b); // System.out.println("0:"+b); // if ((b != 0xE8) && (b != 0xE9) && (!IsJcc(prevByte, b))) { if ((b != 0xE8) && (b != 0xE9) && (!((prevByte == 0x0F) && ((b & 0xF0) == 0x80)))) { prevByte = b; continue; } boolean status; if (b == 0xE8) status = (_statusE8Decoder[prevByte].Decode(_rangeDecoder) == 1); else if (b == 0xE9) status = (_statusE9Decoder.Decode(_rangeDecoder) == 1); else status = (_statusJccDecoder.Decode(_rangeDecoder) == 1); if (status) { int src; if (b == 0xE8) { int b0 = _callStream.read(); // if(b0 == -1) return HRESULT.S_FALSE; src = b0 << 24; b0 = _callStream.read(); // if(b0 == -1) return HRESULT.S_FALSE; src |= b0 << 16; b0 = _callStream.read(); // if(b0 == -1) return HRESULT.S_FALSE; src |= b0 << 8; b0 = _callStream.read(); if(b0 == -1) return HRESULT.S_FALSE; src |= b0; } else { int b0 = _jumpStream.read(); // if(b0 == -1) return HRESULT.S_FALSE; src = b0 << 24; b0 = _jumpStream.read(); // if(b0 == -1) return HRESULT.S_FALSE; src |= b0 << 16; b0 = _jumpStream.read(); // if(b0 == -1) return HRESULT.S_FALSE; src |= b0 << 8; b0 = _jumpStream.read(); if(b0 == -1) return HRESULT.S_FALSE; src |= b0; } int dest = src - ((int)_outStream.GetProcessedSize() + 4) ; _outStream.WriteByte(dest); _outStream.WriteByte((dest >> 8)); _outStream.WriteByte((dest >> 16)); _outStream.WriteByte((dest >> 24)); prevByte = (dest >> 24) & 0xFF; processedBytes += 4; } else prevByte = b; } } public int Flush() throws java.io.IOException { _outStream.Flush(); return HRESULT.S_OK; } public int Code( RecordVector inStreams, // ISequentialInStream **inStreams, Object useless_inSizes, // const UInt64 ** /* inSizes */, int numInStreams, RecordVector outStreams, // ISequentialOutStream **outStreams Object useless_outSizes, // const UInt64 ** /* outSizes */, int numOutStreams, ICompressProgressInfo progress) throws java.io.IOException { try { return CodeReal(inStreams, useless_inSizes, numInStreams, outStreams, useless_outSizes,numOutStreams, progress); } catch(java.io.IOException e) { throw e; } finally { ReleaseStreams(); } } void ReleaseStreams() throws java.io.IOException { _mainInStream.ReleaseStream(); _callStream.ReleaseStream(); _jumpStream.ReleaseStream(); _rangeDecoder.ReleaseStream(); _outStream.ReleaseStream(); } public void close() throws java.io.IOException { ReleaseStreams(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/Branch/BCJ_x86_Decoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.Branch; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressFilter; public class BCJ_x86_Decoder implements ICompressFilter { // struct CBranch86 - begin int [] _prevMask = new int[1]; // UInt32 int [] _prevPos = new int[1]; // UInt32 void x86Init() { _prevMask[0] = 0; _prevPos[0] = -5; } // struct CBranch86 - end static final boolean [] kMaskToAllowedStatus = {true, true, true, false, true, false, false, false }; static final int [] kMaskToBitNumber = {0, 1, 2, 2, 3, 3, 3, 3}; static boolean Test86MSByte(int b) { return ((b) == 0 || (b) == 0xFF); } static int x86_Convert(byte [] buffer, int endPos, int nowPos, int [] prevMask, int [] prevPos, boolean encoding) { int bufferPos = 0; int limit; if (endPos < 5) return 0; if (nowPos - prevPos[0] > 5) prevPos[0] = nowPos - 5; limit = endPos - 5; while(bufferPos <= limit) { int b = (buffer[bufferPos] & 0xFF); int offset; if (b != 0xE8 && b != 0xE9) { bufferPos++; continue; } offset = (nowPos + bufferPos - prevPos[0]); prevPos[0] = (nowPos + bufferPos); if (offset > 5) prevMask[0] = 0; else { for (int i = 0; i < offset; i++) { prevMask[0] &= 0x77; prevMask[0] <<= 1; } } b = (buffer[bufferPos + 4] & 0xFF); if (Test86MSByte(b) && kMaskToAllowedStatus[(prevMask[0] >> 1) & 0x7] && (prevMask[0] >>> 1) < 0x10) { int src = (b << 24) | ((buffer[bufferPos + 3] & 0xFF) << 16) | ((buffer[bufferPos + 2] & 0xFF) << 8) | (buffer[bufferPos + 1] & 0xFF); int dest; for (;;) { int index; if (encoding) dest = (nowPos + bufferPos + 5) + src; else dest = src - (nowPos + bufferPos + 5); if (prevMask[0] == 0) break; index = kMaskToBitNumber[prevMask[0] >>> 1]; b = ((dest >> (24 - index * 8)) & 0xFF); if (!Test86MSByte(b)) break; src = dest ^ ((1 << (32 - index * 8)) - 1); } buffer[bufferPos + 4] = (byte)(~(((dest >> 24) & 1) - 1)); buffer[bufferPos + 3] = (byte)(dest >> 16); buffer[bufferPos + 2] = (byte)(dest >> 8); buffer[bufferPos + 1] = (byte)dest; bufferPos += 5; prevMask[0] = 0; } else { bufferPos++; prevMask[0] |= 1; if (Test86MSByte(b)) prevMask[0] |= 0x10; } } return bufferPos; } public int SubFilter(byte [] data, int size) { return x86_Convert(data, size, _bufferPos, _prevMask, _prevPos, false); } public void SubInit() { x86Init(); } int _bufferPos; // UInt32 // ICompressFilter interface public int Init() { _bufferPos = 0; SubInit(); return HRESULT.S_OK; } public int Filter(byte [] data, int size) { int processedSize = SubFilter(data, size); _bufferPos += processedSize; return processedSize; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/Copy/Decoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.Copy; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; public class Decoder implements ICompressCoder { static final int kBufferSize = 1 << 17; public int Code( java.io.InputStream inStream, // , ISequentialInStream java.io.OutputStream outStream, // ISequentialOutStream long outSize, ICompressProgressInfo progress) throws java.io.IOException { byte [] _buffer = new byte[kBufferSize]; long TotalSize = 0; for (;;) { int realProcessedSize; int size = kBufferSize; if (outSize != -1) // NULL if (size > (outSize - TotalSize)) size = (int)(outSize - TotalSize); realProcessedSize = inStream.read(_buffer, 0,size); if(realProcessedSize == -1) // EOF break; outStream.write(_buffer,0,realProcessedSize); TotalSize += realProcessedSize; if (progress != null) { int res = progress.SetRatioInfo(TotalSize, TotalSize); if (res != HRESULT.S_OK) return res; } } return HRESULT.S_OK; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/LZ/BinTree.java ================================================ // LZ.BinTree package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZ; import java.io.IOException; public class BinTree extends InWindow { int _cyclicBufferPos; int _cyclicBufferSize = 0; int _matchMaxLen; int[] _son; int[] _hash; int _cutValue = 0xFF; int _hashMask; int _hashSizeSum = 0; boolean hashArray = true; static final int kHash2Size = 1 << 10; static final int kHash3Size = 1 << 16; static final int kBT2HashSize = 1 << 16; static final int kStartMaxLen = 1; static final int kHash3Offset = kHash2Size; static final int kEmptyHashValue = 0; static final int kMaxValForNormalize = (1 << 30) - 1; int kNumHashDirectBytes = 0; int kMinMatchCheck = 4; int kFixHashSize = kHash2Size + kHash3Size; public void setType(int numHashBytes) { hashArray = (numHashBytes > 2); if (hashArray) { kNumHashDirectBytes = 0; kMinMatchCheck = 4; kFixHashSize = kHash2Size + kHash3Size; } else { kNumHashDirectBytes = 2; kMinMatchCheck = 2 + 1; kFixHashSize = 0; } } public void Init() throws IOException { super.Init(); for (int i = 0; i < _hashSizeSum; i++) _hash[i] = kEmptyHashValue; _cyclicBufferPos = 0; reduceOffsets(-1); } public void movePos() throws IOException { if (++_cyclicBufferPos >= _cyclicBufferSize) _cyclicBufferPos = 0; super.movePos(); if (_pos == kMaxValForNormalize) Normalize(); } public boolean Create(int historySize, int keepAddBufferBefore, int matchMaxLen, int keepAddBufferAfter) { if (historySize > kMaxValForNormalize - 256) return false; _cutValue = 16 + (matchMaxLen >> 1); int windowReservSize = (historySize + keepAddBufferBefore + matchMaxLen + keepAddBufferAfter) / 2 + 256; super.create(historySize + keepAddBufferBefore, matchMaxLen + keepAddBufferAfter, windowReservSize); _matchMaxLen = matchMaxLen; int cyclicBufferSize = historySize + 1; if (_cyclicBufferSize != cyclicBufferSize) _son = new int[(_cyclicBufferSize = cyclicBufferSize) * 2]; int hs = kBT2HashSize; if (hashArray) { hs = historySize - 1; hs |= (hs >> 1); hs |= (hs >> 2); hs |= (hs >> 4); hs |= (hs >> 8); hs >>= 1; hs |= 0xFFFF; if (hs > (1 << 24)) hs >>= 1; _hashMask = hs; hs++; hs += kFixHashSize; } if (hs != _hashSizeSum) _hash = new int [_hashSizeSum = hs]; return true; } public int GetMatches(int[] distances) throws IOException { int lenLimit; if (_pos + _matchMaxLen <= _streamPos) lenLimit = _matchMaxLen; else { lenLimit = _streamPos - _pos; if (lenLimit < kMinMatchCheck) { movePos(); return 0; } } int offset = 0; int matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0; int cur = _bufferOffset + _pos; int maxLen = kStartMaxLen; // to avoid items for len < hashSize; int hashValue, hash2Value = 0, hash3Value = 0; if (hashArray) { int temp = CrcTable[_bufferBase[cur] & 0xFF] ^ (_bufferBase[cur + 1] & 0xFF); hash2Value = temp & (kHash2Size - 1); temp ^= (_bufferBase[cur + 2] & 0xFF) << 8; hash3Value = temp & (kHash3Size - 1); hashValue = (temp ^ (CrcTable[_bufferBase[cur + 3] & 0xFF] << 5)) & _hashMask; } else hashValue = ((_bufferBase[cur] & 0xFF) ^ ((_bufferBase[cur + 1] & 0xFF) << 8)); int curMatch = _hash[kFixHashSize + hashValue]; if (hashArray) { int curMatch2 = _hash[hash2Value]; int curMatch3 = _hash[kHash3Offset + hash3Value]; _hash[hash2Value] = _pos; _hash[kHash3Offset + hash3Value] = _pos; if (curMatch2 > matchMinPos) if (_bufferBase[_bufferOffset + curMatch2] == _bufferBase[cur]) { distances[offset++] = maxLen = 2; distances[offset++] = _pos - curMatch2 - 1; } if (curMatch3 > matchMinPos) if (_bufferBase[_bufferOffset + curMatch3] == _bufferBase[cur]) { if (curMatch3 == curMatch2) offset -= 2; distances[offset++] = maxLen = 3; distances[offset++] = _pos - curMatch3 - 1; curMatch2 = curMatch3; } if (offset != 0 && curMatch2 == curMatch) { offset -= 2; maxLen = kStartMaxLen; } } _hash[kFixHashSize + hashValue] = _pos; int ptr0 = (_cyclicBufferPos << 1) + 1; int ptr1 = (_cyclicBufferPos << 1); int len0, len1; len0 = len1 = kNumHashDirectBytes; if (kNumHashDirectBytes != 0) { if (curMatch > matchMinPos) { if (_bufferBase[_bufferOffset + curMatch + kNumHashDirectBytes] != _bufferBase[cur + kNumHashDirectBytes]) { distances[offset++] = maxLen = kNumHashDirectBytes; distances[offset++] = _pos - curMatch - 1; } } } int count = _cutValue; while (true) { if (curMatch <= matchMinPos || count-- == 0) { _son[ptr0] = _son[ptr1] = kEmptyHashValue; break; } int delta = _pos - curMatch; int cyclicPos = ((delta <= _cyclicBufferPos) ? (_cyclicBufferPos - delta) : (_cyclicBufferPos - delta + _cyclicBufferSize)) << 1; int pby1 = _bufferOffset + curMatch; int len = Math.min(len0, len1); if (_bufferBase[pby1 + len] == _bufferBase[cur + len]) { while(++len != lenLimit) if (_bufferBase[pby1 + len] != _bufferBase[cur + len]) break; if (maxLen < len) { distances[offset++] = maxLen = len; distances[offset++] = delta - 1; if (len == lenLimit) { _son[ptr1] = _son[cyclicPos]; _son[ptr0] = _son[cyclicPos + 1]; break; } } } if ((_bufferBase[pby1 + len] & 0xFF) < (_bufferBase[cur + len] & 0xFF)) { _son[ptr1] = curMatch; ptr1 = cyclicPos + 1; curMatch = _son[ptr1]; len1 = len; } else { _son[ptr0] = curMatch; ptr0 = cyclicPos; curMatch = _son[ptr0]; len0 = len; } } movePos(); return offset; } public void Skip(int num) throws IOException { do { int lenLimit; if (_pos + _matchMaxLen <= _streamPos) lenLimit = _matchMaxLen; else { lenLimit = _streamPos - _pos; if (lenLimit < kMinMatchCheck) { movePos(); continue; } } int matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0; int cur = _bufferOffset + _pos; int hashValue; if (hashArray) { int temp = CrcTable[_bufferBase[cur] & 0xFF] ^ (_bufferBase[cur + 1] & 0xFF); int hash2Value = temp & (kHash2Size - 1); _hash[hash2Value] = _pos; temp ^= ((_bufferBase[cur + 2] & 0xFF) << 8); int hash3Value = temp & (kHash3Size - 1); _hash[kHash3Offset + hash3Value] = _pos; hashValue = (temp ^ (CrcTable[_bufferBase[cur + 3] & 0xFF] << 5)) & _hashMask; } else hashValue = ((_bufferBase[cur] & 0xFF) ^ ((_bufferBase[cur + 1] & 0xFF) << 8)); int curMatch = _hash[kFixHashSize + hashValue]; _hash[kFixHashSize + hashValue] = _pos; int ptr0 = (_cyclicBufferPos << 1) + 1; int ptr1 = (_cyclicBufferPos << 1); int len0, len1; len0 = len1 = kNumHashDirectBytes; int count = _cutValue; while (true) { if (curMatch <= matchMinPos || count-- == 0) { _son[ptr0] = _son[ptr1] = kEmptyHashValue; break; } int delta = _pos - curMatch; int cyclicPos = ((delta <= _cyclicBufferPos) ? (_cyclicBufferPos - delta) : (_cyclicBufferPos - delta + _cyclicBufferSize)) << 1; int pby1 = _bufferOffset + curMatch; int len = Math.min(len0, len1); if (_bufferBase[pby1 + len] == _bufferBase[cur + len]) { while (++len != lenLimit) if (_bufferBase[pby1 + len] != _bufferBase[cur + len]) break; if (len == lenLimit) { _son[ptr1] = _son[cyclicPos]; _son[ptr0] = _son[cyclicPos + 1]; break; } } if ((_bufferBase[pby1 + len] & 0xFF) < (_bufferBase[cur + len] & 0xFF)) { _son[ptr1] = curMatch; ptr1 = cyclicPos + 1; curMatch = _son[ptr1]; len1 = len; } else { _son[ptr0] = curMatch; ptr0 = cyclicPos; curMatch = _son[ptr0]; len0 = len; } } movePos(); } while (--num != 0); } void NormalizeLinks(int[] items, int numItems, int subValue) { for (int i = 0; i < numItems; i++) { int value = items[i]; if (value <= subValue) value = kEmptyHashValue; else value -= subValue; items[i] = value; } } void Normalize() { int subValue = _pos - _cyclicBufferSize; NormalizeLinks(_son, _cyclicBufferSize * 2, subValue); NormalizeLinks(_hash, _hashSizeSum, subValue); reduceOffsets(subValue); } public void SetCutValue(int cutValue) { _cutValue = cutValue; } private static final int[] CrcTable = new int[256]; static { for (int i = 0; i < 256; i++) { int r = i; for (int j = 0; j < 8; j++) if ((r & 1) != 0) r = (r >>> 1) ^ 0xEDB88320; else r >>>= 1; CrcTable[i] = r; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/LZ/InWindow.java ================================================ // LZ.InWindow package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZ; import java.io.IOException; public class InWindow { public byte[] _bufferBase; // pointer to buffer with data java.io.InputStream _stream; int _posLimit; // offset (from _buffer) of first byte when new block reading must be done boolean _streamEndWasReached; // if (true) then _streamPos shows real end of stream int _pointerToLastSafePosition; public int _bufferOffset; public int _blockSize; // Size of Allocated memory block public int _pos; // offset (from _buffer) of curent byte int _keepSizeBefore; // how many BYTEs must be kept in buffer before _pos int _keepSizeAfter; // how many BYTEs must be kept buffer after _pos public int _streamPos; // offset (from _buffer) of first not read byte from Stream public void moveBlock() { int offset = _bufferOffset + _pos - _keepSizeBefore; // we need one additional byte, since movePos moves on 1 byte. if (offset > 0) offset--; int numBytes = _bufferOffset + _streamPos - offset; // check negative offset ???? for (int i = 0; i < numBytes; i++) _bufferBase[i] = _bufferBase[offset + i]; _bufferOffset -= offset; } public void readBlock() throws IOException { if (_streamEndWasReached) return; while (true) { int size = (0 - _bufferOffset) + _blockSize - _streamPos; if (size == 0) return; int numReadBytes = _stream.read(_bufferBase, _bufferOffset + _streamPos, size); if (numReadBytes == -1) { _posLimit = _streamPos; int pointerToPostion = _bufferOffset + _posLimit; if (pointerToPostion > _pointerToLastSafePosition) _posLimit = _pointerToLastSafePosition - _bufferOffset; _streamEndWasReached = true; return; } _streamPos += numReadBytes; if (_streamPos >= _pos + _keepSizeAfter) _posLimit = _streamPos - _keepSizeAfter; } } void free() { _bufferBase = null; } public void create(int keepSizeBefore, int keepSizeAfter, int keepSizeReserv) { _keepSizeBefore = keepSizeBefore; _keepSizeAfter = keepSizeAfter; int blockSize = keepSizeBefore + keepSizeAfter + keepSizeReserv; if (_bufferBase == null || _blockSize != blockSize) { free(); _blockSize = blockSize; _bufferBase = new byte[_blockSize]; } _pointerToLastSafePosition = _blockSize - keepSizeAfter; } public void SetStream(java.io.InputStream stream) { _stream = stream; } public void ReleaseStream() { _stream = null; } public void Init() throws IOException { _bufferOffset = 0; _pos = 0; _streamPos = 0; _streamEndWasReached = false; readBlock(); } public void movePos() throws IOException { _pos++; if (_pos > _posLimit) { int pointerToPostion = _bufferOffset + _pos; if (pointerToPostion > _pointerToLastSafePosition) moveBlock(); readBlock(); } } public byte getIndexByte(int index) { return _bufferBase[_bufferOffset + _pos + index]; } // index + limit have not to exceed _keepSizeAfter; public int getMatchLen(int index, int distance, int limit) { if (_streamEndWasReached) if ((_pos + index) + limit > _streamPos) limit = _streamPos - (_pos + index); distance++; // Byte *pby = _buffer + (size_t)_pos + index; int pby = _bufferOffset + _pos + index; int i; for (i = 0; i < limit && _bufferBase[pby + i] == _bufferBase[pby + i - distance]; i++); return i; } public int getNumAvailableBytes() { return _streamPos - _pos; } public void reduceOffsets(int subValue) { _bufferOffset += subValue; _posLimit -= subValue; _pos -= subValue; _streamPos -= subValue; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/LZ/OutWindow.java ================================================ // LZ.OutWindow package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZ; import java.io.IOException; public class OutWindow { byte[] _buffer; byte[] _buffer2 = null; int _bufferPos2 = 0; int _pos; int _windowSize = 0; int _streamPos; java.io.OutputStream _stream; long _processedSize; public void Create(int windowSize) { final int kMinBlockSize = 1; if (windowSize < kMinBlockSize) windowSize = kMinBlockSize; if (_buffer == null || _windowSize != windowSize) _buffer = new byte[windowSize]; _windowSize = windowSize; _pos = 0; _streamPos = 0; } public void SetStream(java.io.OutputStream stream) throws IOException { ReleaseStream(); _stream = stream; } public void ReleaseStream() throws IOException { Flush(); _stream = null; } public void SetMemStream(byte [] d) { _buffer2 = d; _bufferPos2 = 0; } public void Init() { Init(false); } public void Init(boolean solid) { _processedSize = 0; if (!solid) { _streamPos = 0; _pos = 0; } } public void Flush() throws IOException { int size = _pos - _streamPos; if (size == 0) return; if (_stream != null) _stream.write(_buffer, _streamPos, size); if (_buffer2 != null) { System.arraycopy(_buffer, _streamPos, _buffer2, _bufferPos2, size); _bufferPos2 += size; } if (_pos >= _windowSize) _pos = 0; _streamPos = _pos; } public void CopyBlock(int distance, int len) throws IOException { int pos = _pos - distance - 1; if (pos < 0) pos += _windowSize; for (; len != 0; len--) { if (pos >= _windowSize) pos = 0; _buffer[_pos++] = _buffer[pos++]; _processedSize++; if (_pos >= _windowSize) Flush(); } } public void PutByte(byte b) throws IOException { _buffer[_pos++] = b; _processedSize++; if (_pos >= _windowSize) Flush(); } public void WriteByte(int b) throws IOException { _buffer[_pos++] = (byte)b; _processedSize++; if (_pos >= _windowSize) Flush(); } public byte GetByte(int distance) { int pos = _pos - distance - 1; if (pos < 0) pos += _windowSize; return _buffer[pos]; } public long GetProcessedSize() { return _processedSize; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/LZMA/Base.java ================================================ // Base.java package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA; public class Base { public static final int kNumRepDistances = 4; public static final int kNumStates = 12; public static int StateInit() { return 0; } public static int StateUpdateChar(int index) { if (index < 4) return 0; if (index < 10) return index - 3; return index - 6; } public static int StateUpdateMatch(int index) { return (index < 7 ? 7 : 10); } public static int StateUpdateRep(int index) { return (index < 7 ? 8 : 11); } public static int StateUpdateShortRep(int index) { return (index < 7 ? 9 : 11); } public static boolean StateIsCharState(int index) { return index < 7; } public static final int kNumPosSlotBits = 6; public static final int kDicLogSizeMin = 0; // public static final int kDicLogSizeMax = 28; // public static final int kDistTableSizeMax = kDicLogSizeMax * 2; public static final int kNumLenToPosStatesBits = 2; // it's for speed optimization public static final int kNumLenToPosStates = 1 << kNumLenToPosStatesBits; public static final int kMatchMinLen = 2; public static int GetLenToPosState(int len) { len -= kMatchMinLen; if (len < kNumLenToPosStates) return len; return kNumLenToPosStates - 1; } public static final int kNumAlignBits = 4; public static final int kAlignTableSize = 1 << kNumAlignBits; public static final int kAlignMask = (kAlignTableSize - 1); public static final int kStartPosModelIndex = 4; public static final int kEndPosModelIndex = 14; public static final int kNumPosModels = kEndPosModelIndex - kStartPosModelIndex; public static final int kNumFullDistances = 1 << (kEndPosModelIndex / 2); public static final int kNumLitPosStatesBitsEncodingMax = 4; public static final int kNumLitContextBitsMax = 8; public static final int kNumPosStatesBitsMax = 4; public static final int kNumPosStatesMax = (1 << kNumPosStatesBitsMax); public static final int kNumPosStatesBitsEncodingMax = 4; public static final int kNumPosStatesEncodingMax = (1 << kNumPosStatesBitsEncodingMax); public static final int kNumLowLenBits = 3; public static final int kNumMidLenBits = 3; public static final int kNumHighLenBits = 8; public static final int kNumLowLenSymbols = 1 << kNumLowLenBits; public static final int kNumMidLenSymbols = 1 << kNumMidLenBits; public static final int kNumLenSymbols = kNumLowLenSymbols + kNumMidLenSymbols + (1 << kNumHighLenBits); public static final int kMatchMaxLen = kMatchMinLen + kNumLenSymbols - 1; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/LZMA/Decoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA; import java.io.IOException; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressProgressInfo; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZ.OutWindow; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitTreeDecoder; /* public ICompressCoder, public ICompressSetDecoderProperties2, public ICompressGetInStreamProcessedSize, #ifdef _ST_MODE public ICompressSetInStream, public ICompressSetOutStreamSize, public ISequentialInStream, #endif */ // OLD CODE public class Decoder implements SevenZip.ICompressCoder , SevenZip.ICompressSetDecoderProperties2 public class Decoder extends java.io.InputStream // _ST_MODE implements com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressCoder , com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetDecoderProperties2 , com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressGetInStreamProcessedSize, // #ifdef _ST_MODE com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetInStream, com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetOutStreamSize // #endif { static class LenDecoder { short[] m_Choice = new short[2]; BitTreeDecoder[] m_LowCoder = new BitTreeDecoder[Base.kNumPosStatesMax]; BitTreeDecoder[] m_MidCoder = new BitTreeDecoder[Base.kNumPosStatesMax]; BitTreeDecoder m_HighCoder = new BitTreeDecoder(Base.kNumHighLenBits); int m_NumPosStates = 0; public void Create(int numPosStates) { for (; m_NumPosStates < numPosStates; m_NumPosStates++) { m_LowCoder[m_NumPosStates] = new BitTreeDecoder(Base.kNumLowLenBits); m_MidCoder[m_NumPosStates] = new BitTreeDecoder(Base.kNumMidLenBits); } } public void Init() { com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_Choice); for (int posState = 0; posState < m_NumPosStates; posState++) { m_LowCoder[posState].Init(); m_MidCoder[posState].Init(); } m_HighCoder.Init(); } public int Decode(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder rangeDecoder, int posState) throws IOException { if (rangeDecoder.DecodeBit(m_Choice, 0) == 0) return m_LowCoder[posState].Decode(rangeDecoder); int symbol = Base.kNumLowLenSymbols; if (rangeDecoder.DecodeBit(m_Choice, 1) == 0) symbol += m_MidCoder[posState].Decode(rangeDecoder); else symbol += Base.kNumMidLenSymbols + m_HighCoder.Decode(rangeDecoder); return symbol; } } static class LiteralDecoder { static class Decoder2 { short[] m_Decoders = new short[0x300]; public void Init() { com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_Decoders); } public byte DecodeNormal(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder rangeDecoder) throws IOException { int symbol = 1; do symbol = (symbol << 1) | rangeDecoder.DecodeBit(m_Decoders, symbol); while (symbol < 0x100); return (byte)symbol; } public byte DecodeWithMatchByte(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder rangeDecoder, byte matchByte) throws IOException { int symbol = 1; do { int matchBit = (matchByte >> 7) & 1; matchByte <<= 1; int bit = rangeDecoder.DecodeBit(m_Decoders, ((1 + matchBit) << 8) + symbol); symbol = (symbol << 1) | bit; if (matchBit != bit) { while (symbol < 0x100) symbol = (symbol << 1) | rangeDecoder.DecodeBit(m_Decoders, symbol); break; } } while (symbol < 0x100); return (byte)symbol; } } Decoder2[] m_Coders; int m_NumPrevBits; int m_NumPosBits; int m_PosMask; public void Create(int numPosBits, int numPrevBits) { if (m_Coders != null && m_NumPrevBits == numPrevBits && m_NumPosBits == numPosBits) return; m_NumPosBits = numPosBits; m_PosMask = (1 << numPosBits) - 1; m_NumPrevBits = numPrevBits; int numStates = 1 << (m_NumPrevBits + m_NumPosBits); m_Coders = new Decoder2[numStates]; for (int i = 0; i < numStates; i++) m_Coders[i] = new Decoder2(); } public void Init() { int numStates = 1 << (m_NumPrevBits + m_NumPosBits); for (int i = 0; i < numStates; i++) m_Coders[i].Init(); } Decoder2 GetDecoder(int pos, byte prevByte) { return m_Coders[((pos & m_PosMask) << m_NumPrevBits) + ((prevByte & 0xFF) >>> (8 - m_NumPrevBits))]; } } OutWindow m_OutWindow = new OutWindow(); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder m_RangeDecoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder(); short[] m_IsMatchDecoders = new short[Base.kNumStates << Base.kNumPosStatesBitsMax]; short[] m_IsRepDecoders = new short[Base.kNumStates]; short[] m_IsRepG0Decoders = new short[Base.kNumStates]; short[] m_IsRepG1Decoders = new short[Base.kNumStates]; short[] m_IsRepG2Decoders = new short[Base.kNumStates]; short[] m_IsRep0LongDecoders = new short[Base.kNumStates << Base.kNumPosStatesBitsMax]; BitTreeDecoder[] m_PosSlotDecoder = new BitTreeDecoder[Base.kNumLenToPosStates]; short[] m_PosDecoders = new short[Base.kNumFullDistances - Base.kEndPosModelIndex]; BitTreeDecoder m_PosAlignDecoder = new BitTreeDecoder(Base.kNumAlignBits); LenDecoder m_LenDecoder = new LenDecoder(); LenDecoder m_RepLenDecoder = new LenDecoder(); LiteralDecoder m_LiteralDecoder = new LiteralDecoder(); int m_DictionarySize = -1; int m_DictionarySizeCheck = -1; int m_posStateMask; public Decoder() { for (int i = 0; i < Base.kNumLenToPosStates; i++) m_PosSlotDecoder[i] = new BitTreeDecoder(Base.kNumPosSlotBits); } boolean SetDictionarySize(int dictionarySize) { if (dictionarySize < 0) return false; if (m_DictionarySize != dictionarySize) { m_DictionarySize = dictionarySize; m_DictionarySizeCheck = Math.max(m_DictionarySize, 1); m_OutWindow.Create(Math.max(m_DictionarySizeCheck, (1 << 12))); m_RangeDecoder.Create(1 << 20); } return true; } boolean setLcLpPb(int lc, int lp, int pb) { if (lc > Base.kNumLitContextBitsMax || lp > 4 || pb > Base.kNumPosStatesBitsMax) return false; m_LiteralDecoder.Create(lp, lc); int numPosStates = 1 << pb; m_LenDecoder.Create(numPosStates); m_RepLenDecoder.Create(numPosStates); m_posStateMask = numPosStates - 1; return true; } public long GetInStreamProcessedSize() { throw new UnknownError("GetInStreamProcessedSize"); // return m_RangeDecoder.GetProcessedSize(); } public int ReleaseInStream() throws IOException { m_RangeDecoder.ReleaseStream(); return com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT.S_OK; } public int SetInStream(java.io.InputStream inStream) { // Common.ISequentialInStream m_RangeDecoder.SetStream(inStream); return com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT.S_OK; } long _outSize = 0; boolean _outSizeDefined = false; int _remainLen; // -1 means end of stream. // -2 means need Init static final int kLenIdFinished = -1; static final int kLenIdNeedInit = -2; int _rep0; int _rep1; int _rep2; int _rep3; int _state; public int SetOutStreamSize(long outSize /* const UInt64 *outSize*/ ) { _outSizeDefined = (outSize != com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICompressSetOutStreamSize.INVALID_OUTSIZE); if (_outSizeDefined) _outSize = outSize; _remainLen = kLenIdNeedInit; m_OutWindow.Init(); return com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.HRESULT.S_OK; } // #ifdef _ST_MODE public int read() throws java.io.IOException { throw new java.io.IOException("LZMA Decoder - read() not implemented"); } public int read(byte [] data, int off, int size) throws IOException { if (off != 0)throw new java.io.IOException("LZMA Decoder - read(byte [] data, int off != 0, int size)) not implemented"); long startPos = m_OutWindow.GetProcessedSize(); m_OutWindow.SetMemStream(data); int res = CodeSpec(size); if (res != HRESULT.S_OK) throw new IOException("Read - CodeSpec = " + res); res = Flush(); if (res != HRESULT.S_OK) throw new IOException("Read - Flush = " + res); int ret = (int)(m_OutWindow.GetProcessedSize() - startPos); if (ret == 0) ret = -1; return ret; } // #endif // _ST_MODE void Init() { m_OutWindow.Init(false); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_IsMatchDecoders); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_IsRep0LongDecoders); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_IsRepDecoders); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_IsRepG0Decoders); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_IsRepG1Decoders); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_IsRepG2Decoders); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Decoder.InitBitModels(m_PosDecoders); _rep0 = _rep1 = _rep2 = _rep3 = 0; _state = Base.StateInit(); m_LiteralDecoder.Init(); int i; for (i = 0; i < Base.kNumLenToPosStates; i++) m_PosSlotDecoder[i].Init(); m_LenDecoder.Init(); m_RepLenDecoder.Init(); m_PosAlignDecoder.Init(); } public int Flush() throws IOException { m_OutWindow.Flush(); return HRESULT.S_OK; } void ReleaseStreams() throws IOException { m_OutWindow.ReleaseStream(); ReleaseInStream(); } public int CodeReal( java.io.InputStream inStream, // , ISequentialInStream java.io.OutputStream outStream, // ISequentialOutStream long outSize, ICompressProgressInfo progress // useless_progress ) throws IOException { SetInStream(inStream); m_OutWindow.SetStream(outStream); SetOutStreamSize(outSize); for (;;) { int curSize = 1 << 18; int res = CodeSpec(curSize); if (res != HRESULT.S_OK) { return res; } if (_remainLen == kLenIdFinished) break; if (progress != null) { long inSize = m_RangeDecoder.GetProcessedSize(); long nowPos64 = m_OutWindow.GetProcessedSize(); res = progress.SetRatioInfo(inSize, nowPos64); if (res != HRESULT.S_OK) { return res; } } if (_outSizeDefined) if (m_OutWindow.GetProcessedSize() >= _outSize) break; } return Flush(); } public int Code( java.io.InputStream inStream, // , ISequentialInStream java.io.OutputStream outStream, // ISequentialOutStream long outSize, ICompressProgressInfo progress // useless_progress ) throws IOException { int ret = HRESULT.S_FALSE; try { ret = CodeReal(inStream,outStream,outSize,progress); } catch (IOException e) { e.printStackTrace(); // TBD this.Flush(); this.ReleaseStreams(); throw e; } finally { this.Flush(); this.ReleaseStreams(); } return ret; } int CodeSpec(int curSize) throws IOException // UInt32 { if (_outSizeDefined) { long rem = _outSize - m_OutWindow.GetProcessedSize(); if (curSize > rem) curSize = (int)rem; } if (_remainLen == kLenIdFinished) return HRESULT.S_OK; if (_remainLen == kLenIdNeedInit) { m_RangeDecoder.Init(); Init(); _remainLen = 0; } if (curSize == 0) return HRESULT.S_OK; int rep0 = _rep0; int rep1 = _rep1; int rep2 = _rep2; int rep3 = _rep3; int state = _state; byte prevByte; while(_remainLen > 0 && curSize > 0) { prevByte = m_OutWindow.GetByte(rep0); m_OutWindow.PutByte(prevByte); _remainLen--; curSize--; } long nowPos64 = m_OutWindow.GetProcessedSize(); if (nowPos64 == 0) prevByte = 0; else prevByte = m_OutWindow.GetByte(0); while(curSize > 0) { if (m_RangeDecoder.bufferedStream.WasFinished()) return HRESULT.S_FALSE; int posState = (int)nowPos64 & m_posStateMask; if (m_RangeDecoder.DecodeBit(m_IsMatchDecoders, (state << Base.kNumPosStatesBitsMax) + posState) == 0) { LiteralDecoder.Decoder2 decoder2 = m_LiteralDecoder.GetDecoder((int)nowPos64, prevByte); if (!Base.StateIsCharState(state)) prevByte = decoder2.DecodeWithMatchByte(m_RangeDecoder, m_OutWindow.GetByte(rep0)); else prevByte = decoder2.DecodeNormal(m_RangeDecoder); m_OutWindow.PutByte(prevByte); state = Base.StateUpdateChar(state); curSize--; nowPos64++; } else { int len; if (m_RangeDecoder.DecodeBit(m_IsRepDecoders, state) == 1) { len = 0; if (m_RangeDecoder.DecodeBit(m_IsRepG0Decoders, state) == 0) { if (m_RangeDecoder.DecodeBit(m_IsRep0LongDecoders, (state << Base.kNumPosStatesBitsMax) + posState) == 0) { state = Base.StateUpdateShortRep(state); len = 1; } } else { int distance; if (m_RangeDecoder.DecodeBit(m_IsRepG1Decoders, state) == 0) distance = rep1; else { if (m_RangeDecoder.DecodeBit(m_IsRepG2Decoders, state) == 0) distance = rep2; else { distance = rep3; rep3 = rep2; } rep2 = rep1; } rep1 = rep0; rep0 = distance; } if (len == 0) { len = m_RepLenDecoder.Decode(m_RangeDecoder, posState) + Base.kMatchMinLen; state = Base.StateUpdateRep(state); } } else { rep3 = rep2; rep2 = rep1; rep1 = rep0; len = Base.kMatchMinLen + m_LenDecoder.Decode(m_RangeDecoder, posState); state = Base.StateUpdateMatch(state); int posSlot = m_PosSlotDecoder[Base.GetLenToPosState(len)].Decode(m_RangeDecoder); if (posSlot >= Base.kStartPosModelIndex) { int numDirectBits = (posSlot >> 1) - 1; rep0 = ((2 | (posSlot & 1)) << numDirectBits); if (posSlot < Base.kEndPosModelIndex) rep0 += BitTreeDecoder.ReverseDecode(m_PosDecoders, rep0 - posSlot - 1, m_RangeDecoder, numDirectBits); else { rep0 += (m_RangeDecoder.DecodeDirectBits( numDirectBits - Base.kNumAlignBits) << Base.kNumAlignBits); rep0 += m_PosAlignDecoder.ReverseDecode(m_RangeDecoder); if (rep0 < 0) { if (rep0 == -1) break; return HRESULT.S_FALSE; } } } else rep0 = posSlot; } if (rep0 >= nowPos64 || rep0 >= m_DictionarySizeCheck) { // m_OutWindow.Flush(); _remainLen = kLenIdFinished; return HRESULT.S_FALSE; } int locLen = Math.min(len, curSize); // if (!m_OutWindow.CopyBlock(rep0, locLen)) // return HRESULT.S_FALSE; m_OutWindow.CopyBlock(rep0, locLen); prevByte = m_OutWindow.GetByte(0); curSize -= locLen; nowPos64 += locLen; len -= locLen; if (len != 0) { _remainLen = len; break; } } } if (m_RangeDecoder.bufferedStream.WasFinished()) return HRESULT.S_FALSE; _rep0 = rep0; _rep1 = rep1; _rep2 = rep2; _rep3 = rep3; _state = state; return HRESULT.S_OK; } public boolean SetDecoderProperties2(byte[] properties) { if (properties.length < 5) return false; int val = properties[0] & 0xFF; int lc = val % 9; int remainder = val / 9; int lp = remainder % 5; int pb = remainder / 5; int dictionarySize = 0; for (int i = 0; i < 4; i++) dictionarySize += ((int) (properties[1 + i]) & 0xFF) << (i * 8); return setLcLpPb(lc, lp, pb) && SetDictionarySize(dictionarySize); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/LZMA/Encoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA; import java.io.IOException; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.ICodeProgress; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.BitTreeEncoder; public class Encoder { private static final int EMatchFinderTypeBT2 = 0; private static final int EMatchFinderTypeBT4 = 1; static final int kInfinityPrice = 0xFFFFFFF; static byte[] g_FastPos = new byte[1 << 11]; static { int kFastSlots = 22; int c = 2; g_FastPos[0] = 0; g_FastPos[1] = 1; for (int slotFast = 2; slotFast < kFastSlots; slotFast++) { int k = (1 << ((slotFast >> 1) - 1)); for (int j = 0; j < k; j++, c++) g_FastPos[c] = (byte)slotFast; } } static int GetPosSlot(int pos) { if (pos < (1 << 11)) return g_FastPos[pos]; if (pos < (1 << 21)) return (g_FastPos[pos >> 10] + 20); return (g_FastPos[pos >> 20] + 40); } static int GetPosSlot2(int pos) { if (pos < (1 << 17)) return (g_FastPos[pos >> 6] + 12); if (pos < (1 << 27)) return (g_FastPos[pos >> 16] + 32); return (g_FastPos[pos >> 26] + 52); } int _state = Base.StateInit(); byte _previousByte; int[] _repDistances = new int[Base.kNumRepDistances]; void BaseInit() { _state = Base.StateInit(); _previousByte = 0; for (int i = 0; i < Base.kNumRepDistances; i++) _repDistances[i] = 0; } static final int kDefaultDictionaryLogSize = 22; static final int kNumFastBytesDefault = 0x20; static class LiteralEncoder { static class Encoder2 { short[] m_Encoders = new short[0x300]; public void Init() { com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.InitBitModels(m_Encoders); } void Encode(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder rangeEncoder, byte symbol) throws IOException { int context = 1; for (int i = 7; i >= 0; i--) { int bit = ((symbol >> i) & 1); rangeEncoder.Encode(m_Encoders, context, bit); context = (context << 1) | bit; } } void EncodeMatched(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder rangeEncoder, byte matchByte, byte symbol) throws IOException { int context = 1; boolean same = true; for (int i = 7; i >= 0; i--) { int bit = ((symbol >> i) & 1); int state = context; if (same) { int matchBit = ((matchByte >> i) & 1); state += ((1 + matchBit) << 8); same = (matchBit == bit); } rangeEncoder.Encode(m_Encoders, state, bit); context = (context << 1) | bit; } } public int GetPrice(boolean matchMode, byte matchByte, byte symbol) { int price = 0; int context = 1; int i = 7; if (matchMode) { for (; i >= 0; i--) { int matchBit = (matchByte >> i) & 1; int bit = (symbol >> i) & 1; price += com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice(m_Encoders[((1 + matchBit) << 8) + context], bit); context = (context << 1) | bit; if (matchBit != bit) { i--; break; } } } for (; i >= 0; i--) { int bit = (symbol >> i) & 1; price += com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice(m_Encoders[context], bit); context = (context << 1) | bit; } return price; } } Encoder2[] m_Coders; int m_NumPrevBits; int m_NumPosBits; int m_PosMask; public void Create(int numPosBits, int numPrevBits) { if (m_Coders != null && m_NumPrevBits == numPrevBits && m_NumPosBits == numPosBits) return; m_NumPosBits = numPosBits; m_PosMask = (1 << numPosBits) - 1; m_NumPrevBits = numPrevBits; int numStates = 1 << (m_NumPrevBits + m_NumPosBits); m_Coders = new Encoder2[numStates]; for (int i = 0; i < numStates; i++) m_Coders[i] = new Encoder2(); } public void Init() { int numStates = 1 << (m_NumPrevBits + m_NumPosBits); for (int i = 0; i < numStates; i++) m_Coders[i].Init(); } public Encoder2 GetSubCoder(int pos, byte prevByte) { return m_Coders[((pos & m_PosMask) << m_NumPrevBits) + ((prevByte & 0xFF) >>> (8 - m_NumPrevBits))]; } } static class LenEncoder { short[] _choice = new short[2]; BitTreeEncoder[] _lowCoder = new BitTreeEncoder[Base.kNumPosStatesEncodingMax]; BitTreeEncoder[] _midCoder = new BitTreeEncoder[Base.kNumPosStatesEncodingMax]; BitTreeEncoder _highCoder = new BitTreeEncoder(Base.kNumHighLenBits); public LenEncoder() { for (int posState = 0; posState < Base.kNumPosStatesEncodingMax; posState++) { _lowCoder[posState] = new BitTreeEncoder(Base.kNumLowLenBits); _midCoder[posState] = new BitTreeEncoder(Base.kNumMidLenBits); } } public void Init(int numPosStates) { com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.InitBitModels(_choice); for (int posState = 0; posState < numPosStates; posState++) { _lowCoder[posState].Init(); _midCoder[posState].Init(); } _highCoder.Init(); } public void Encode(com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder rangeEncoder, int symbol, int posState) throws IOException { if (symbol < Base.kNumLowLenSymbols) { rangeEncoder.Encode(_choice, 0, 0); _lowCoder[posState].Encode(rangeEncoder, symbol); } else { symbol -= Base.kNumLowLenSymbols; rangeEncoder.Encode(_choice, 0, 1); if (symbol < Base.kNumMidLenSymbols) { rangeEncoder.Encode(_choice, 1, 0); _midCoder[posState].Encode(rangeEncoder, symbol); } else { rangeEncoder.Encode(_choice, 1, 1); _highCoder.Encode(rangeEncoder, symbol - Base.kNumMidLenSymbols); } } } public void SetPrices(int posState, int numSymbols, int[] prices, int st) { int a0 = com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_choice[0]); int a1 = com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_choice[0]); int b0 = a1 + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_choice[1]); int b1 = a1 + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_choice[1]); int i; for (i = 0; i < Base.kNumLowLenSymbols; i++) { if (i >= numSymbols) return; prices[st + i] = a0 + _lowCoder[posState].GetPrice(i); } for (; i < Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; i++) { if (i >= numSymbols) return; prices[st + i] = b0 + _midCoder[posState].GetPrice(i - Base.kNumLowLenSymbols); } for (; i < numSymbols; i++) prices[st + i] = b1 + _highCoder.GetPrice(i - Base.kNumLowLenSymbols - Base.kNumMidLenSymbols); } } public static final int kNumLenSpecSymbols = Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; class LenPriceTableEncoder extends LenEncoder { int[] _prices = new int[Base.kNumLenSymbols< 0) { lenRes = _matchDistances[_numDistancePairs - 2]; if (lenRes == _numFastBytes) lenRes += _matchFinder.getMatchLen(lenRes - 1, _matchDistances[_numDistancePairs - 1], Base.kMatchMaxLen - lenRes); } _additionalOffset++; return lenRes; } void MovePos(int num) throws java.io.IOException { if (num > 0) { _matchFinder.Skip(num); _additionalOffset += num; } } int GetRepLen1Price(int state, int posState) { return com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isRepG0[state]) + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isRep0Long[(state << Base.kNumPosStatesBitsMax) + posState]); } int GetPureRepPrice(int repIndex, int state, int posState) { int price; if (repIndex == 0) { price = com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isRepG0[state]); price += com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRep0Long[(state << Base.kNumPosStatesBitsMax) + posState]); } else { price = com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRepG0[state]); if (repIndex == 1) price += com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isRepG1[state]); else { price += com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRepG1[state]); price += com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice(_isRepG2[state], repIndex - 2); } } return price; } int GetRepPrice(int repIndex, int len, int state, int posState) { int price = _repMatchLenEncoder.GetPrice(len - Base.kMatchMinLen, posState); return price + GetPureRepPrice(repIndex, state, posState); } int GetPosLenPrice(int pos, int len, int posState) { int price; int lenToPosState = Base.GetLenToPosState(len); if (pos < Base.kNumFullDistances) price = _distancesPrices[(lenToPosState * Base.kNumFullDistances) + pos]; else price = _posSlotPrices[(lenToPosState << Base.kNumPosSlotBits) + GetPosSlot2(pos)] + _alignPrices[pos & Base.kAlignMask]; return price + _lenEncoder.GetPrice(len - Base.kMatchMinLen, posState); } int Backward(int cur) { _optimumEndIndex = cur; int posMem = _optimum[cur].PosPrev; int backMem = _optimum[cur].BackPrev; do { if (_optimum[cur].Prev1IsChar) { _optimum[posMem].MakeAsChar(); _optimum[posMem].PosPrev = posMem - 1; if (_optimum[cur].Prev2) { _optimum[posMem - 1].Prev1IsChar = false; _optimum[posMem - 1].PosPrev = _optimum[cur].PosPrev2; _optimum[posMem - 1].BackPrev = _optimum[cur].BackPrev2; } } int posPrev = posMem; int backCur = backMem; backMem = _optimum[posPrev].BackPrev; posMem = _optimum[posPrev].PosPrev; _optimum[posPrev].BackPrev = backCur; _optimum[posPrev].PosPrev = cur; cur = posPrev; } while (cur > 0); backRes = _optimum[0].BackPrev; _optimumCurrentIndex = _optimum[0].PosPrev; return _optimumCurrentIndex; } int[] reps = new int[Base.kNumRepDistances]; int[] repLens = new int[Base.kNumRepDistances]; int backRes; int GetOptimum(int position) throws IOException { if (_optimumEndIndex != _optimumCurrentIndex) { int lenRes = _optimum[_optimumCurrentIndex].PosPrev - _optimumCurrentIndex; backRes = _optimum[_optimumCurrentIndex].BackPrev; _optimumCurrentIndex = _optimum[_optimumCurrentIndex].PosPrev; return lenRes; } _optimumCurrentIndex = _optimumEndIndex = 0; int lenMain, numDistancePairs; if (!_longestMatchWasFound) { lenMain = ReadMatchDistances(); } else { lenMain = _longestMatchLength; _longestMatchWasFound = false; } numDistancePairs = _numDistancePairs; int numAvailableBytes = _matchFinder.getNumAvailableBytes() + 1; if (numAvailableBytes < 2) { backRes = -1; return 1; } if (numAvailableBytes > Base.kMatchMaxLen) numAvailableBytes = Base.kMatchMaxLen; int repMaxIndex = 0; int i; for (i = 0; i < Base.kNumRepDistances; i++) { reps[i] = _repDistances[i]; repLens[i] = _matchFinder.getMatchLen(0 - 1, reps[i], Base.kMatchMaxLen); if (repLens[i] > repLens[repMaxIndex]) repMaxIndex = i; } if (repLens[repMaxIndex] >= _numFastBytes) { backRes = repMaxIndex; int lenRes = repLens[repMaxIndex]; MovePos(lenRes - 1); return lenRes; } if (lenMain >= _numFastBytes) { backRes = _matchDistances[numDistancePairs - 1] + Base.kNumRepDistances; MovePos(lenMain - 1); return lenMain; } byte currentByte = _matchFinder.getIndexByte(0 - 1); byte matchByte = _matchFinder.getIndexByte(0 - _repDistances[0] - 1 - 1); if (lenMain < 2 && currentByte != matchByte && repLens[repMaxIndex] < 2) { backRes = -1; return 1; } _optimum[0].State = _state; int posState = (position & _posStateMask); _optimum[1].Price = com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isMatch[(_state << Base.kNumPosStatesBitsMax) + posState]) + _literalEncoder.GetSubCoder(position, _previousByte).GetPrice(!Base.StateIsCharState(_state), matchByte, currentByte); _optimum[1].MakeAsChar(); int matchPrice = com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isMatch[(_state << Base.kNumPosStatesBitsMax) + posState]); int repMatchPrice = matchPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRep[_state]); if (matchByte == currentByte) { int shortRepPrice = repMatchPrice + GetRepLen1Price(_state, posState); if (shortRepPrice < _optimum[1].Price) { _optimum[1].Price = shortRepPrice; _optimum[1].MakeAsShortRep(); } } int lenEnd = ((lenMain >= repLens[repMaxIndex]) ? lenMain : repLens[repMaxIndex]); if (lenEnd < 2) { backRes = _optimum[1].BackPrev; return 1; } _optimum[1].PosPrev = 0; _optimum[0].Backs0 = reps[0]; _optimum[0].Backs1 = reps[1]; _optimum[0].Backs2 = reps[2]; _optimum[0].Backs3 = reps[3]; int len = lenEnd; do _optimum[len--].Price = kInfinityPrice; while (len >= 2); for (i = 0; i < Base.kNumRepDistances; i++) { int repLen = repLens[i]; if (repLen < 2) continue; int price = repMatchPrice + GetPureRepPrice(i, _state, posState); do { int curAndLenPrice = price + _repMatchLenEncoder.GetPrice(repLen - 2, posState); Optimal optimum = _optimum[repLen]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = 0; optimum.BackPrev = i; optimum.Prev1IsChar = false; } } while (--repLen >= 2); } int normalMatchPrice = matchPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isRep[_state]); len = ((repLens[0] >= 2) ? repLens[0] + 1 : 2); if (len <= lenMain) { int offs = 0; while (len > _matchDistances[offs]) offs += 2; for (; ; len++) { int distance = _matchDistances[offs + 1]; int curAndLenPrice = normalMatchPrice + GetPosLenPrice(distance, len, posState); Optimal optimum = _optimum[len]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = 0; optimum.BackPrev = distance + Base.kNumRepDistances; optimum.Prev1IsChar = false; } if (len == _matchDistances[offs]) { offs += 2; if (offs == numDistancePairs) break; } } } int cur = 0; while (true) { cur++; if (cur == lenEnd) return Backward(cur); int newLen = ReadMatchDistances(); numDistancePairs = _numDistancePairs; if (newLen >= _numFastBytes) { _longestMatchLength = newLen; _longestMatchWasFound = true; return Backward(cur); } position++; int posPrev = _optimum[cur].PosPrev; int state; if (_optimum[cur].Prev1IsChar) { posPrev--; if (_optimum[cur].Prev2) { state = _optimum[_optimum[cur].PosPrev2].State; if (_optimum[cur].BackPrev2 < Base.kNumRepDistances) state = Base.StateUpdateRep(state); else state = Base.StateUpdateMatch(state); } else state = _optimum[posPrev].State; state = Base.StateUpdateChar(state); } else state = _optimum[posPrev].State; if (posPrev == cur - 1) { if (_optimum[cur].IsShortRep()) state = Base.StateUpdateShortRep(state); else state = Base.StateUpdateChar(state); } else { int pos; if (_optimum[cur].Prev1IsChar && _optimum[cur].Prev2) { posPrev = _optimum[cur].PosPrev2; pos = _optimum[cur].BackPrev2; state = Base.StateUpdateRep(state); } else { pos = _optimum[cur].BackPrev; if (pos < Base.kNumRepDistances) state = Base.StateUpdateRep(state); else state = Base.StateUpdateMatch(state); } Optimal opt = _optimum[posPrev]; if (pos < Base.kNumRepDistances) { if (pos == 0) { reps[0] = opt.Backs0; reps[1] = opt.Backs1; reps[2] = opt.Backs2; reps[3] = opt.Backs3; } else if (pos == 1) { reps[0] = opt.Backs1; reps[1] = opt.Backs0; reps[2] = opt.Backs2; reps[3] = opt.Backs3; } else if (pos == 2) { reps[0] = opt.Backs2; reps[1] = opt.Backs0; reps[2] = opt.Backs1; reps[3] = opt.Backs3; } else { reps[0] = opt.Backs3; reps[1] = opt.Backs0; reps[2] = opt.Backs1; reps[3] = opt.Backs2; } } else { reps[0] = (pos - Base.kNumRepDistances); reps[1] = opt.Backs0; reps[2] = opt.Backs1; reps[3] = opt.Backs2; } } _optimum[cur].State = state; _optimum[cur].Backs0 = reps[0]; _optimum[cur].Backs1 = reps[1]; _optimum[cur].Backs2 = reps[2]; _optimum[cur].Backs3 = reps[3]; int curPrice = _optimum[cur].Price; currentByte = _matchFinder.getIndexByte(0 - 1); matchByte = _matchFinder.getIndexByte(0 - reps[0] - 1 - 1); posState = (position & _posStateMask); int curAnd1Price = curPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isMatch[(state << Base.kNumPosStatesBitsMax) + posState]) + _literalEncoder.GetSubCoder(position, _matchFinder.getIndexByte(0 - 2)). GetPrice(!Base.StateIsCharState(state), matchByte, currentByte); Optimal nextOptimum = _optimum[cur + 1]; boolean nextIsChar = false; if (curAnd1Price < nextOptimum.Price) { nextOptimum.Price = curAnd1Price; nextOptimum.PosPrev = cur; nextOptimum.MakeAsChar(); nextIsChar = true; } matchPrice = curPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isMatch[(state << Base.kNumPosStatesBitsMax) + posState]); repMatchPrice = matchPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRep[state]); if (matchByte == currentByte && !(nextOptimum.PosPrev < cur && nextOptimum.BackPrev == 0)) { int shortRepPrice = repMatchPrice + GetRepLen1Price(state, posState); if (shortRepPrice <= nextOptimum.Price) { nextOptimum.Price = shortRepPrice; nextOptimum.PosPrev = cur; nextOptimum.MakeAsShortRep(); nextIsChar = true; } } int numAvailableBytesFull = _matchFinder.getNumAvailableBytes() + 1; numAvailableBytesFull = Math.min(kNumOpts - 1 - cur, numAvailableBytesFull); numAvailableBytes = numAvailableBytesFull; if (numAvailableBytes < 2) continue; if (numAvailableBytes > _numFastBytes) numAvailableBytes = _numFastBytes; if (!nextIsChar && matchByte != currentByte) { // try Literal + rep0 int t = Math.min(numAvailableBytesFull - 1, _numFastBytes); int lenTest2 = _matchFinder.getMatchLen(0, reps[0], t); if (lenTest2 >= 2) { int state2 = Base.StateUpdateChar(state); int posStateNext = (position + 1) & _posStateMask; int nextRepMatchPrice = curAnd1Price + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isMatch[(state2 << Base.kNumPosStatesBitsMax) + posStateNext]) + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRep[state2]); { int offset = cur + 1 + lenTest2; while (lenEnd < offset) _optimum[++lenEnd].Price = kInfinityPrice; int curAndLenPrice = nextRepMatchPrice + GetRepPrice( 0, lenTest2, state2, posStateNext); Optimal optimum = _optimum[offset]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur + 1; optimum.BackPrev = 0; optimum.Prev1IsChar = true; optimum.Prev2 = false; } } } } int startLen = 2; // speed optimization for (int repIndex = 0; repIndex < Base.kNumRepDistances; repIndex++) { int lenTest = _matchFinder.getMatchLen(0 - 1, reps[repIndex], numAvailableBytes); if (lenTest < 2) continue; int lenTestTemp = lenTest; do { while (lenEnd < cur + lenTest) _optimum[++lenEnd].Price = kInfinityPrice; int curAndLenPrice = repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState); Optimal optimum = _optimum[cur + lenTest]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur; optimum.BackPrev = repIndex; optimum.Prev1IsChar = false; } } while (--lenTest >= 2); lenTest = lenTestTemp; if (repIndex == 0) startLen = lenTest + 1; // if (_maxMode) if (lenTest < numAvailableBytesFull) { int t = Math.min(numAvailableBytesFull - 1 - lenTest, _numFastBytes); int lenTest2 = _matchFinder.getMatchLen(lenTest, reps[repIndex], t); if (lenTest2 >= 2) { int state2 = Base.StateUpdateRep(state); int posStateNext = (position + lenTest) & _posStateMask; int curAndLenCharPrice = repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState) + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isMatch[(state2 << Base.kNumPosStatesBitsMax) + posStateNext]) + _literalEncoder.GetSubCoder(position + lenTest, _matchFinder.getIndexByte(lenTest - 1 - 1)).GetPrice(true, _matchFinder.getIndexByte(lenTest - 1 - (reps[repIndex] + 1)), _matchFinder.getIndexByte(lenTest - 1)); state2 = Base.StateUpdateChar(state2); posStateNext = (position + lenTest + 1) & _posStateMask; int nextMatchPrice = curAndLenCharPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isMatch[(state2 << Base.kNumPosStatesBitsMax) + posStateNext]); int nextRepMatchPrice = nextMatchPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRep[state2]); // for(; lenTest2 >= 2; lenTest2--) { int offset = lenTest + 1 + lenTest2; while (lenEnd < cur + offset) _optimum[++lenEnd].Price = kInfinityPrice; int curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext); Optimal optimum = _optimum[cur + offset]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur + lenTest + 1; optimum.BackPrev = 0; optimum.Prev1IsChar = true; optimum.Prev2 = true; optimum.PosPrev2 = cur; optimum.BackPrev2 = repIndex; } } } } } if (newLen > numAvailableBytes) { newLen = numAvailableBytes; for (numDistancePairs = 0; newLen > _matchDistances[numDistancePairs]; numDistancePairs += 2) ; _matchDistances[numDistancePairs] = newLen; numDistancePairs += 2; } if (newLen >= startLen) { normalMatchPrice = matchPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isRep[state]); while (lenEnd < cur + newLen) _optimum[++lenEnd].Price = kInfinityPrice; int offs = 0; while (startLen > _matchDistances[offs]) offs += 2; for (int lenTest = startLen; ; lenTest++) { int curBack = _matchDistances[offs + 1]; int curAndLenPrice = normalMatchPrice + GetPosLenPrice(curBack, lenTest, posState); Optimal optimum = _optimum[cur + lenTest]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur; optimum.BackPrev = curBack + Base.kNumRepDistances; optimum.Prev1IsChar = false; } if (lenTest == _matchDistances[offs]) { if (lenTest < numAvailableBytesFull) { int t = Math.min(numAvailableBytesFull - 1 - lenTest, _numFastBytes); int lenTest2 = _matchFinder.getMatchLen(lenTest, curBack, t); if (lenTest2 >= 2) { int state2 = Base.StateUpdateMatch(state); int posStateNext = (position + lenTest) & _posStateMask; int curAndLenCharPrice = curAndLenPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice0(_isMatch[(state2 << Base.kNumPosStatesBitsMax) + posStateNext]) + _literalEncoder.GetSubCoder(position + lenTest, _matchFinder.getIndexByte(lenTest - 1 - 1)). GetPrice(true, _matchFinder.getIndexByte(lenTest - (curBack + 1) - 1), _matchFinder.getIndexByte(lenTest - 1)); state2 = Base.StateUpdateChar(state2); posStateNext = (position + lenTest + 1) & _posStateMask; int nextMatchPrice = curAndLenCharPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isMatch[(state2 << Base.kNumPosStatesBitsMax) + posStateNext]); int nextRepMatchPrice = nextMatchPrice + com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.GetPrice1(_isRep[state2]); int offset = lenTest + 1 + lenTest2; while (lenEnd < cur + offset) _optimum[++lenEnd].Price = kInfinityPrice; curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext); optimum = _optimum[cur + offset]; if (curAndLenPrice < optimum.Price) { optimum.Price = curAndLenPrice; optimum.PosPrev = cur + lenTest + 1; optimum.BackPrev = 0; optimum.Prev1IsChar = true; optimum.Prev2 = true; optimum.PosPrev2 = cur; optimum.BackPrev2 = curBack + Base.kNumRepDistances; } } } offs += 2; if (offs == numDistancePairs) break; } } } } } boolean ChangePair(int smallDist, int bigDist) { int kDif = 7; return (smallDist < (1 << (32 - kDif)) && bigDist >= (smallDist << kDif)); } void WriteEndMarker(int posState) throws IOException { if (!_writeEndMark) return; _rangeEncoder.Encode(_isMatch, (_state << Base.kNumPosStatesBitsMax) + posState, 1); _rangeEncoder.Encode(_isRep, _state, 0); _state = Base.StateUpdateMatch(_state); int len = Base.kMatchMinLen; _lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); int posSlot = (1 << Base.kNumPosSlotBits) - 1; int lenToPosState = Base.GetLenToPosState(len); _posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot); int footerBits = 30; int posReduced = (1 << footerBits) - 1; _rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits); _posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask); } void Flush(int nowPos) throws IOException { ReleaseMFStream(); WriteEndMarker(nowPos & _posStateMask); _rangeEncoder.FlushData(); _rangeEncoder.FlushStream(); } public void CodeOneBlock(long[] inSize, long[] outSize, boolean[] finished) throws IOException { inSize[0] = 0; outSize[0] = 0; finished[0] = true; if (_inStream != null) { _matchFinder.SetStream(_inStream); _matchFinder.Init(); _needReleaseMFStream = true; _inStream = null; } if (_finished) return; _finished = true; long progressPosValuePrev = nowPos64; if (nowPos64 == 0) { if (_matchFinder.getNumAvailableBytes() == 0) { Flush((int)nowPos64); return; } ReadMatchDistances(); int posState = (int)(nowPos64) & _posStateMask; _rangeEncoder.Encode(_isMatch, (_state << Base.kNumPosStatesBitsMax) + posState, 0); _state = Base.StateUpdateChar(_state); byte curByte = _matchFinder.getIndexByte(0 - _additionalOffset); _literalEncoder.GetSubCoder((int)(nowPos64), _previousByte).Encode(_rangeEncoder, curByte); _previousByte = curByte; _additionalOffset--; nowPos64++; } if (_matchFinder.getNumAvailableBytes() == 0) { Flush((int)nowPos64); return; } while (true) { int len = GetOptimum((int)nowPos64); int pos = backRes; int posState = ((int)nowPos64) & _posStateMask; int complexState = (_state << Base.kNumPosStatesBitsMax) + posState; if (len == 1 && pos == -1) { _rangeEncoder.Encode(_isMatch, complexState, 0); byte curByte = _matchFinder.getIndexByte(-_additionalOffset); LiteralEncoder.Encoder2 subCoder = _literalEncoder.GetSubCoder((int)nowPos64, _previousByte); if (!Base.StateIsCharState(_state)) { byte matchByte = _matchFinder.getIndexByte((-_repDistances[0] - 1 - _additionalOffset)); subCoder.EncodeMatched(_rangeEncoder, matchByte, curByte); } else subCoder.Encode(_rangeEncoder, curByte); _previousByte = curByte; _state = Base.StateUpdateChar(_state); } else { _rangeEncoder.Encode(_isMatch, complexState, 1); if (pos < Base.kNumRepDistances) { _rangeEncoder.Encode(_isRep, _state, 1); if (pos == 0) { _rangeEncoder.Encode(_isRepG0, _state, 0); if (len == 1) _rangeEncoder.Encode(_isRep0Long, complexState, 0); else _rangeEncoder.Encode(_isRep0Long, complexState, 1); } else { _rangeEncoder.Encode(_isRepG0, _state, 1); if (pos == 1) _rangeEncoder.Encode(_isRepG1, _state, 0); else { _rangeEncoder.Encode(_isRepG1, _state, 1); _rangeEncoder.Encode(_isRepG2, _state, pos - 2); } } if (len == 1) _state = Base.StateUpdateShortRep(_state); else { _repMatchLenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); _state = Base.StateUpdateRep(_state); } int distance = _repDistances[pos]; if (pos != 0) { for (int i = pos; i >= 1; i--) _repDistances[i] = _repDistances[i - 1]; _repDistances[0] = distance; } } else { _rangeEncoder.Encode(_isRep, _state, 0); _state = Base.StateUpdateMatch(_state); _lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); pos -= Base.kNumRepDistances; int posSlot = GetPosSlot(pos); int lenToPosState = Base.GetLenToPosState(len); _posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot); if (posSlot >= Base.kStartPosModelIndex) { int footerBits = (posSlot >> 1) - 1; int baseVal = ((2 | (posSlot & 1)) << footerBits); int posReduced = pos - baseVal; if (posSlot < Base.kEndPosModelIndex) BitTreeEncoder.ReverseEncode(_posEncoders, baseVal - posSlot - 1, _rangeEncoder, footerBits, posReduced); else { _rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits); _posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask); _alignPriceCount++; } } int distance = pos; for (int i = Base.kNumRepDistances - 1; i >= 1; i--) _repDistances[i] = _repDistances[i - 1]; _repDistances[0] = distance; _matchPriceCount++; } _previousByte = _matchFinder.getIndexByte(len - 1 - _additionalOffset); } _additionalOffset -= len; nowPos64 += len; if (_additionalOffset == 0) { // if (!_fastMode) if (_matchPriceCount >= (1 << 7)) FillDistancesPrices(); if (_alignPriceCount >= Base.kAlignTableSize) FillAlignPrices(); inSize[0] = nowPos64; outSize[0] = _rangeEncoder.GetProcessedSizeAdd(); if (_matchFinder.getNumAvailableBytes() == 0) { Flush((int)nowPos64); return; } if (nowPos64 - progressPosValuePrev >= (1 << 12)) { _finished = false; finished[0] = false; return; } } } } void ReleaseMFStream() { if (_matchFinder != null && _needReleaseMFStream) { _matchFinder.ReleaseStream(); _needReleaseMFStream = false; } } void SetOutStream(java.io.OutputStream outStream) { _rangeEncoder.SetStream(outStream); } void ReleaseOutStream() { _rangeEncoder.ReleaseStream(); } void ReleaseStreams() { ReleaseMFStream(); ReleaseOutStream(); } void SetStreams(java.io.InputStream inStream, java.io.OutputStream outStream, long inSize, long outSize) { _inStream = inStream; _finished = false; Create(); SetOutStream(outStream); Init(); // if (!_fastMode) { FillDistancesPrices(); FillAlignPrices(); } _lenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen); _lenEncoder.UpdateTables(1 << _posStateBits); _repMatchLenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen); _repMatchLenEncoder.UpdateTables(1 << _posStateBits); nowPos64 = 0; } long[] processedInSize = new long[1]; long[] processedOutSize = new long[1]; boolean[] finished = new boolean[1]; public void Code(java.io.InputStream inStream, java.io.OutputStream outStream, long inSize, long outSize, ICodeProgress progress) throws IOException { _needReleaseMFStream = false; try { SetStreams(inStream, outStream, inSize, outSize); while (true) { CodeOneBlock(processedInSize, processedOutSize, finished); if (finished[0]) return; if (progress != null) { progress.SetProgress(processedInSize[0], processedOutSize[0]); } } } finally { ReleaseStreams(); } } public static final int kPropSize = 5; byte[] properties = new byte[kPropSize]; public void WriteCoderProperties(java.io.OutputStream outStream) throws IOException { properties[0] = (byte)((_posStateBits * 5 + _numLiteralPosStateBits) * 9 + _numLiteralContextBits); for (int i = 0; i < 4; i++) properties[1 + i] = (byte)(_dictionarySize >> (8 * i)); outStream.write(properties, 0, kPropSize); } int[] tempPrices = new int[Base.kNumFullDistances]; int _matchPriceCount; void FillDistancesPrices() { for (int i = Base.kStartPosModelIndex; i < Base.kNumFullDistances; i++) { int posSlot = GetPosSlot(i); int footerBits = (posSlot >> 1) - 1; int baseVal = ((2 | (posSlot & 1)) << footerBits); tempPrices[i] = BitTreeEncoder.ReverseGetPrice(_posEncoders, baseVal - posSlot - 1, footerBits, i - baseVal); } for (int lenToPosState = 0; lenToPosState < Base.kNumLenToPosStates; lenToPosState++) { int posSlot; BitTreeEncoder encoder = _posSlotEncoder[lenToPosState]; int st = (lenToPosState << Base.kNumPosSlotBits); for (posSlot = 0; posSlot < _distTableSize; posSlot++) _posSlotPrices[st + posSlot] = encoder.GetPrice(posSlot); for (posSlot = Base.kEndPosModelIndex; posSlot < _distTableSize; posSlot++) _posSlotPrices[st + posSlot] += ((((posSlot >> 1) - 1) - Base.kNumAlignBits) << com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder.Encoder.kNumBitPriceShiftBits); int st2 = lenToPosState * Base.kNumFullDistances; int i; for (i = 0; i < Base.kStartPosModelIndex; i++) _distancesPrices[st2 + i] = _posSlotPrices[st + i]; for (; i < Base.kNumFullDistances; i++) _distancesPrices[st2 + i] = _posSlotPrices[st + GetPosSlot(i)] + tempPrices[i]; } _matchPriceCount = 0; } void FillAlignPrices() { for (int i = 0; i < Base.kAlignTableSize; i++) _alignPrices[i] = _posAlignEncoder.ReverseGetPrice(i); _alignPriceCount = 0; } public boolean SetAlgorithm(int algorithm) { /* _fastMode = (algorithm == 0); _maxMode = (algorithm >= 2); */ return true; } public boolean SetDictionarySize(int dictionarySize) { int kDicLogSizeMaxCompress = 29; if (dictionarySize < (1 << Base.kDicLogSizeMin) || dictionarySize > (1 << kDicLogSizeMaxCompress)) return false; _dictionarySize = dictionarySize; int dicLogSize; for (dicLogSize = 0; dictionarySize > (1 << dicLogSize); dicLogSize++) ; _distTableSize = dicLogSize * 2; return true; } public boolean SeNumFastBytes(int numFastBytes) { if (numFastBytes < 5 || numFastBytes > Base.kMatchMaxLen) return false; _numFastBytes = numFastBytes; return true; } public boolean SetMatchFinder(int matchFinderIndex) { if (matchFinderIndex < 0 || matchFinderIndex > 2) return false; int matchFinderIndexPrev = _matchFinderType; _matchFinderType = matchFinderIndex; if (_matchFinder != null && matchFinderIndexPrev != _matchFinderType) { _dictionarySizePrev = -1; _matchFinder = null; } return true; } public boolean SetLcLpPb(int lc, int lp, int pb) { if ( lp < 0 || lp > Base.kNumLitPosStatesBitsEncodingMax || lc < 0 || lc > Base.kNumLitContextBitsMax || pb < 0 || pb > Base.kNumPosStatesBitsEncodingMax) return false; _numLiteralPosStateBits = lp; _numLiteralContextBits = lc; _posStateBits = pb; _posStateMask = ((1) << _posStateBits) - 1; return true; } public void SetEndMarkerMode(boolean endMarkerMode) { _writeEndMark = endMarkerMode; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/RangeCoder/BitDecoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder; public class BitDecoder extends BitModel { public BitDecoder(int num) { super(num); } public int Decode(Decoder decoder) throws java.io.IOException { int newBound = (decoder.Range >>> kNumBitModelTotalBits) * this.Prob; if ((decoder.Code ^ 0x80000000) < (newBound ^ 0x80000000)) { decoder.Range = newBound; this.Prob += (kBitModelTotal - this.Prob) >>> numMoveBits; if ((decoder.Range & kTopMask) == 0) { decoder.Code = (decoder.Code << 8) | decoder.bufferedStream.read(); decoder.Range <<= 8; } return 0; } else { decoder.Range -= newBound; decoder.Code -= newBound; this.Prob -= (this.Prob) >>> numMoveBits; if ((decoder.Range & kTopMask) == 0) { decoder.Code = (decoder.Code << 8) | decoder.bufferedStream.read(); decoder.Range <<= 8; } return 1; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/RangeCoder/BitModel.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder; public class BitModel { public static final int kTopMask = ~((1 << 24) - 1); public static final int kNumBitModelTotalBits = 11; public static final int kBitModelTotal = (1 << kNumBitModelTotalBits); int numMoveBits; int Prob; public BitModel(int num) { numMoveBits = num; } /* public void UpdateModel(UInt32 symbol) { if (symbol == 0) Prob += (kBitModelTotal - Prob) >> numMoveBits; else Prob -= (Prob) >> numMoveBits; } */ public void Init() { Prob = kBitModelTotal / 2; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/RangeCoder/BitTreeDecoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder; public class BitTreeDecoder { short[] Models; int NumBitLevels; public BitTreeDecoder(int numBitLevels) { NumBitLevels = numBitLevels; Models = new short[1 << numBitLevels]; } public void Init() { Decoder.InitBitModels(Models); } public int Decode(Decoder rangeDecoder) throws java.io.IOException { int m = 1; for (int bitIndex = NumBitLevels; bitIndex != 0; bitIndex--) m = (m << 1) + rangeDecoder.DecodeBit(Models, m); return m - (1 << NumBitLevels); } public int ReverseDecode(Decoder rangeDecoder) throws java.io.IOException { int m = 1; int symbol = 0; for (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++) { int bit = rangeDecoder.DecodeBit(Models, m); m <<= 1; m += bit; symbol |= (bit << bitIndex); } return symbol; } public static int ReverseDecode(short[] Models, int startIndex, Decoder rangeDecoder, int NumBitLevels) throws java.io.IOException { int m = 1; int symbol = 0; for (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++) { int bit = rangeDecoder.DecodeBit(Models, startIndex + m); m <<= 1; m += bit; symbol |= (bit << bitIndex); } return symbol; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/RangeCoder/BitTreeEncoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder; import java.io.IOException; public class BitTreeEncoder { short[] Models; int NumBitLevels; public BitTreeEncoder(int numBitLevels) { NumBitLevels = numBitLevels; Models = new short[1 << numBitLevels]; } public void Init() { Decoder.InitBitModels(Models); } public void Encode(Encoder rangeEncoder, int symbol) throws IOException { int m = 1; for (int bitIndex = NumBitLevels; bitIndex != 0; ) { bitIndex--; int bit = (symbol >>> bitIndex) & 1; rangeEncoder.Encode(Models, m, bit); m = (m << 1) | bit; } } public void ReverseEncode(Encoder rangeEncoder, int symbol) throws IOException { int m = 1; for (int i = 0; i < NumBitLevels; i++) { int bit = symbol & 1; rangeEncoder.Encode(Models, m, bit); m = (m << 1) | bit; symbol >>= 1; } } public int GetPrice(int symbol) { int price = 0; int m = 1; for (int bitIndex = NumBitLevels; bitIndex != 0; ) { bitIndex--; int bit = (symbol >>> bitIndex) & 1; price += Encoder.GetPrice(Models[m], bit); m = (m << 1) + bit; } return price; } public int ReverseGetPrice(int symbol) { int price = 0; int m = 1; for (int i = NumBitLevels; i != 0; i--) { int bit = symbol & 1; symbol >>>= 1; price += Encoder.GetPrice(Models[m], bit); m = (m << 1) | bit; } return price; } public static int ReverseGetPrice(short[] Models, int startIndex, int NumBitLevels, int symbol) { int price = 0; int m = 1; for (int i = NumBitLevels; i != 0; i--) { int bit = symbol & 1; symbol >>>= 1; price += Encoder.GetPrice(Models[startIndex + m], bit); m = (m << 1) | bit; } return price; } public static void ReverseEncode(short[] Models, int startIndex, Encoder rangeEncoder, int NumBitLevels, int symbol) throws IOException { int m = 1; for (int i = 0; i < NumBitLevels; i++) { int bit = symbol & 1; rangeEncoder.Encode(Models, startIndex + m, bit); m = (m << 1) | bit; symbol >>= 1; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/RangeCoder/Decoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder; import java.io.IOException; public class Decoder { static final int kTopMask = ~((1 << 24) - 1); static final int kNumBitModelTotalBits = 11; static final int kBitModelTotal = (1 << kNumBitModelTotalBits); static final int kNumMoveBits = 5; int Range; int Code; // boolean _wasFinished; // long _processedSize; // public boolean WasFinished() { return _wasFinished; } // public java.io.InputStream Stream; public com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.InBuffer bufferedStream = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Common.InBuffer(); int read() throws IOException { return bufferedStream.read(); } public void Create(int bufferSize) { bufferedStream.Create(bufferSize); } public long GetProcessedSize() { return bufferedStream.GetProcessedSize(); } public final void SetStream(java.io.InputStream stream) { bufferedStream.SetStream(stream); } public final void ReleaseStream() throws IOException { bufferedStream.ReleaseStream(); } public final void Init() throws IOException { bufferedStream.Init(); Code = 0; Range = -1; for (int i = 0; i < 5; i++) { Code = (Code << 8) | this.read(); } } public final int DecodeDirectBits(int numTotalBits) throws IOException { int result = 0; for (int i = numTotalBits; i != 0; i--) { Range >>>= 1; int t = ((Code - Range) >>> 31); Code -= Range & (t - 1); result = (result << 1) | (1 - t); if ((Range & kTopMask) == 0) { Code = (Code << 8) | this.read(); Range <<= 8; } } return result; } public int DecodeBit(short []probs, int index) throws IOException { int prob = probs[index]; int newBound = (Range >>> kNumBitModelTotalBits) * prob; if ((Code ^ 0x80000000) < (newBound ^ 0x80000000)) { Range = newBound; probs[index] = (short)(prob + ((kBitModelTotal - prob) >>> kNumMoveBits)); if ((Range & kTopMask) == 0) { Code = (Code << 8) | this.read(); Range <<= 8; } return 0; } else { Range -= newBound; Code -= newBound; probs[index] = (short)(prob - ((prob) >>> kNumMoveBits)); if ((Range & kTopMask) == 0) { Code = (Code << 8) | this.read(); Range <<= 8; } return 1; } } public static void InitBitModels(short []probs) { for (int i = 0; i < probs.length; i++) probs[i] = (kBitModelTotal >>> 1); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/Compression/RangeCoder/Encoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.RangeCoder; import java.io.IOException; public class Encoder { static final int kTopMask = ~((1 << 24) - 1); static final int kNumBitModelTotalBits = 11; static final int kBitModelTotal = (1 << kNumBitModelTotalBits); static final int kNumMoveBits = 5; java.io.OutputStream Stream; long Low; int Range; int _cacheSize; int _cache; long _position; public void SetStream(java.io.OutputStream stream) { Stream = stream; } public void ReleaseStream() { Stream = null; } public void Init() { _position = 0; Low = 0; Range = -1; _cacheSize = 1; _cache = 0; } public void FlushData() throws IOException { for (int i = 0; i < 5; i++) ShiftLow(); } public void FlushStream() throws IOException { Stream.flush(); } public void ShiftLow() throws IOException { int LowHi = (int)(Low >>> 32); if (LowHi != 0 || Low < 0xFF000000L) { _position += _cacheSize; int temp = _cache; do { Stream.write(temp + LowHi); temp = 0xFF; } while(--_cacheSize != 0); _cache = (((int)Low) >>> 24); } _cacheSize++; Low = (Low & 0xFFFFFF) << 8; } public void EncodeDirectBits(int v, int numTotalBits) throws IOException { for (int i = numTotalBits - 1; i >= 0; i--) { Range >>>= 1; if (((v >>> i) & 1) == 1) Low += Range; if ((Range & Encoder.kTopMask) == 0) { Range <<= 8; ShiftLow(); } } } public long GetProcessedSizeAdd() { return _cacheSize + _position + 4; } static final int kNumMoveReducingBits = 2; public static final int kNumBitPriceShiftBits = 6; public static void InitBitModels(short []probs) { for (int i = 0; i < probs.length; i++) probs[i] = (kBitModelTotal >>> 1); } public void Encode(short []probs, int index, int symbol) throws IOException { int prob = probs[index]; int newBound = (Range >>> kNumBitModelTotalBits) * prob; if (symbol == 0) { Range = newBound; probs[index] = (short)(prob + ((kBitModelTotal - prob) >>> kNumMoveBits)); } else { Low += (newBound & 0xFFFFFFFFL); Range -= newBound; probs[index] = (short)(prob - ((prob) >>> kNumMoveBits)); } if ((Range & kTopMask) == 0) { Range <<= 8; ShiftLow(); } } private static int[] ProbPrices = new int[kBitModelTotal >>> kNumMoveReducingBits]; static { int kNumBits = (kNumBitModelTotalBits - kNumMoveReducingBits); for (int i = kNumBits - 1; i >= 0; i--) { int start = 1 << (kNumBits - i - 1); int end = 1 << (kNumBits - i); for (int j = start; j < end; j++) ProbPrices[j] = (i << kNumBitPriceShiftBits) + (((end - j) << kNumBitPriceShiftBits) >>> (kNumBits - i - 1)); } } static public int GetPrice(int Prob, int symbol) { return ProbPrices[(((Prob - symbol) ^ ((-symbol))) & (kBitModelTotal - 1)) >>> kNumMoveReducingBits]; } static public int GetPrice0(int Prob) { return ProbPrices[Prob >>> kNumMoveReducingBits]; } static public int GetPrice1(int Prob) { return ProbPrices[(kBitModelTotal - Prob) >>> kNumMoveReducingBits]; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/HRESULT.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public class HRESULT { public static final int S_OK = 0; public static final int S_FALSE = 1; public static final int E_NOTIMPL = 0x80004001; public static final int E_FAIL = 0x80004005; public static final int E_INVALIDARG = 0x80070057; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICodeProgress.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICodeProgress { void SetProgress(long inSize, long outSize); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressCoder.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressCoder { int Code( java.io.InputStream inStream, // , ISequentialInStream java.io.OutputStream outStream, // ISequentialOutStream long outSize, ICompressProgressInfo progress) throws java.io.IOException ; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressCoder2.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; import com.mucommander.commons.file.impl.sevenzip.provider.Common.RecordVector; public interface ICompressCoder2 { int Code( RecordVector inStreams, Object useless1, // const UInt64 ** /* inSizes */, int numInStreams, RecordVector outStreams, Object useless2, // const UInt64 ** /* outSizes */, int numOutStreams, ICompressProgressInfo progress) throws java.io.IOException; void close() throws java.io.IOException ; // destructor } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressFilter.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressFilter { int Init(); int Filter(byte [] data, int size); // Filter return outSize (UInt32) // if (outSize <= size): Filter have converted outSize bytes // if (outSize > size): Filter have not converted anything. // and it needs at least outSize bytes to convert one block // (it's for crypto block algorithms). } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressGetInStreamProcessedSize.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressGetInStreamProcessedSize { long GetInStreamProcessedSize(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressProgressInfo.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressProgressInfo { long INVALID = -1; int SetRatioInfo(long inSize, long outSize); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressSetDecoderProperties2.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressSetDecoderProperties2 { boolean SetDecoderProperties2(byte[] properties); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressSetInStream.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressSetInStream { int SetInStream(java.io.InputStream inStream); int ReleaseInStream() throws java.io.IOException ; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressSetOutStream.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressSetOutStream { int SetOutStream(java.io.OutputStream inStream); int ReleaseOutStream() throws java.io.IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/ICompressSetOutStreamSize.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface ICompressSetOutStreamSize { int INVALID_OUTSIZE = -1; int SetOutStreamSize(long outSize); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/IInStream.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public abstract class IInStream extends java.io.InputStream { static public final int STREAM_SEEK_SET = 0; static public final int STREAM_SEEK_CUR = 1; // static public final int STREAM_SEEK_END = 2; public abstract long Seek(long offset, int seekOrigin) throws java.io.IOException ; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/IProgress.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public interface IProgress { int SetTotal(long total); int SetCompleted(long completeValue); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/J7zip.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; import java.text.DateFormat; import java.util.Arrays; import java.util.Vector; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IArchiveExtractCallback; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.IInArchive; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZipEntry; import com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Archive.SevenZip.Handler; public class J7zip { static void PrintHelp() { System.out.println( "\nUsage: JZip [...]\n" + " l : Lists files\n" + " t : Tests archive.7z\n" + " x : eXtracts files\n"); } static void listing(IInArchive archive,Vector listOfNames,boolean techMode) { if (!techMode) { System.out.println(" Date Time Attr Size Compressed Name"); System.out.println("-------------- ----- ------------ ------------ ------------"); } long size = 0; long packSize = 0; long nbFiles = 0; for(int i = 0; i < archive.size() ; i++) { SevenZipEntry item = archive.getEntry(i); DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT , DateFormat.SHORT ); String str_tm = formatter.format(new java.util.Date(item.getTime())); if (listOfNames.contains(item.getName())) { if (techMode) { System.out.println("Path = " + item.getName()); System.out.println("Size = " + item.getSize()); System.out.println("Packed Size = " + item.getCompressedSize()); System.out.println("Modified = " + str_tm); System.out.println(" Attributes : " + item.getAttributesString()); long crc = item.getCrc(); if (crc != -1) System.out.println("CRC = " + Long.toHexString(crc).toUpperCase()); else System.out.println("CRC ="); System.out.println("Method = " + item.getMethods() ); System.out.println(); } else { System.out.print(str_tm + " " + item.getAttributesString()); System.out.print(String.format("%13d",item.getSize())); System.out.print(String.format("%13d",item.getCompressedSize())); System.out.println(" " + item.getName()); } size += item.getSize(); packSize += item.getCompressedSize(); nbFiles ++; } } if (!techMode) { System.out.println("-------------- ----- ------------ ------------ ------------"); System.out.print(String.format(" %13d%13d %d files",size,packSize,nbFiles)); } } static void testOrExtract(IInArchive archive,Vector listOfNames,int mode) { ArchiveExtractCallback extractCallbackSpec = new ArchiveExtractCallback(); IArchiveExtractCallback extractCallback = extractCallbackSpec; extractCallbackSpec.Init(archive); extractCallbackSpec.PasswordIsDefined = false; try { int len = 0; int arrays [] = null; if (listOfNames.size() >= 1) { arrays = new int[listOfNames.size()]; for(int i = 0 ; i < archive.size() ; i++) { if (listOfNames.contains(archive.getEntry(i).getName())) { arrays[len++] = i; } } } int res; if (len == 0) { res = archive.Extract(null, -1, mode , extractCallback); } else { res = archive.Extract(arrays, len, mode, extractCallback); } if (res == HRESULT.S_OK) { if (extractCallbackSpec.NumErrors == 0) System.out.println("Ok Done"); else System.out.println(" " + extractCallbackSpec.NumErrors + " errors"); } else { System.out.println("ERROR !!"); } } catch (java.io.IOException e) { System.out.println("IO error : " + e.getLocalizedMessage()); } } public static void main(String[] args) throws Exception { System.out.println("\nJ7zip 4.43 ALPHA 2 (" + Runtime.getRuntime().availableProcessors() + " CPUs)"); if (args.length < 2) { PrintHelp(); return ; } final int MODE_LISTING = 0; final int MODE_TESTING = 1; final int MODE_EXTRACT = 2; int mode = -1; Vector listOfNames = new Vector<>(Arrays.asList(args).subList(2, args.length)); switch (args[0]) { case "l": mode = MODE_LISTING; break; case "t": mode = MODE_TESTING; break; case "x": mode = MODE_EXTRACT; break; default: PrintHelp(); return; } String filename = args[1]; MyRandomAccessFile istream = new MyRandomAccessFile(filename,"r"); IInArchive archive = new Handler(); int ret = archive.Open( istream ); if (ret != 0) { System.out.println("ERROR !"); return ; } switch(mode) { case MODE_LISTING: listing(archive,listOfNames,false); break; case MODE_TESTING: testOrExtract(archive,listOfNames,IInArchive.NExtract_NAskMode_kTest); break; case MODE_EXTRACT: testOrExtract(archive,listOfNames,IInArchive.NExtract_NAskMode_kExtract); break; } archive.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/LzmaAlone.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public class LzmaAlone { static public class CommandLine { public static final int kEncode = 0; public static final int kDecode = 1; public static final int kBenchmak = 2; public int Command = -1; public int NumBenchmarkPasses = 10; public int dictionarySize = 1 << 23; public boolean dictionarySizeIsDefined = false; public int Lc = 3; public int Lp = 0; public int Pb = 2; public int fb = 128; public boolean fbIsDefined = false; public boolean Eos = false; public int algorithm = 2; public int matchFinder = 1; public String inFile; public String outFile; boolean ParseSwitch(String s) { if (s.startsWith("d")) { dictionarySize = 1 << Integer.parseInt(s.substring(1)); dictionarySizeIsDefined = true; } else if (s.startsWith("fb")) { fb = Integer.parseInt(s.substring(2)); fbIsDefined = true; } else if (s.startsWith("a")) { algorithm = Integer.parseInt(s.substring(1)); } else if (s.startsWith("lc")) { Lc = Integer.parseInt(s.substring(2)); } else if (s.startsWith("lp")) { Lp = Integer.parseInt(s.substring(2)); } else if (s.startsWith("pb")) { Pb = Integer.parseInt(s.substring(2)); } else if (s.startsWith("eos")) { Eos = true; } else if (s.startsWith("mf")) { String mfs = s.substring(2); switch (mfs) { case "bt2": matchFinder = 0; break; case "bt4": matchFinder = 1; break; case "bt4b": matchFinder = 2; break; default: return false; } } else { return false; } return true; } public boolean Parse(String[] args) { int pos = 0; boolean switchMode = true; for (String s : args) { if (s.isEmpty()) return false; if (switchMode) { if (s.compareTo("--") == 0) { switchMode = false; continue; } if (s.charAt(0) == '-') { String sw = s.substring(1).toLowerCase(); if (sw.isEmpty()) return false; try { if (!ParseSwitch(sw)) return false; } catch (NumberFormatException e) { return false; } continue; } } if (pos == 0) { if (s.equalsIgnoreCase("e")) Command = kEncode; else if (s.equalsIgnoreCase("d")) Command = kDecode; else if (s.equalsIgnoreCase("b")) Command = kBenchmak; else return false; } else if (pos == 1) { if (Command == kBenchmak) { try { NumBenchmarkPasses = Integer.parseInt(s); if (NumBenchmarkPasses < 1) return false; } catch (NumberFormatException e) { return false; } } else inFile = s; } else if (pos == 2) outFile = s; else return false; pos++; //continue; } return true; } } static void PrintHelp() { System.out.println( "\nUsage: LZMA [...] inputFile outputFile\n" + " e: encode file\n" + " d: decode file\n" + " b: Benchmark\n" + "\n" + // " -a{N}: set compression mode - [0, 1], default: 1 (max)\n" + " -d{N}: set dictionary - [0,28], default: 23 (8MB)\n" + " -fb{N}: set number of fast bytes - [5, 273], default: 128\n" + " -lc{N}: set number of literal context bits - [0, 8], default: 3\n" + " -lp{N}: set number of literal pos bits - [0, 4], default: 0\n" + " -pb{N}: set number of pos bits - [0, 4], default: 2\n" + " -mf{MF_ID}: set Match Finder: [bt2, bt4], default: bt4\n" + " -eos: write End Of Stream marker\n" ); } public static void main(String[] args) throws Exception { System.out.println("\nLZMA (Java) 4.42 Copyright (c) 1999-2006 Igor Pavlov 2006-05-15\n"); if (args.length < 1) { PrintHelp(); return; } CommandLine params = new CommandLine(); if (!params.Parse(args)) { System.out.println("\nIncorrect command"); return; } if (params.Command == CommandLine.kBenchmak) { int dictionary = (1 << 21); if (params.dictionarySizeIsDefined) dictionary = params.dictionarySize; if (params.matchFinder > 1) throw new Exception("Unsupported match finder"); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.LzmaBench.LzmaBenchmark(params.NumBenchmarkPasses, dictionary); } else if (params.Command == CommandLine.kEncode || params.Command == CommandLine.kDecode) { java.io.File inFile = new java.io.File(params.inFile); java.io.File outFile = new java.io.File(params.outFile); java.io.BufferedInputStream inStream = new java.io.BufferedInputStream(new java.io.FileInputStream(inFile)); java.io.BufferedOutputStream outStream = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outFile)); boolean eos = false; if (params.Eos) eos = true; if (params.Command == CommandLine.kEncode) { com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Encoder encoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Encoder(); if (!encoder.SetAlgorithm(params.algorithm)) throw new Exception("Incorrect compression mode"); if (!encoder.SetDictionarySize(params.dictionarySize)) throw new Exception("Incorrect dictionary size"); if (!encoder.SeNumFastBytes(params.fb)) throw new Exception("Incorrect -fb value"); if (!encoder.SetMatchFinder(params.matchFinder)) throw new Exception("Incorrect -mf value"); if (!encoder.SetLcLpPb(params.Lc, params.Lp, params.Pb)) throw new Exception("Incorrect -lc or -lp or -pb value"); encoder.SetEndMarkerMode(eos); encoder.WriteCoderProperties(outStream); long fileSize; if (eos) fileSize = -1; else fileSize = inFile.length(); for (int i = 0; i < 8; i++) outStream.write((int)(fileSize >>> (8 * i)) & 0xFF); encoder.Code(inStream, outStream, -1, -1, null); } else { int propertiesSize = 5; byte[] properties = new byte[propertiesSize]; if (inStream.read(properties, 0, propertiesSize) != propertiesSize) throw new Exception("input .lzma file is too short"); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Decoder decoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Decoder(); if (!decoder.SetDecoderProperties2(properties)) throw new Exception("Incorrect stream properties"); long outSize = 0; for (int i = 0; i < 8; i++) { int v = inStream.read(); if (v < 0) throw new Exception("Can't read stream size"); outSize |= ((long)v) << (8 * i); } if (decoder.Code(inStream, outStream, outSize,null) != HRESULT.S_OK) throw new Exception("Error in data stream"); } outStream.flush(); outStream.close(); inStream.close(); } else throw new Exception("Incorrect command"); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/LzmaBench.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; import java.io.ByteArrayOutputStream; import java.io.IOException; public class LzmaBench { static final int kAdditionalSize = (1 << 21); static final int kCompressedAdditionalSize = (1 << 10); static class CRandomGenerator { int A1; int A2; public CRandomGenerator() { Init(); } public void Init() { A1 = 362436069; A2 = 521288629; } public int GetRnd() { return ((A1 = 36969 * (A1 & 0xffff) + (A1 >>> 16)) << 16) ^ ((A2 = 18000 * (A2 & 0xffff) + (A2 >>> 16))); } } static class CBitRandomGenerator { CRandomGenerator RG = new CRandomGenerator(); int Value; int NumBits; public void Init() { Value = 0; NumBits = 0; } public int GetRnd(int numBits) { int result; if (NumBits > numBits) { result = Value & ((1 << numBits) - 1); Value >>>= numBits; NumBits -= numBits; return result; } numBits -= NumBits; result = (Value << numBits); Value = RG.GetRnd(); result |= Value & ((1 << numBits) - 1); Value >>>= numBits; NumBits = 32 - numBits; return result; } } static class CBenchRandomGenerator { CBitRandomGenerator RG = new CBitRandomGenerator(); int Pos; int Rep0; public int BufferSize; public byte[] Buffer = null; public CBenchRandomGenerator() { } public void Set(int bufferSize) { Buffer = new byte[bufferSize]; Pos = 0; BufferSize = bufferSize; } int GetRndBit() { return RG.GetRnd(1); } int GetLogRandBits(int numBits) { int len = RG.GetRnd(numBits); return RG.GetRnd(len); } int GetOffset() { if (GetRndBit() == 0) return GetLogRandBits(4); return (GetLogRandBits(4) << 10) | RG.GetRnd(10); } int GetLen1() { return RG.GetRnd(1 + RG.GetRnd(2)); } int GetLen2() { return RG.GetRnd(2 + RG.GetRnd(2)); } public void Generate() { RG.Init(); Rep0 = 1; while (Pos < BufferSize) { if (GetRndBit() == 0 || Pos < 1) Buffer[Pos++] = (byte)(RG.GetRnd(8)); else { int len; if (RG.GetRnd(3) == 0) len = 1 + GetLen1(); else { do Rep0 = GetOffset(); while (Rep0 >= Pos); Rep0++; len = 2 + GetLen2(); } for (int i = 0; i < len && Pos < BufferSize; i++, Pos++) Buffer[Pos] = Buffer[Pos - Rep0]; } } } } static class CrcOutStream extends java.io.OutputStream { public com.mucommander.commons.file.impl.sevenzip.provider.Common.CRC CRC = new com.mucommander.commons.file.impl.sevenzip.provider.Common.CRC(); public void Init() { CRC.Init(); } public int GetDigest() { return CRC.getDigest(); } public void write(byte[] b) { CRC.Update(b); } public void write(byte[] b, int off, int len) { CRC.Update(b, off, len); } public void write(int b) { CRC.updateByte(b); } } static class MyOutputStream extends java.io.OutputStream { byte[] _buffer; int _size; int _pos; public MyOutputStream(byte[] buffer) { _buffer = buffer; _size = _buffer.length; } public void reset() { _pos = 0; } public void write(int b) throws IOException { if (_pos >= _size) throw new IOException("Error"); _buffer[_pos++] = (byte)b; } public int size() { return _pos; } } static class MyInputStream extends java.io.InputStream { byte[] _buffer; int _size; int _pos; public MyInputStream(byte[] buffer, int size) { _buffer = buffer; _size = size; } public void reset() { _pos = 0; } public int read() { if (_pos >= _size) return -1; return _buffer[_pos++] & 0xFF; } } static class CProgressInfo implements ICodeProgress { public long ApprovedStart; public long InSize; public long Time; public void Init() { InSize = 0; } public void SetProgress(long inSize, long outSize) { if (inSize >= ApprovedStart && InSize == 0) { Time = System.currentTimeMillis(); InSize = inSize; } } } static final int kSubBits = 8; static int GetLogSize(int size) { for (int i = kSubBits; i < 32; i++) for (int j = 0; j < (1 << kSubBits); j++) if (size <= ((1) << i) + (j << (i - kSubBits))) return (i << kSubBits) + j; return (32 << kSubBits); } static long MyMultDiv64(long value, long elapsedTime) { long freq = 1000; // ms long elTime = elapsedTime; while (freq > 1000000) { freq >>>= 1; elTime >>>= 1; } if (elTime == 0) elTime = 1; return value * freq / elTime; } static long GetCompressRating(int dictionarySize, long elapsedTime, long size) { long t = GetLogSize(dictionarySize) - (18 << kSubBits); long numCommandsForOne = 1060 + ((t * t * 10) >> (2 * kSubBits)); long numCommands = (size) * numCommandsForOne; return MyMultDiv64(numCommands, elapsedTime); } static long GetDecompressRating(long elapsedTime, long outSize, long inSize) { long numCommands = inSize * 220 + outSize * 20; return MyMultDiv64(numCommands, elapsedTime); } static long GetTotalRating( int dictionarySize, long elapsedTimeEn, long sizeEn, long elapsedTimeDe, long inSizeDe, long outSizeDe) { return (GetCompressRating(dictionarySize, elapsedTimeEn, sizeEn) + GetDecompressRating(elapsedTimeDe, inSizeDe, outSizeDe)) / 2; } static void PrintValue(long v) { String s = ""; s += v; for (int i = 0; i + s.length() < 6; i++) System.out.print(" "); System.out.print(s); } static void PrintRating(long rating) { PrintValue(rating / 1000000); System.out.print(" MIPS"); } static void PrintResults( int dictionarySize, long elapsedTime, long size, boolean decompressMode, long secondSize) { long speed = MyMultDiv64(size, elapsedTime); PrintValue(speed / 1024); System.out.print(" KB/s "); long rating; if (decompressMode) rating = GetDecompressRating(elapsedTime, size, secondSize); else rating = GetCompressRating(dictionarySize, elapsedTime, size); PrintRating(rating); } static public int LzmaBenchmark(int numIterations, int dictionarySize) throws Exception { if (numIterations <= 0) return 0; if (dictionarySize < (1 << 18)) { System.out.println("\nError: dictionary size for benchmark must be >= 18 (256 KB)"); return 1; } System.out.print("\n Compressing Decompressing\n\n"); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Encoder encoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Encoder(); com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Decoder decoder = new com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.Compression.LZMA.Decoder(); if (!encoder.SetDictionarySize(dictionarySize)) throw new Exception("Incorrect dictionary size"); int kBufferSize = dictionarySize + kAdditionalSize; int kCompressedBufferSize = (kBufferSize / 2) + kCompressedAdditionalSize; ByteArrayOutputStream propStream = new ByteArrayOutputStream(); encoder.WriteCoderProperties(propStream); byte[] propArray = propStream.toByteArray(); decoder.SetDecoderProperties2(propArray); CBenchRandomGenerator rg = new CBenchRandomGenerator(); rg.Set(kBufferSize); rg.Generate(); com.mucommander.commons.file.impl.sevenzip.provider.Common.CRC crc = new com.mucommander.commons.file.impl.sevenzip.provider.Common.CRC(); crc.Init(); crc.Update(rg.Buffer, 0, rg.BufferSize); CProgressInfo progressInfo = new CProgressInfo(); progressInfo.ApprovedStart = dictionarySize; long totalBenchSize = 0; long totalEncodeTime = 0; long totalDecodeTime = 0; long totalCompressedSize = 0; MyInputStream inStream = new MyInputStream(rg.Buffer, rg.BufferSize); byte[] compressedBuffer = new byte[kCompressedBufferSize]; MyOutputStream compressedStream = new MyOutputStream(compressedBuffer); CrcOutStream crcOutStream = new CrcOutStream(); MyInputStream inputCompressedStream = null; int compressedSize = 0; for (int i = 0; i < numIterations; i++) { progressInfo.Init(); inStream.reset(); compressedStream.reset(); encoder.Code(inStream, compressedStream, -1, -1, progressInfo); long encodeTime = System.currentTimeMillis() - progressInfo.Time; if (i == 0) { compressedSize = compressedStream.size(); inputCompressedStream = new MyInputStream(compressedBuffer, compressedSize); } else if (compressedSize != compressedStream.size()) throw (new Exception("Encoding error")); if (progressInfo.InSize == 0) throw (new Exception("Internal ERROR 1282")); long decodeTime = 0; for (int j = 0; j < 2; j++) { inputCompressedStream.reset(); crcOutStream.Init(); long startTime = System.currentTimeMillis(); if (decoder.Code(inputCompressedStream, crcOutStream, (long) kBufferSize, null) != HRESULT.S_OK) throw (new Exception("Decoding Error")); decodeTime = System.currentTimeMillis() - startTime; if (crcOutStream.GetDigest() != crc.getDigest()) throw (new Exception("CRC Error")); } long benchSize = kBufferSize - progressInfo.InSize; PrintResults(dictionarySize, encodeTime, benchSize, false, 0); System.out.print(" "); PrintResults(dictionarySize, decodeTime, kBufferSize, true, compressedSize); System.out.println(); totalBenchSize += benchSize; totalEncodeTime += encodeTime; totalDecodeTime += decodeTime; totalCompressedSize += compressedSize; } System.out.println("---------------------------------------------------"); PrintResults(dictionarySize, totalEncodeTime, totalBenchSize, false, 0); System.out.print(" "); PrintResults(dictionarySize, totalDecodeTime, kBufferSize * (long)numIterations, true, totalCompressedSize); System.out.println(" Average"); return 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sevenzip/provider/SevenZip/MyRandomAccessFile.java ================================================ package com.mucommander.commons.file.impl.sevenzip.provider.SevenZip; public class MyRandomAccessFile extends com.mucommander.commons.file.impl.sevenzip.provider.SevenZip.IInStream { java.io.RandomAccessFile _file; MyRandomAccessFile(String filename,String mode) throws java.io.IOException { _file = new java.io.RandomAccessFile(filename,mode); } public long Seek(long offset, int seekOrigin) throws java.io.IOException { if (seekOrigin == STREAM_SEEK_SET) { _file.seek(offset); } else if (seekOrigin == STREAM_SEEK_CUR) { _file.seek(offset + _file.getFilePointer()); } return _file.getFilePointer(); } public int read() throws java.io.IOException { return _file.read(); } public int read(byte [] data, int off, int size) throws java.io.IOException { return _file.read(data,off,size); } public int read(byte [] data, int size) throws java.io.IOException { return _file.read(data,0,size); } public void close() throws java.io.IOException { _file.close(); _file = null; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sftp/SFTPConnectionHandler.java ================================================ package com.mucommander.commons.file.impl.sftp; import com.mucommander.commons.file.AuthException; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.connection.ConnectionHandler; import com.sshtools.net.SocketTransport; import com.sshtools.publickey.InvalidPassphraseException; import com.sshtools.publickey.SshPrivateKeyFile; import com.sshtools.publickey.SshPrivateKeyFileFactory; import com.sshtools.sftp.SftpClient; import com.sshtools.sftp.SftpStatusException; import com.sshtools.sftp.SftpSubsystemChannel; import com.sshtools.ssh.*; import com.sshtools.ssh.components.SshKeyPair; import com.sshtools.ssh2.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Handles connections to SFTP servers. * * @author Maxence Bernard, Vassil Dichev */ class SFTPConnectionHandler extends ConnectionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(SFTPConnectionHandler.class); Ssh2Client sshClient; SftpClient sftpClient; SftpSubsystemChannel sftpSubsystem; /** 'Password' SSH authentication method */ private final static String PASSWORD_AUTH_METHOD = "password"; /** 'Keyboard interactive' SSH authentication method */ private final static String KEYBOARD_INTERACTIVE_AUTH_METHOD = "keyboard-interactive"; /** 'Public key' SSH authentication method, not supported at the moment */ private final static String PUBLIC_KEY_AUTH_METHOD = "publickey"; SFTPConnectionHandler(FileURL location) { super(location); } @Override public void startConnection() throws IOException { LOGGER.info("starting connection to {}", realm); try { FileURL realm = getRealm(); // Retrieve credentials to be used to authenticate final Credentials credentials = getCredentials(); // Throw an AuthException if no auth information, required for SSH if (credentials == null) { throwAuthException("Login and password required"); // Todo: localize this entry } LOGGER.trace("creating SshClient"); // Override default port (22) if a custom port was specified in the URL int port = realm.getPort(); if (port == -1) { port = 22; } // Connect to server, no host key verification SshConnector con = SshConnector.createInstance(); // Lets do some host key verification HostKeyVerification hkv = (hostname, key) -> { try { System.out.println("The connected host's key ("+ key.getAlgorithm() + ") is"); System.out.println(key.getFingerprint()); } catch (SshException ignore) {} return true; }; con.getContext().setHostKeyVerification(hkv); con.getContext().setPreferredPublicKey(Ssh2Context.PUBLIC_KEY_SSHDSS); // Init SSH client sshClient = (Ssh2Client) con.connect(new SocketTransport(realm.getHost(), port), credentials.getLogin(), true); // sshClient.connect(realm.getHost(), port, new IgnoreHostKeyVerification()); // Retrieve a list of available authentication methods on the server. // Some SSH servers support the 'password' auth method (e.g. OpenSSH on Debian unstable), some don't // and only support the 'keyboard-interactive' method. List authMethods = new ArrayList<>(Arrays.asList(sshClient.getAuthenticationMethods(credentials.getLogin()))); LOGGER.info("getAvailableAuthMethods()={}", sshClient.getAuthenticationMethods(credentials.getLogin())); SshAuthentication authClient = null; String privateKeyPath = realm.getProperty(SFTPFile.PRIVATE_KEY_PATH_PROPERTY_NAME); // Try public key first. Don't try other methods if there's a key file defined if (authMethods.contains(PUBLIC_KEY_AUTH_METHOD) && privateKeyPath != null) { LOGGER.info("Using {} authentication method", PUBLIC_KEY_AUTH_METHOD); Ssh2PublicKeyAuthentication pk = new Ssh2PublicKeyAuthentication(); pk.setUsername(credentials.getLogin()); // Throw an AuthException if problems with private key file try { SshPrivateKeyFile pkfile = SshPrivateKeyFileFactory.parse(new FileInputStream(privateKeyPath)); SshKeyPair pair = pkfile.toKeyPair(pkfile.isPassphraseProtected() ? credentials.getPassword() : null); pk.setPrivateKey(pair.getPrivateKey()); pk.setPublicKey(pair.getPublicKey()); } catch (IOException | InvalidPassphraseException e) { LOGGER.error("Keys error", e); privateKeyPath = null; // try to authorize via password on error // throwAuthException("Invalid private key file or passphrase"); // Todo: localize this entry // } catch (IOException e) { // e.printStackTrace(); // throwAuthException("Error reading private key file"); // Todo: localize this entry } authClient = pk; } // Use 'keyboard-interactive' method only if 'password' auth method is not available and // 'keyboard-interactive' is supported by the server //else if (!authMethods.contains(PASSWORD_AUTH_METHOD) && authMethods.contains(KEYBOARD_INTERACTIVE_AUTH_METHOD) && privateKeyPath == null) { LOGGER.info("Using {} authentication method", KEYBOARD_INTERACTIVE_AUTH_METHOD); KBIAuthentication kbi = new KBIAuthentication(); kbi.setUsername(credentials.getLogin()); // Fake keyboard password input kbi.setKBIRequestHandler((name, instruction, prompts) -> { // Workaround for what seems to be a bug in J2SSH: this method is called twice, first time // with a valid KBIPrompt array, second time with null if (prompts == null) { LOGGER.trace("prompts is null!"); return false; } for (int i = 0; i < prompts.length; i++) { LOGGER.trace("prompts[{}]={}", i, prompts[i].getPrompt()); prompts[i].setResponse(credentials.getPassword()); } return true; }); authClient = kbi; } // Default to 'password' method, even if server didn't report as being supported else if (privateKeyPath == null) { LOGGER.info("Using {} authentication method", PASSWORD_AUTH_METHOD); Ssh2PasswordAuthentication pwd = new Ssh2PasswordAuthentication(); pwd.setUsername(credentials.getLogin()); pwd.setPassword(credentials.getPassword()); authClient = pwd; } authenticate(authClient); // Init SFTP connections sftpClient = new SftpClient(sshClient); SshSession session = sshClient.openSessionChannel(); if (session instanceof Ssh2Session) { ((Ssh2Session) session).startSubsystem("sftp"); } sftpSubsystem = new SftpSubsystemChannel(session); sftpSubsystem.initialize(); } catch(IOException | SftpStatusException | SshException | ChannelOpenException e) { LOGGER.info("IOException thrown while starting connection", e); // Disconnect if something went wrong if (sshClient != null && sshClient.isConnected()) { sshClient.disconnect(); } sshClient = null; sftpClient = null; sftpSubsystem = null; // Re-throw exception if (e instanceof IOException) { throw (IOException)e; } else { throw new IOException(e); } } } private void authenticate(SshAuthentication authClient) throws SshException, AuthException { try { int authResult = sshClient.authenticate(authClient); // Throw an AuthException if authentication failed if (authResult != SshAuthentication.COMPLETE) { throwAuthException("Login or password rejected"); // Todo: localize this entry } LOGGER.info("authentication complete, authResult={}", authResult); } catch(AuthException e) { LOGGER.info("Caught exception while authenticating", e); throw e;//throwAuthException(e.getMessage()); } } @Override public synchronized boolean isConnected() { return sshClient != null && sshClient.isConnected() && sftpClient != null && !sftpClient.isClosed() && sftpSubsystem !=null && !sftpSubsystem.isClosed(); } @Override public synchronized void closeConnection() { if (sftpClient != null) { try { sftpClient.quit(); } catch(SshException e) { LOGGER.info("IOException caught while calling sftpClient.quit()", e); } } if (sftpSubsystem != null) { try { sftpSubsystem.close(); } catch(IOException e) { LOGGER.info("IOException caught while calling sftpChannel.close ()"); } } if (sshClient != null) { sshClient.disconnect(); } } @Override public void keepAlive() { // No-op, keep alive is not available and shouldn't really be necessary, SSH servers such as OpenSSH usually // maintain connections open without limit. } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sftp/SFTPConnectionHandlerFactory.java ================================================ package com.mucommander.commons.file.impl.sftp; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.connection.ConnectionHandler; import com.mucommander.commons.file.connection.ConnectionHandlerFactory; /** * ConnectionHandlerFactory that creates {@link SFTPConnectionHandler} instances. * * @author Maxence Bernard */ public class SFTPConnectionHandlerFactory implements ConnectionHandlerFactory { public ConnectionHandler createConnectionHandler(FileURL location) { return new SFTPConnectionHandler(location); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/sftp/SFTPFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.sftp; import com.mucommander.commons.file.*; import com.mucommander.commons.file.connection.ConnectionHandler; import com.mucommander.commons.file.connection.ConnectionPool; import com.mucommander.commons.io.*; import com.sshtools.sftp.*; import com.sshtools.ssh.SshException; import com.sshtools.util.UnsignedInteger32; import com.sshtools.util.UnsignedInteger64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * SFTPFile provides access to files located on an SFTP server. * *

    The associated {@link FileURL} scheme is {@link FileProtocols#SFTP}. The host part of the URL designates the * SFTP server. Credentials must be specified in the login and password parts as SFTP servers require a login and * password. The path separator is '/'. * *

    Here are a few examples of valid SFTP URLs: * * sftp://server/pathto/somefile
    * sftp://login:password@server/pathto/somefile
    *
    * *

    Internally, SFTPFile uses {@link ConnectionPool} to create SFTP connections as needed and allows them to be * reused by SFTPFile instances located on the same server, dealing with concurrency issues. Connections are * thus managed transparently and need not be manually managed. * *

    Low-level SFTP implementation is provided by the J2SSH library distributed under the LGPL license. * * @see ConnectionPool * @author Maxence Bernard */ public class SFTPFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(SFTPFile.class); /** The absolute path to the file on the remote server, not the full URL */ private final String absPath; /** Contains the file attribute values */ private final SFTPFileAttributes fileAttributes; /** Cached parent file instance, null if not created yet or if this file has no parent */ private AbstractFile parent; /** Has the parent file been determined yet? */ private boolean parentValSet; /** Cached canonical path value, null if the canonical path hasn't been fetched yet */ private String canonicalPath; /** Timestamp when the canonical path value was fetched */ private long canonicalPathFetchedTime; /** Period of time during which file attributes are cached, before being fetched again from the server. */ private static long attributeCachingPeriod = 60000; /** a SFTPConnectionHandlerFactory instance */ private final static SFTPConnectionHandlerFactory CONN_HANDLER_FACTORY = new SFTPConnectionHandlerFactory(); /** Name of the property that holds the path to a private key. This property is optional; if it is set, private key * authentication is used. */ public final static String PRIVATE_KEY_PATH_PROPERTY_NAME = "privateKeyPath"; private final static String SEPARATOR = DEFAULT_SEPARATOR; /** * Creates a new instance of SFTPFile and initializes the SSH/SFTP connection to the server. * @throws IOException if an I/O error occurred */ SFTPFile(FileURL fileURL) throws IOException { this(fileURL, null); } SFTPFile(FileURL fileURL, SFTPFileAttributes fileAttributes) throws IOException { super(fileURL); // // Throw an AuthException if the url doesn't contain any credentials // if(!fileURL.containsCredentials()) // throw new AuthException(fileURL); this.absPath = fileURL.getPath(); this.fileAttributes = fileAttributes == null ? new SFTPFileAttributes(fileURL) : fileAttributes; } /** * Sets the time period during which attributes values (e.g. isDirectory, last modified, ...) are cached. * The higher this value, the lower the number of network requests but also the longer it takes * before those attributes can be refreshed. A value of 0 disables attributes caching. * *

    This class ensures that the attributes changed remotely by one of its methods are always updated locally, even * with attributes caching enabled. To illustrate, after a call to {@link #mkdir()}, {@link #isDirectory()} will * return true, even if the attributes haven't been refreshed. The attributes will however not be * consistent if they have been changed by another {@link SFTPFile} or by another process, and will remain * inconsistent for up to period milliseconds. * * @param period time period during which attributes values are cached, in milliseconds. 0 disables attributes caching. */ public static void setAttributeCachingPeriod(long period) { attributeCachingPeriod = period; } private OutputStream getOutputStream(boolean append) throws IOException { // Retrieve a ConnectionHandler and lock it final SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); SftpFile sftpFile; if (exists()) { sftpFile = connHandler.sftpSubsystem.openFile(absPath, append ? SftpSubsystemChannel.OPEN_WRITE | SftpSubsystemChannel.OPEN_APPEND : SftpSubsystemChannel.OPEN_WRITE | SftpSubsystemChannel.OPEN_TRUNCATE); // Update local attributes if (!append) { fileAttributes.setSize(0); } } else { // Set new file permissions to 644 octal (420 dec): "rw-r--r--" // Note: by default, permissions for files freshly created is 0 (not readable/writable/executable by anyone)! // TODO pass real type SftpFileAttributes atts = new SftpFileAttributes(connHandler.sftpSubsystem, SftpFileAttributes.SSH_FILEXFER_TYPE_REGULAR); atts.setPermissions(new UnsignedInteger32(0644)); sftpFile = connHandler.sftpSubsystem.openFile(absPath, SftpSubsystemChannel.OPEN_WRITE|SftpSubsystemChannel.OPEN_CREATE, atts); // Update local attributes fileAttributes.setExists(true); fileAttributes.setDate(System.currentTimeMillis()); fileAttributes.setSize(0); } // Custom SftpFileOutputStream constructor, not part of the official J2SSH API OutputStream os = new SftpFileOutputStreamEx(sftpFile, append ? getSize() : 0L) { @Override public void close() throws IOException { // SftpFileOutputStream.close() closes the open SftpFile file handle super.close(); // Release the lock on the ConnectionHandler connHandler.releaseLock(); } }; ByteCounter byteCounter = new ByteCounter() { @Override public synchronized void add(long nbBytes) { fileAttributes.addToSize(nbBytes); fileAttributes.setDate(System.currentTimeMillis()); } }; return new CounterOutputStream(os, byteCounter); } catch(IOException e) { // Release the lock on the ConnectionHandler if the OutputStream could not be created connHandler.releaseLock(); // Re-throw IOException throw e; } catch (SftpStatusException | SshException e) { // Release the lock on the ConnectionHandler if the OutputStream could not be created connHandler.releaseLock(); throw new IOException(e); } } public ConnectionHandler createConnectionHandler(FileURL location) { return new SFTPConnectionHandler(location); } /** * Implementation note: the value returned by this method will always be false if this file was * created by the public constructor. If this file was created by the private constructor (by {@link #ls()}, * the value will be accurate (true if this file is a symlink) but will never get updated. * See {@link com.mucommander.commons.file.impl.sftp.SFTPFile.SFTPFileAttributes} for more information. */ @Override public boolean isSymlink() { return fileAttributes.isSymlink(); } @Override public boolean isSystem() { return false; } /** * Implementation note: for symlinks, returns the date of the link's target. */ @Override public long getLastModifiedDate() { return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).getLastModifiedDate(); } @Override public void setLastModifiedDate(long lastModified) throws IOException { SFTPConnectionHandler connHandler = null; SftpFile sftpFile = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // Retrieve an SftpFile instance for write, will throw an IOException if the file does not exist or cannot // be written. // /!\ SftpFile instance must be closed afterwards to release its file handle sftpFile = connHandler.sftpSubsystem.openFile(absPath, SftpSubsystemChannel.OPEN_WRITE); SftpFileAttributes attributes = sftpFile.getAttributes(); attributes.setTimes(attributes.getAccessedTime(), new UnsignedInteger64(lastModified/1000)); connHandler.sftpSubsystem.setAttributes(sftpFile, attributes); // Update local attribute copy fileAttributes.setDate(lastModified); } catch (SshException | SftpStatusException e) { LOGGER.error("failed to change the modification date of " + absPath, e); throw new IOException(e); } finally { // Close SftpFile instance to release its handle if (sftpFile != null) { try { sftpFile.close(); } catch (SftpStatusException | SshException ignore) {} } // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } /** * Implementation note: for symlinks, returns the size of the link's target. */ @Override public long getSize() { return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).getSize(); } @Override public AbstractFile getParent() { if(!parentValSet) { FileURL parentFileURL = this.fileURL.getParent(); if (parentFileURL != null) { parent = FileFactory.getFile(parentFileURL); // Note: parent may be null if it can't be resolved } parentValSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValSet = true; } /** * Implementation note: for symlinks, returns the value of the link's target. */ @Override public boolean exists() { return fileAttributes.exists(); } /** * Implementation note: for symlinks, returns the permissions of the link's target. */ @Override public FilePermissions getPermissions() { return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).getPermissions(); } @Override public PermissionBits getChangeablePermissions() { return PermissionBits.FULL_PERMISSION_BITS; // Full permission support (777 octal) } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { changePermissions(ByteUtils.setBit(getPermissions().getIntValue(), (permission << (access*3)), enabled)); } @Override public String getOwner() { return fileAttributes.getOwner(); } @Override public boolean canGetOwner() { return true; } @Override public String getGroup() { return fileAttributes.getGroup(); } @Override public boolean canGetGroup() { return true; } /** * Implementation note: for symlinks, returns the value of the link's target. */ @Override public boolean isDirectory() { return ((SFTPFileAttributes)getCanonicalFile().getUnderlyingFileObject()).isDirectory(); } @Override public InputStream getInputStream() throws IOException { return getInputStream(0); } @Override public OutputStream getOutputStream() throws IOException { return getOutputStream(false); } @Override public OutputStream getAppendOutputStream() throws IOException { return getOutputStream(true); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new SFTPRandomAccessInputStream(); } @Override public void delete() throws IOException { // Retrieve a ConnectionHandler and lock it SFTPConnectionHandler connHandler = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); try { if (isDirectory()) { connHandler.sftpSubsystem.removeDirectory(absPath); } else { connHandler.sftpSubsystem.removeFile(absPath); } } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } // Update local attributes fileAttributes.setExists(false); fileAttributes.setDirectory(false); fileAttributes.setSymlink(false); fileAttributes.setSize(0); } finally { // Release the lock on the ConnectionHandler if the OutputStream could not be created if (connHandler != null) { connHandler.releaseLock(); } } } @Override public AbstractFile[] ls() throws IOException { SftpFile[] files = getSftpFiles(); int nbFiles = files.length; // File doesn't exist, return an empty file array if (nbFiles == 0) { return new AbstractFile[] {}; } AbstractFile[] children = new AbstractFile[nbFiles]; int fileCount = 0; String parentPath = fileURL.getPath(); if (!parentPath.endsWith(SEPARATOR)) { parentPath += SEPARATOR; } // Fill AbstractFile array and discard '.' and '..' files for (SftpFile file : files) { String filename = file.getFilename(); // Discard '.' and '..' files, dunno why these are returned if (filename.equals(".") || filename.equals("..")) { continue; } FileURL childURL = (FileURL) fileURL.clone(); childURL.setPath(parentPath + filename); try { children[fileCount++] = FileFactory.getFile(childURL, this, new SFTPFileAttributes(childURL, file.getAttributes())); } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } } // create new array of the exact file count if (fileCount < nbFiles) { AbstractFile[] newChildren = new AbstractFile[fileCount]; System.arraycopy(children, 0, newChildren, 0, fileCount); return newChildren; } return children; } private SftpFile[] getSftpFiles() throws IOException { // Retrieve a ConnectionHandler and lock it SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); SftpFile[] files; try { connHandler.checkConnection(); // Makes sure the connection is started, if not starts it // connHandler.sftpSubsystem.listChildren(file, files); // Modified J2SSH method to remove the 100 files limitation // Use SftpClient.ls() rather than SftpChannel.listChildren() as it seems to be working better files = connHandler.sftpClient.ls(absPath); } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } return files; } @Override public void mkdir() throws IOException { // Retrieve a ConnectionHandler and lock it SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // Note: this J2SSH method has been patched to set the permissions of the new directory to 0755 (rwxr-xr-x) // instead of 0. This patches allows to avoid a 'change permissions' request (cf comment code hereunder). connHandler.sftpSubsystem.makeDirectory(absPath); // // Set new directory permissions to 755 octal (493 dec): "rwxr-xr-x" // // Note: by default, permissions for files freshly created is 0 (not readable/writable/executable by anyone)! // connHandler.sftpSubsystem.changePermissions(absPath, 493); // Update local attributes fileAttributes.setExists(true); fileAttributes.setDirectory(true); fileAttributes.setDate(System.currentTimeMillis()); fileAttributes.setSize(0); } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } finally { // Release the lock on the ConnectionHandler connHandler.releaseLock(); } } /** * Implementation notes: server-to-server renaming will work if the destination file also uses the 'SFTP' scheme * and is located on the same host. */ @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // Fail in situations where SFTPFile#renameTo() does not, for instance when the source and destination are the same. checkRenamePrerequisites(destFile, true, false); // Retrieve a ConnectionHandler and lock it SFTPConnectionHandler connHandler = null; try { connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // SftpClient#rename() throws an IOException if the destination exists (instead of overwriting the file) if (destFile.exists()) { destFile.delete(); } // Will throw an IOException if the operation failed try { connHandler.sftpClient.rename(absPath, destFile.getURL().getPath()); } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } // Update destination file attributes by fetching them from the server ((SFTPFileAttributes)destFile.getUnderlyingFileObject()).fetchAttributes(); // Update this file's attributes locally fileAttributes.setExists(false); fileAttributes.setDirectory(false); fileAttributes.setSize(0); } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } /** * Returns a {@link com.mucommander.commons.file.impl.sftp.SFTPFile.SFTPFileAttributes} instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return fileAttributes; } // Unsupported file operations /** * Always throws an {@link UnsupportedFileOperationException}: random write access is not supported. */ @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { // No way to retrieve this information with J2SSH throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { // No way to retrieve this information with J2SSH throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } //////////////////////// // Overridden methods // //////////////////////// @Override public void changePermissions(int permissions) throws IOException { // Retrieve a ConnectionHandler and lock it SFTPConnectionHandler connHandler = null; try { connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); connHandler.sftpSubsystem.changePermissions(absPath, permissions); // Update local attribute copy fileAttributes.setPermissions(new SimpleFilePermissions(permissions)); } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } @Override public InputStream getInputStream(long offset) throws IOException { // Retrieve a ConnectionHandler and lock it final SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); try { // Makes sure the connection is started, if not starts it connHandler.checkConnection(); SftpFile sftpFile = connHandler.sftpSubsystem.openFile(absPath, SftpSubsystemChannel.OPEN_READ); // Custom made constructor, not part of the official J2SSH API return new SftpFileInputStream(sftpFile, offset) { @Override public void close() throws IOException { // SftpFileInputStream.close() closes the open SftpFile file handle super.close(); // Release the lock on the ConnectionHandler connHandler.releaseLock(); } }; } catch(IOException e) { // Release the lock on the ConnectionHandler if the InputStream could not be created connHandler.releaseLock(); // Re-throw IOException throw e; } catch (SshException | SftpStatusException e) { e.printStackTrace(); throw new IOException(e); } } @Override public String getCanonicalPath() { if (isSymlink()) { // Check if there is a previous value that hasn't expired yet if (canonicalPath != null && (System.currentTimeMillis() - canonicalPathFetchedTime < attributeCachingPeriod)) return canonicalPath; SFTPConnectionHandler connHandler = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // getSymbolicLinkTarget returns the raw symlink target which can either be an absolute path or a // relative path. If the path is relative preprend the absolute path of the symlink's parent folder. String symlinkTargetPath = connHandler.sftpSubsystem.getSymbolicLinkTarget(fileURL.getPath()); if (!symlinkTargetPath.startsWith("/")) { String parentPath = fileURL.getParent().getPath(); if (!parentPath.endsWith("/")) { parentPath += "/"; } symlinkTargetPath = parentPath + symlinkTargetPath; } FileURL canonicalURL = (FileURL)fileURL.clone(); canonicalURL.setPath(symlinkTargetPath); // Cache the value and return it until it expires canonicalPath = canonicalURL.toString(false); canonicalPathFetchedTime = System.currentTimeMillis(); return canonicalPath; } catch(IOException | SftpStatusException | SshException e) { // Simply continue and return the absolute path } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } // If this file is not a symlink, or the symlink target path could not be retrieved, return the absolute path return getAbsolutePath(); } /** * If the SFTPFile is a symbolic link, this method returns the name of the file being pointed to by the symbolic link. * @return The file pointed to by the symbolic link (null if the FTPFile is not a symbolic link). */ public String getLink() { if (!isSymlink()) { return null; } String symlinkTargetPath; SFTPConnectionHandler connHandler = null; // Retrieve a ConnectionHandler and lock it try { connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // getSymbolicLinkTarget returns the raw symlink target which can either be an absolute path or a // relative path. If the path is relative preprend the absolute path of the symlink's parent folder. symlinkTargetPath = connHandler.sftpSubsystem.getSymbolicLinkTarget(fileURL.getPath()); } catch (IOException | SftpStatusException | SshException e) { symlinkTargetPath = null; e.printStackTrace(); } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } return symlinkTargetPath; } /////////////////// // Inner classes // /////////////////// /** * SFTPFileAttributes provides getters and setters for SFTP file attributes. By extending * SyncedFileAttributes, this class caches attributes for a certain amount of time * ({@link SFTPFile#attributeCachingPeriod}) after which a fresh value is retrieved from the server. */ static class SFTPFileAttributes extends SyncedFileAttributes { /** The URL pointing to the file whose attributes are cached by this class */ private final FileURL url; /** True if the file is a symlink */ private boolean isSymlink; // this constructor is called by SFTPFile public constructor private SFTPFileAttributes(FileURL url) throws AuthException { super(attributeCachingPeriod, false); // no initial update this.url = url; setPermissions(FilePermissions.EMPTY_FILE_PERMISSIONS); fetchAttributes(); // throws AuthException if no or bad credentials updateExpirationDate(); // declare the attributes as 'fresh' } // this constructor is called by #ls() private SFTPFileAttributes(FileURL url, SftpFileAttributes attrs) { super(attributeCachingPeriod, false); // no initial update this.url = url; setPermissions(FilePermissions.EMPTY_FILE_PERMISSIONS); setAttributes(attrs); setExists(true); // Some information about this value: // FileAttribute#isLink() returns a proper value only for FileAttributes instances that were returned by // SftpFile#ls(). FileAttributes that are returned by SftpSubsystemClient#getAttributes(String) always // return false for isLink(). // That means the value of isSymlink is not updated by fetchAttributes(), because if it was, isSymlink // would be false after the first attributes update. this.isSymlink = attrs.isLink(); updateExpirationDate(); // declare the attributes as 'fresh' } private void fetchAttributes() throws AuthException { SFTPConnectionHandler connHandler = null; try { // Retrieve a ConnectionHandler and lock it connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(SFTPFile.CONN_HANDLER_FACTORY, url, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); // Retrieve the file attributes from the server. This will throws an IOException if the file doesn't // exist on the server // Note for symlinks: the FileAttributes returned by SftpSubsystemClient#getAttributes(String) // returns the values of the symlink's target, not the symlink file itself. In other words: the size, // date, isDirectory, isLink values are those of the linked file. This is not a problem, except for // isLink because it makes impossible to detect changes in the isLink state. Changes should not happen // very often, but still. // Todo: try and fix for this in J2SSH setAttributes(connHandler.sftpSubsystem.getAttributes(url.getPath())); setExists(true); } catch (IOException | SftpStatusException | SshException e) { e.printStackTrace(); // File doesn't exist on the server setExists(false); // Rethrow AuthException if (e instanceof AuthException) { throw (AuthException) e; } } finally { // Release the lock on the ConnectionHandler if (connHandler != null) { connHandler.releaseLock(); } } } /** * Sets the file attributes using the values contained in the specified J2SSH FileAttributes instance. * * @param attrs J2SSH FileAttributes instance that contains the values to use */ private void setAttributes(SftpFileAttributes attrs) { setDirectory(attrs.isDirectory()); setDate(attrs.getModifiedTime().longValue()*1000); setSize(attrs.getSize().longValue()); setPermissions(new SimpleFilePermissions( attrs.getPermissions().intValue() & PermissionBits.FULL_PERMISSION_INT )); setOwner(attrs.getUID()); setGroup(attrs.getGID()); setSymlink(isSymlink); } /** * Increments the size attribute's value by the given number of bytes. * * @param increment number of bytes to add to the current size attribute's value */ private void addToSize(long increment) { setSize(getSize()+increment); } /** * Returns true if the file is a symlink. * * @return true if the file is a symlink */ private boolean isSymlink() { checkForExpiration(false); return isSymlink; } /** * Sets whether the file is a symlink. * * @param isSymlink true if the file is a symlink */ private void setSymlink(boolean isSymlink) { this.isSymlink = isSymlink; } //////////////////////////////////////////// // SyncedFileAttributes implementation // //////////////////////////////////////////// @Override public void updateAttributes() { try { fetchAttributes(); } catch(Exception e) { // AuthException LOGGER.info("Failed to refresh attributes", e); } } } /** * SFTPRandomAccessInputStream extends RandomAccessInputStream to provide random read access to an SFTPFile. */ private class SFTPRandomAccessInputStream extends RandomAccessInputStream { private final SftpFileInputStreamEx in; private SFTPRandomAccessInputStream() throws IOException { try { final SFTPConnectionHandler connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // Makes sure the connection is started, if not starts it connHandler.checkConnection(); SftpFile sftpFile = connHandler.sftpSubsystem.openFile(absPath, SftpSubsystemChannel.OPEN_READ); this.in = new SftpFileInputStreamEx(sftpFile);//SftpFileInputStreamEx)getInputStream(); } catch (SftpStatusException | SshException e) { e.printStackTrace(); throw new IOException(e); } } @Override public int read(byte[] b, int off, int len) throws IOException { return in.read(b, off, len); } @Override public int read() throws IOException { return in.read(); } public long getOffset() { // Custom method, not part of the official J2SSH API return in.getPosition(); } public long getLength() { return getSize(); } public void seek(long offset) { // Custom method, not part of the official J2SSH API in.setPosition(offset); } @Override public void close() throws IOException { in.close(); } } // private class SFTPProcess extends AbstractProcess { // // private boolean success; // private SessionChannelClient sessionClient; // private SFTPConnectionHandler connHandler; // // private SFTPProcess(String tokens[]) throws IOException { // // try { // // Retrieve a ConnectionHandler and lock it // connHandler = (SFTPConnectionHandler)ConnectionPool.getConnectionHandler(CONN_HANDLER_FACTORY, fileURL, true); // // Makes sure the connection is started, if not starts it // connHandler.checkConnection(); // // sessionClient = connHandler.sshClient.openSessionChannel(); //// sessionClient.startShell(); // //// success = sessionClient.executeCommand("cd "+(isDirectory()?fileURL.getPath():fileURL.getParent().getPath())); ////FileLogger.finest("commmand="+("cd "+(isDirectory()?fileURL.getPath():fileURL.getParent().getPath()))+" returned "+success); // // // Environment variables are refused by most servers for security reasons //// sessionClient.setEnvironmentVariable("cd", isDirectory()?fileURL.getPath():fileURL.getParent().getPath()); // // // No way to set the current working directory: // // 1/ when executing a single command: // // + environment variables are ignored by most server, so can't use PWD for that. // // + could send 'cd dir ; command' but it's not platform independant and prevents the command from being // // executed under Windows // // 2/ when starting a shell, no problem to change the current working directory (cd dir\n is sent before // // the command), but there is no reliable way to detect the end of the command execution, as confirmed // // by one of the J2SSH developers : http://sourceforge.net/forum/message.php?msg_id=1826569 // // // Concatenates all tokens to create the command string // StringBuffer command = new StringBuffer(); // int nbTokens = tokens.length; // for(int i=0; i Provides an implementation of the SFTP protocol. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/smb/SMBFile.java ================================================ package com.mucommander.commons.file.impl.smb; import com.mucommander.commons.file.*; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import jcifs.smb.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; /** * SMBFile provides access to files located on an SMB/CIFS server. *

    * The associated {@link FileURL} scheme is {@link FileProtocols#SMB}. The host part of the URL designates the * SMB server. Credentials are specified in the login and password parts. The path separator is '/'. *

    * Here are a few examples of valid SMB URLs: * * smb://server/path/to/file
    * smb://domain;username:password@server/path/to/file
    * smb://workgroup/
    *
    *

    * The special 'smb://' URL represents the SMB root and lists all workgroups that are available on the network, * akin to Windows' network neighborhood. *

    * Access to SMB files is provided by the jCIFS library distributed under the LGPL license. * The {@link #getUnderlyingFileObject()} method allows to retrieve a jcifs.smb.SmbFile instance * corresponding to this SMBFile. * * @author Maxence Bernard */ public class SMBFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(SMBFile.class); private SmbFile file; private FilePermissions permissions; private AbstractFile parent; private boolean parentValSet; /** Bit mask that indicates which permissions can be changed. Only the 'write' permission for 'user' access can * be changed. */ private final static PermissionBits CHANGEABLE_PERMISSIONS = new GroupedPermissionBits(128); // -w------- (200 octal) SMBFile(FileURL fileURL) throws IOException { this(fileURL, null); } SMBFile(FileURL fileURL, SmbFile smbFile) throws IOException { super(fileURL); if (!fileURL.containsCredentials()) { throw new AuthException(fileURL, "Authentication required"); } if (smbFile == null) { while(true) { file = createSmbFile(fileURL); // The following test comes at a cost, so it's only used by the public constructor, SmbFile instances // created by this class are considered OK. try { // SmbFile requires a trailing slash for directories otherwise listFiles() will throw an SmbException. // As we cannot guarantee that the path will contain a trailing slash for directories, we test if the // SmbFile is a directory and if it doesn't contain a trailing slash, we create a new SmbFile with // a trailing slash. // SmbFile.isDirectory() will throw an SmbAuthException if access to the file requires different credentials. if(file.isDirectory() && !getURL().getPath().endsWith("/")) { // Add trailing slash and loop to create a new SmbFile fileURL.setPath(fileURL.getPath()+'/'); continue; } break; } catch(SmbException e) { // SmbFile.isDirectory() threw an exception. We distinguish 2 types of SmbException: // 1) SmbAuthException, caused by a credentials problem -> turn it into an AuthException and throw it // 2) any other SmbException -> this may happen if access to the file was denied for example, this // shouldn't prevent this SMBFile from being created. // 1) create an AuthException out of the SmbAuthException and throw it if(e instanceof SmbAuthException) throw new AuthException(fileURL, e.getMessage()); // 2) Swallow the exception to let this SMBFile be created break; } } } else { // The private constructor was called directly file = smbFile; } permissions = new SMBFilePermissions(file); } /** * Creates and returns a jcifs.smb.SmbFile for the given location. The credentials contained by * the {@link FileURL} (if any) are passed along to the SmbFile. * * @param url the location to the SmbFile file to create * @return an SmbFile corresponding to the given location * @throws MalformedURLException if an error occurred while creating the SmbFile instance */ private static SmbFile createSmbFile(FileURL url) throws MalformedURLException { Credentials credentials = url.getCredentials(); if (credentials == null) return new SmbFile(url.toString(false)); // Extract the domain (if any) from the username String login = credentials.getLogin(); String domain; int domainStart = login.indexOf(";"); if (domainStart != -1) { domain = login.substring(0, domainStart); login = login.substring(domainStart+1); } else { domain = null; } // A NtlmPasswordAuthentication is created from the FileURL credentials and passed to a specific SmbFile constructor. // The reason for doing this rather than using the SmbFile(String) constructor is that SmbFile uses java.net.URL // for the URL parsing which is unable to properly parse urls where the password contains a '@' character, // such as smb://user:p@ssword@host/path . return new SmbFile(url.toString(false), new NtlmPasswordAuthentication(domain, login, credentials.getPassword())); } /** * Background information: jcifs.smb.SmbFile is a tad cumbersome to work with because it requires its * file path to end with '/' when the file is a directory and vice-versa. * This method ensures that the path of the current jcifs.smb.SmbFile instance matches the * directory argument and if not, recreates it with the proper path. * * @param directory true if the current jcifs.smb.SmbFile designates a directory */ private void checkSmbFile(boolean directory) { try { String path = file.getURL().getPath(); boolean endsWithSeparator = path.endsWith("/"); if (directory) { if (!endsWithSeparator) { fileURL.setPath(path+"/"); file = createSmbFile(fileURL); } } else { if (endsWithSeparator) { fileURL.setPath(removeTrailingSeparator(path)); file = createSmbFile(fileURL); } } } catch(MalformedURLException e) { // This should never happen. If some reason wicked reason it ever did, SmbFile would just not be changed. } } /** * Sets the time period during which attributes values (e.g. isDirectory, last modified, ...) are cached by * jcifs.smb.SmbFile. The higher this value, the lower the number of network requests but also the longer it takes * before those attributes can be refreshed. * * @param period time period during which attributes values are cached, in milliseconds */ static void setAttributeCachingPeriod(long period) { jcifs.Config.setProperty("jcifs.smb.client.attrExpirationPeriod", ""+period); } ///////////////////////////////////////// // AbstractFile methods implementation // ///////////////////////////////////////// @Override public long getLastModifiedDate() { try { return file.lastModified(); } catch(SmbException e) { return 0; } } @Override public void setLastModifiedDate(long lastModified) throws IOException { file.setLastModified(lastModified); } @Override public long getSize() { try { return file.length(); } catch(SmbException e) { return 0; } } @Override public AbstractFile getParent() { if (!parentValSet) { FileURL parentURL = fileURL.getParent(); if (parentURL!=null) { parent = FileFactory.getFile(parentURL); // Note: parent may be null if it can't be resolved } // Note: do not make the special smb:// file a parent of smb://host/, this would cause parent unit tests to fail parentValSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValSet = true; } @Override public boolean exists() { // Unlike java.io.File, SmbFile.exists() can throw an SmbException try { return file.exists(); } catch(IOException e) { LOGGER.info("Exception caught while calling SmbFile#exists(): " + e.getMessage()); return e instanceof SmbAuthException; } } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { return CHANGEABLE_PERMISSIONS; } @Override public void changePermission(int access, int permission, boolean enabled) throws IOException { if (access!=USER_ACCESS || permission!=WRITE_PERMISSION) { throw new IOException(); } if (enabled) { file.setReadWrite(); } else { file.setReadOnly(); } } /** * Always returns null, this information is not available unfortunately. */ @Override public String getOwner() { return null; } /** * Always returns false, this information is not available unfortunately. */ @Override public boolean canGetOwner() { return false; } /** * Always returns null, this information is not available unfortunately. */ @Override public String getGroup() { return null; } /** * Always returns false, this information is not available unfortunately. */ @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { try { return file.isDirectory(); } catch(SmbException e) { return false; } } @Override public boolean isSymlink() { // Symlinks are not supported by jCIFS (or maybe by CIFS/SMB?) return false; } @Override public boolean isSystem() { return false; } @Override public InputStream getInputStream() throws IOException { return new SmbFileInputStream(file); } @Override public OutputStream getOutputStream() throws IOException { return new SmbFileOutputStream(file, false); } @Override public OutputStream getAppendOutputStream() throws IOException { return new SmbFileOutputStream(file, true); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { // This needs to be checked explicitly (SmbRandomAccessFile can be created even if the file does not exist) if(!exists()) throw new IOException(); // // Explicitly allow the file to be read/write/delete by another random access file while this one is open // return new SMBRandomAccessInputStream(new SmbRandomAccessFile(fileURL.toString(true), "r", SmbFile.FILE_SHARE_READ | SmbFile.FILE_SHARE_WRITE | SmbFile.FILE_SHARE_DELETE)); return new SMBRandomAccessInputStream(new SmbRandomAccessFile(file, "r")); } @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { // // Explicitly allow the file to be read/write/delete by another random access file while this one is open // return new SMBRandomAccessOutputStream(new SmbRandomAccessFile(fileURL.toString(true), "rw", SmbFile.FILE_SHARE_READ | SmbFile.FILE_SHARE_WRITE | SmbFile.FILE_SHARE_DELETE)); return new SMBRandomAccessOutputStream(new SmbRandomAccessFile(file, "rw")); } @Override public void delete() throws IOException { file.delete(); checkSmbFile(false); } @Override public AbstractFile[] ls() throws IOException { return ls(null); } @Override public void mkdir() throws IOException { // Ensure that the jcifs.smb.SmbFile's path ends with a '/' otherwise it will throw an exception checkSmbFile(true); // Note: unlike java.io.File.mkdir(), SmbFile does not return a boolean value // to indicate if the folder could be created file.mkdir(); } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // This method fails in situations where SmbFile#copyTo() doesn't, for instance: // - when the destination file exists (the destination is simply overwritten) // - when the source file doesn't exist checkCopyRemotelyPrerequisites(destFile, false, false); // Reuse the destination SmbFile instance SmbFile destSmbFile = ((SMBFile)destFile).file; // Remotely copy the file file.copyTo(destSmbFile); // Ensure that the destination jcifs.smb.SmbFile's path is consistent with its new directory/non-directory state ((SMBFile)destFile).checkSmbFile(file.isDirectory()); } /** * Implementation notes: server-to-server renaming will work if the destination file also uses the 'SMB' scheme. * Hosts do not necessarily have to be the same for this operation to succeed. */ @Override public void renameTo(AbstractFile destFile) throws IOException { // Throw an exception if the file cannot be renamed to the specified destination. // This method fails in situations where SFTPFile#renameTo() doesn't, for instance: // - when the source and destination are the same // - when the source file doesn't exist checkRenamePrerequisites(destFile, true, true); // Attempt to move the file using jcifs.smb.SmbFile#renameTo. boolean isDirectory = file.isDirectory(); // // SmbFile#renameTo() throws an IOException if the destination exists (instead of overwriting the file) // if(destFile.exists()) // destFile.delete(); // Rename the file file.renameTo(((SMBFile)destFile).file); // Ensure that the destination jcifs.smb.SmbFile's path is consistent with its new directory/non-directory state ((SMBFile)destFile).checkSmbFile(isDirectory); } @Override public long getFreeSpace() throws IOException { return file.getDiskFreeSpace(); } /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException always */ @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { // No way to retrieve this information with jCIFS throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } /** * Returns a jcifs.smb.SmbFile instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } //////////////////////// // Overridden methods // //////////////////////// @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { try { SmbFile smbFiles[] = file.listFiles(filenameFilter==null?null:new SMBFilenameFilter(filenameFilter)); if(smbFiles==null) throw new IOException(); // Count the number of files to exclude: excluded files are those that are not file share/ not browsable // (Printers, named pipes, comm ports) int nbSmbFiles = smbFiles.length; int nbSmbFilesToExclude = 0; int smbFileType; for (SmbFile smbFile1 : smbFiles) { smbFileType = smbFile1.getType(); if (smbFileType == SmbFile.TYPE_PRINTER || smbFileType == SmbFile.TYPE_NAMED_PIPE || smbFileType == SmbFile.TYPE_COMM) nbSmbFilesToExclude++; } // create SMBFile by using SmbFile instance and sharing parent instance among children AbstractFile children[] = new AbstractFile[nbSmbFiles-nbSmbFilesToExclude]; FileURL childURL; SmbFile smbFile; int currentIndex = 0; for (SmbFile smbFile1 : smbFiles) { smbFile = smbFile1; smbFileType = smbFile.getType(); if (smbFileType == SmbFile.TYPE_PRINTER || smbFileType == SmbFile.TYPE_NAMED_PIPE || smbFileType == SmbFile.TYPE_COMM) continue; // Note: properties and credentials are cloned for every children's url childURL = (FileURL) fileURL.clone(); childURL.setHost(smbFile.getServer()); childURL.setPath(smbFile.getURL().getPath()); // Use SMBFile private constructor to recycle the SmbFile instance children[currentIndex++] = FileFactory.getFile(childURL, this, smbFile); } return children; } catch(SmbAuthException e) { throw new AuthException(fileURL, e.getMessage()); } } @Override public boolean isHidden() { try { return file.isHidden(); } catch(SmbException e) { return false; } } @Override public boolean equalsCanonical(Object f) { if(!(f instanceof SMBFile)) return super.equalsCanonical(f); // could be equal to an AbstractArchiveFile // SmbFile's equals method is just perfect: compares canonical paths // and IP addresses return file.equals(((SMBFile)f).file); } /////////////////// // Inner classes // /////////////////// /** * SMBRandomAccessInputStream extends RandomAccessInputStream to provide random read access to an SMBFile. */ public static class SMBRandomAccessInputStream extends RandomAccessInputStream { private SmbRandomAccessFile raf; public SMBRandomAccessInputStream(SmbRandomAccessFile raf) { this.raf = raf; } @Override public int read() throws IOException { return raf.read(); } @Override public int read(byte b[], int off, int len) throws IOException { return raf.read(b, off, len); } @Override public void close() throws IOException { raf.close(); } public long getOffset() throws IOException { return raf.getFilePointer(); } public long getLength() throws IOException { return raf.length(); } public void seek(long offset) throws IOException { raf.seek(offset); } } /** * SMBRandomAccessOutputStream extends RandomAccessOutputStream to provide random write access to an SMBFile. */ public static class SMBRandomAccessOutputStream extends RandomAccessOutputStream { private SmbRandomAccessFile raf; public SMBRandomAccessOutputStream(SmbRandomAccessFile raf) { this.raf = raf; } @Override public void write(int i) throws IOException { raf.write(i); } @Override public void write(byte b[]) throws IOException { raf.write(b); } @Override public void write(byte b[], int off, int len) throws IOException { raf.write(b, off, len); } @Override public void close() throws IOException { raf.close(); } public long getOffset() throws IOException { return raf.getFilePointer(); } public long getLength() throws IOException { return raf.length(); } public void seek(long offset) throws IOException { raf.seek(offset); } @Override public void setLength(long newLength) throws IOException { raf.setLength(newLength); // jCIFS doesn't automatically position the offset to the end of the file when it is truncated. // We have to do it ourselves to honour this method's contract. if(getOffset()>newLength) raf.seek(newLength); } } /** * A Permissions implementation for SMBFile. */ private static class SMBFilePermissions extends IndividualPermissionBits implements FilePermissions { private SmbFile file; private final static PermissionBits MASK = new GroupedPermissionBits(384); // rw------- (300 octal) public SMBFilePermissions(SmbFile file) { this.file = file; } public boolean getBitValue(int access, int type) { if(access!=USER_ACCESS) return false; try { if(type==READ_PERMISSION) return file.canRead(); else if(type==WRITE_PERMISSION) return file.canWrite(); else return false; } // Unlike java.io.File, SmbFile#canRead() and SmbFile#canWrite() can throw an SmbException catch(SmbException e) { return false; } } public PermissionBits getMask() { return MASK; } } /** * Turns a {@link FilenameFilter} into a {@link jcifs.smb.SmbFilenameFilter}. */ private static class SMBFilenameFilter implements jcifs.smb.SmbFilenameFilter { private FilenameFilter filter; private SMBFilenameFilter(FilenameFilter filter) { this.filter = filter; } //////////////////////////////////////////////// // jicfs.smb.SmbFilenameFilter implementation // //////////////////////////////////////////////// public boolean accept(SmbFile dir, String name) { return filter.accept(name); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/smb/SMBProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.smb; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import jcifs.smb.SmbFile; import java.io.IOException; /** * This class is the provider for the SMB filesystem implemented by {@link com.mucommander.commons.file.impl.smb.SMBFile}. * * @author Nicolas Rinaudo, Maxence Bernard * @see com.mucommander.commons.file.impl.smb.SMBFile */ public class SMBProtocolProvider implements ProtocolProvider { static { // Silence jCIFS's output if not in debug mode // Quote from jCIFS's documentation : "0 - No log messages are printed -- not even crticial exceptions." System.setProperty("jcifs.util.loglevel", "0"); // Lower the timeout values // "The time period in milliseconds that the client will wait for a response to a request from the server. // The default value is 30000." System.setProperty("jcifs.smb.client.responseTimeout", "10000"); // "To prevent the client from holding server resources unnecessarily, sockets are closed after this time period // if there is no activity. This time is specified in milliseconds. The default is 35000." System.setProperty("jcifs.smb.client.soTimeout", "15000"); // Leaving this option enabled has a serious impact on performance (observed with jCIFS 1.2.25). // "If this property is true, domain based DFS referrals will be disabled. The default value is false. // This property can be important in non-domain environments where domain-based DFS referrals that normally run // when JCIFS first tries to resolve a path would timeout causing a long startup delay (e.g. running JCIFS only // on the local machine without a network like on a laptop)." System.setProperty("jcifs.smb.client.dfs.disabled", "true"); } /** * Sets the authentication protocol to use when connecting to SMB servers. This configuration method must be called * before {@link SMBFile} is first instantiated ; calling it after that will have no effect. *

    * This configuration option is mapped onto jCIFS's jcifs.smb.lmCompatibility client property. * jCIFS's default will be used if this method is not called. *

    * Here's a list of allowed values ; refer to JCIFS's documentation for more information: *

    *
    0,1
    Sends LM and NTLM responses
    *
    2
    Sends only the NTLM response. This is more secure than Levels 0 and 1, because it eliminates the * cryptographically-weak LM response
    *
    3,4,5
    Sends LMv2 and NTLMv2 data. NTLMv2 session security is also negotiated if the server supports * it. This is the default behavior (in 1.3.0 or later)
    *
    * * @param value one of the allowed values, refer to JCIFS's documentation for more information. */ public static void setSmbLmCompatibility(int value) { // Since jCIFS 1.3.0, the default is to use NTLM v2 authentication (value=3). // Note: jCIFS configuration is unfortunately global and cannot be set per connection. System.setProperty("jcifs.smb.lmCompatibility", Integer.toString(value)); } /** * Sets whether 'extended security' should be used when connecting to SMB servers. This configuration method * must be called before {@link SMBFile} is first instantiated ; calling it after that will have no effect. *

    * This configuration option is mapped onto jCIFS's jcifs.smb.client.useExtendedSecurity client * property. jCIFS's default value will be used if this method is not called, which is true since * jCIFS 1.3.0. * * @param value true to enable extended security, refer to JCIFS's documentation for more information. */ public static void setExtendedSecurity(boolean value) { // Since jCIFS 1.3.0, extended security is turned on by default, which causes issues when connecting to older // SMB servers such as Samba 3.0. // Note jCIFS configuration is unfortunately global and cannot be set per connection. System.setProperty("jcifs.smb.client.useExtendedSecurity", Boolean.toString(value)); } @Override public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return instantiationParams.length == 0 ? new SMBFile(url) : new SMBFile(url, (SmbFile)instantiationParams[0]); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/smb/package.html ================================================ Provides an implementation of the SMB protocol. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/TarArchiveFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.impl.tar; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.tar.provider.TarEntry; import com.mucommander.commons.file.impl.tar.provider.TarInputStream; import com.mucommander.commons.io.StreamUtils; import com.mucommander.commons.util.StringUtils; import org.apache.hadoop.io.compress.bzip2.CBZip2InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; /** * TarArchiveFile provides read-only access to archives in the Tar/Tgz format. * *

    The actual decompression work is performed by the Apache Ant library under the terms of the * Apache Software License. * * @see com.mucommander.commons.file.impl.tar.TarFormatProvider * @author Maxence Bernard */ public class TarArchiveFile extends AbstractROArchiveFile { private static final Logger LOGGER = LoggerFactory.getLogger(TarArchiveFile.class); /** * Creates a TarArchiveFile on of the given file. * * @param file the underlying archive file */ public TarArchiveFile(AbstractFile file) { super(file); } /** * Returns a TarInputStream which can be used to read TAR entries. * * @param entryOffset offset from the start of the archive to an entry. Must be a multiple of recordSize, or * 0 to start at the first entry. * @return a TarInputStream which can be used to read TAR entries * @throws IOException if an error occurred while create the stream */ private TarInputStream createTarStream(long entryOffset) throws IOException { InputStream in = file.getInputStream(); String name = getName(); // Gzip-compressed file if (StringUtils.endsWithIgnoreCase(name, "tgz") || StringUtils.endsWithIgnoreCase(name, "tar.gz")) // Note: this will fail for gz/tgz entries inside a tar file (IOException: Not in GZIP format), // why is a complete mystery: the gz/tgz entry can be extracted and then properly browsed in = new GZIPInputStream(in); // Bzip2-compressed file else if (StringUtils.endsWithIgnoreCase(name, "tbz2") || StringUtils.endsWithIgnoreCase(name, "tar.bz2")) { try { // Skips the 2 magic bytes 'BZ', as required by CBZip2InputStream. Quoted from CBZip2InputStream's Javadoc: // "Although BZip2 headers are marked with the magic 'Bz'. this constructor expects the next byte in the // stream to be the first one after the magic. Thus callers have to skip the first two bytes. Otherwise // this constructor will throw an exception." StreamUtils.skipFully(in, 2); // Quoted from CBZip2InputStream's Javadoc: // "CBZip2InputStream reads bytes from the compressed source stream via the single byte {@link java.io.InputStream#read() // read()} method exclusively. Thus you should consider to use a buffered source stream." in = new CBZip2InputStream(new BufferedInputStream(in)); } catch (Exception e) { // CBZip2InputStream is known to throw NullPointerException if file is not properly Bzip2-encoded // so we need to catch those and throw them as IOException LOGGER.info("Exception caught while creating CBZip2InputStream, throwing IOException", e); throw new IOException(); } } return new TarInputStream(in, entryOffset); } //////////////////////////////////////// // AbstractArchiveFile implementation // //////////////////////////////////////// @Override public ArchiveEntryIterator getEntryIterator() throws IOException { return new TarEntryIterator(createTarStream(0)); } @Override public InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException { if (entry.isDirectory()) { throw new IOException(); } // Optimization: first check if the specified iterator is positionned at the beginning of the entry. // This will typically be the case if an iterator is being used to read all the archive's entries // (unpack operation). In that case, we save the cost of looking for the entry in the archive, which is all // the more expensive if the TAR archive is GZipped. if ((entryIterator instanceof TarEntryIterator)) { ArchiveEntry currentEntry = ((TarEntryIterator)entryIterator).getCurrentEntry(); if (currentEntry.getPath().equals(entry.getPath())) { // The entry/tar stream is wrapped in a FilterInputStream where #close is implemented as a no-op: // we don't want the TarInputStream to be closed when the caller closes the entry's stream. return new FilterInputStream(((TarEntryIterator)entryIterator).getTarInputStream()) { @Override public void close() { // No-op } }; } // This is not the one, look for the entry from the beginning of the archive } // Iterate through the archive until we've found the entry TarEntry tarEntry = (TarEntry)entry.getEntryObject(); if (tarEntry != null) { TarInputStream tin = createTarStream(tarEntry.getOffset()); tin.getNextEntry(); return tin; } throw new IOException("Unknown TAR entry: "+entry.getName()); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/TarEntryIterator.java ================================================ package com.mucommander.commons.file.impl.tar; import com.mucommander.commons.file.ArchiveEntry; import com.mucommander.commons.file.ArchiveEntryIterator; import com.mucommander.commons.file.PermissionBits; import com.mucommander.commons.file.SimpleFilePermissions; import com.mucommander.commons.file.impl.tar.provider.TarEntry; import com.mucommander.commons.file.impl.tar.provider.TarInputStream; import java.io.IOException; /** * An ArchiveEntryIterator that iterates through a {@link TarInputStream}. * * @author Maxence Bernard */ class TarEntryIterator implements ArchiveEntryIterator { /** InputStream to the archive file */ private final TarInputStream tin; /** The current entry, where the TarInputStream is currently positionned */ private ArchiveEntry currentEntry; /** * Creates a new TarEntryIterator that iterates through the entries of the given {@link TarInputStream}. * * @param tin the TarInputStream to iterate through */ TarEntryIterator(TarInputStream tin) { this.tin = tin; } /** * Returns the {@link TarInputStream} instance that was used to create this object. * * @return the {@link TarInputStream} instance that was used to create this object. */ TarInputStream getTarInputStream() { return tin; } /** * Returns the current entry where the {@link #getTarInputStream()} TarInputStream} is currently positionned. * The returned value is null until {@link #nextEntry()} is called for the first time. * * @return the current entry where the {@link #getTarInputStream()} TarInputStream} is currently positionned. */ ArchiveEntry getCurrentEntry() { return currentEntry; } /** * Creates and return an {@link ArchiveEntry()} whose attributes are fetched from the given * org.apache.tools.tar.TarEntry. * * @param tarEntry the object that serves to initialize the attributes of the returned ArchiveEntry * @return an ArchiveEntry whose attributes are fetched from the given org.apache.tools.tar.TarEntry */ private ArchiveEntry createArchiveEntry(TarEntry tarEntry) { ArchiveEntry entry = new ArchiveEntry(tarEntry.getName(), tarEntry.isDirectory(), tarEntry.getModTime().getTime(), tarEntry.getSize(), true); entry.setPermissions(new SimpleFilePermissions(tarEntry.getMode() & PermissionBits.FULL_PERMISSION_INT)); entry.setOwner(tarEntry.getUserName()); entry.setGroup(tarEntry.getGroupName()); entry.setEntryObject(tarEntry); return entry; } /** * Advances the {@link TarInputStream} to the next entry and returns the corresponding {@link ArchiveEntry}. * * @return the next ArchiveEntry * @throws IOException if an I/O error occurred */ private ArchiveEntry getNextEntry() throws IOException { TarEntry entry = tin.getNextEntry(); return entry == null ? null : createArchiveEntry(entry); } ///////////////////////////////////////// // ArchiveEntryIterator implementation // ///////////////////////////////////////// public ArchiveEntry nextEntry() throws IOException { // Get the next entry, if any this.currentEntry = getNextEntry(); return currentEntry; } public void close() throws IOException { tin.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/TarFormatProvider.java ================================================ package com.mucommander.commons.file.impl.tar; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import java.io.IOException; /** * This class is the provider for the 'Tar' archive format implemented by {@link TarArchiveFile}. * * @see com.mucommander.commons.file.impl.tar.TarArchiveFile * @author Nicolas Rinaudo, Maxence Bernard */ public class TarFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".cbt"}; /** * Static instance of the filename filter that matches archive filenames * */ private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new TarArchiveFile(file); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/package.html ================================================ Provides an implementation of the tar archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/provider/TarBuffer.java ================================================ package com.mucommander.commons.file.impl.tar.provider; import com.mucommander.commons.io.BufferPool; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; /** * The TarBuffer class implements the tar archive concept * of a buffered input stream. This concept goes back to the * days of blocked tape drives and special io devices. In the * Java universe, the only real function that this class * performs is to ensure that files have the correct "block" * size, or other tars will complain. *

    * You should never have a need to access this class directly. * TarBuffers are created by Tar IO Streams. * *

    This class is based off the org.apache.tools.tar package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.1 of Ant. * * @author Apache Ant, Maxence Bernard */ public class TarBuffer { /** Default record size */ static final int DEFAULT_RCDSIZE = (512); /** Default block size */ static final int DEFAULT_BLKSIZE = (DEFAULT_RCDSIZE * 20); private InputStream inStream; private OutputStream outStream; private byte[] blockBuffer; private int currBlkIdx; private int currRecIdx; private int blockSize; private int recordSize; private int recsPerBlock; private boolean debug; /** * Constructor for a TarBuffer on an input stream. * @param inStream the input stream to use */ public TarBuffer(InputStream inStream) { this(inStream, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); } /** * Constructor for a TarBuffer on an input stream. * @param inStream the input stream to use * @param blockSize the block size to use * @param recordSize the record size to use */ TarBuffer(InputStream inStream, int blockSize, int recordSize) { this.inStream = inStream; this.outStream = null; this.initialize(blockSize, recordSize); } /** * Constructor for a TarBuffer on an output stream. * @param outStream the output stream to use */ public TarBuffer(OutputStream outStream) { this(outStream, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); } /** * Constructor for a TarBuffer on an output stream. * @param outStream the output stream to use * @param blockSize the block size to use * @param recordSize the record size to use */ TarBuffer(OutputStream outStream, int blockSize, int recordSize) { this.inStream = null; this.outStream = outStream; this.initialize(blockSize, recordSize); } /** * Initialization common to all constructors. * * @param blockSize the block size to use * @param recordSize the record size to use */ private void initialize(int blockSize, int recordSize) { this.debug = false; this.blockSize = blockSize; this.recordSize = recordSize; this.recsPerBlock = (this.blockSize / this.recordSize); this.blockBuffer = BufferPool.getByteArray(this.blockSize); if (this.inStream != null) { this.currBlkIdx = -1; this.currRecIdx = this.recsPerBlock; } else { this.currBlkIdx = 0; this.currRecIdx = 0; } } /** * Get the TAR Buffer's block size. Blocks consist of multiple records. * @return the block size */ int getBlockSize() { return this.blockSize; } /** * Get the TAR Buffer's record size. * @return the record size */ int getRecordSize() { return this.recordSize; } /** * Set the debugging flag for the buffer. * * @param debug If true, print debugging output. */ public void setDebug(boolean debug) { this.debug = debug; } /** * Determine if an archive record indicate End of Archive. End of * archive is indicated by a record that consists entirely of null bytes. * * @param record The record data to check. * @return true if the record data is an End of Archive */ boolean isEOFRecord(byte[] record) { for (int i = 0, sz = getRecordSize(); i < sz; ++i) { if (record[i] != 0) { return false; } } return true; } /** * Skip over a record on the input stream. * * @return true if the record has been skipped, false if EOF has been reached * @throws IOException on error */ boolean skipRecord() throws IOException { if (debug) { System.err.println("SkipRecord: recIdx = " + currRecIdx + " blkIdx = " + currBlkIdx); } if (inStream == null) { throw new IOException("reading (via skip) from an output buffer"); } if (currRecIdx >= recsPerBlock) { if (!readBlock()) { return false; } } currRecIdx++; return true; } /** * Read a record from the input stream and stores it into the specified buffer. * * @param recordBuf the buffer into which the record will be stored. Its length must be {@link #getRecordSize()}. * @return true if the record has been read, false if EOF has been reached * @throws IOException on error */ boolean readRecord(byte[] recordBuf) throws IOException { if (debug) { System.err.println("ReadRecord: recIdx = " + currRecIdx + " blkIdx = " + currBlkIdx); } if(recordBuf.length!=recordSize) throw new IOException("specified record buffer doesn't match record size: "+recordSize); if (inStream == null) { throw new IOException("reading from an output buffer"); } if (currRecIdx >= recsPerBlock) { if (!readBlock()) { return false; } } System.arraycopy(blockBuffer, (currRecIdx * recordSize), recordBuf, 0, recordSize); currRecIdx++; return true; } /** * Read a block from the input stream and stores it into the block buffer. * * @return true if a block was read, false if EOF was reached * @throws IOException on error */ private boolean readBlock() throws IOException { if (debug) { System.err.println("readBlock: blkIdx = " + currBlkIdx); } if (inStream == null) { throw new IOException("reading from an output buffer"); } currRecIdx = 0; int offset = 0; int bytesNeeded = blockSize; while (bytesNeeded > 0) { long numBytes = inStream.read(blockBuffer, offset, bytesNeeded); // // NOTE // We have fit EOF, and the block is not full! // // This is a broken archive. It does not follow the standard // blocking algorithm. However, because we are generous, and // it requires little effort, we will simply ignore the error // and continue as if the entire block were read. This does // not appear to break anything upstream. We used to return // false in this case. // // Thanks to 'Yohann.Roussel@alcatel.fr' for this fix. // if (numBytes == -1) { if (offset == 0) { // Ensure that we do not read gigabytes of zeros // for a corrupt tar file. // See http://issues.apache.org/bugzilla/show_bug.cgi?id=39924 return false; } // However, just leaving the unread portion of the buffer dirty does // cause problems in some cases. This problem is described in // http://issues.apache.org/bugzilla/show_bug.cgi?id=29877 // // The solution is to fill the unused portion of the buffer with zeros. Arrays.fill(blockBuffer, offset, offset + bytesNeeded, (byte) 0); break; } offset += numBytes; bytesNeeded -= numBytes; if (numBytes != blockSize) { if (debug) { System.err.println("readBlock: INCOMPLETE READ " + numBytes + " of " + blockSize + " bytes read."); } } } currBlkIdx++; return true; } /** * Skip over a block on the input stream. * * @return true if a block was read, false if EOF was reached * @throws IOException on error */ boolean skipBlock() throws IOException { int bytesToSkip = blockSize; int noSkipCnt = 0; while (bytesToSkip > 0) { long numBytes = inStream.skip(bytesToSkip); if (numBytes == 0) { noSkipCnt++; if (noSkipCnt >= 10) { return false; } try { Thread.sleep(10); } catch (InterruptedException ignore) {} } // Adopt the same 'generous' behavior as #readBlock(), i.e. allow a premature EOF only if at least // a byte was properly skipped. if (numBytes == -1) { return bytesToSkip != blockSize; } bytesToSkip -= numBytes; } currBlkIdx++; currRecIdx = recsPerBlock; return true; } /** * Get the current block number, zero based. * * @return The current zero based block number. */ int getCurrentBlockNum() { return currBlkIdx; } /** * Sets the current block number, zero based. * * @param blockNum the current block number, zero based */ public void setCurrentBlockNum(int blockNum) { this.currBlkIdx = blockNum; } /** * Get the current record number, within the current block, zero based. * Thus, current offset = (currentBlockNum * recsPerBlk) + currentRecNum. * * @return The current zero based record number. */ int getCurrentRecordNum() { return currRecIdx - 1; } /** * Returns the number of records per block. * * @return the number of records per block */ int getRecordsPerBlock() { return recsPerBlock; } /** * Write an archive record to the archive. * * @param record The record data to write to the archive. * @throws IOException on error */ void writeRecord(byte[] record) throws IOException { if (debug) { System.err.println("WriteRecord: recIdx = " + currRecIdx + " blkIdx = " + currBlkIdx); } if (outStream == null) { throw new IOException("writing to an input buffer"); } if (record.length != recordSize) { throw new IOException("record to write has length '" + record.length + "' which is not the record size of '" + recordSize + "'"); } if (currRecIdx >= recsPerBlock) { writeBlock(); } System.arraycopy(record, 0, blockBuffer, (currRecIdx * recordSize), recordSize); currRecIdx++; } /** * Write an archive record to the archive, where the record may be * inside of a larger array buffer. The buffer must be "offset plus * record size" long. * * @param buf The buffer containing the record data to write. * @param offset The offset of the record data within buf. * @throws IOException on error */ void writeRecord(byte[] buf, int offset) throws IOException { if (debug) { System.err.println("WriteRecord: recIdx = " + currRecIdx + " blkIdx = " + currBlkIdx); } if (outStream == null) { throw new IOException("writing to an input buffer"); } if ((offset + recordSize) > buf.length) { throw new IOException("record has length '" + buf.length + "' with offset '" + offset + "' which is less than the record size of '" + recordSize + "'"); } if (currRecIdx >= recsPerBlock) { writeBlock(); } System.arraycopy(buf, offset, blockBuffer, (currRecIdx * recordSize), recordSize); currRecIdx++; } /** * Write a TarBuffer block to the archive. */ private void writeBlock() throws IOException { if (debug) { System.err.println("WriteBlock: blkIdx = " + currBlkIdx); } if (outStream == null) { throw new IOException("writing to an input buffer"); } outStream.write(blockBuffer, 0, blockSize); outStream.flush(); currRecIdx = 0; currBlkIdx++; } /** * Flush the current data block if it has any data in it. */ private void flushBlock() throws IOException { if (debug) { System.err.println("TarBuffer.flushBlock() called."); } if (outStream == null) { throw new IOException("writing to an input buffer"); } if (currRecIdx > 0) { writeBlock(); } } /** * Close the TarBuffer. If this is an output buffer, also flush the * current block before closing. * @throws IOException on error */ public void close() throws IOException { if (debug) { System.err.println("TarBuffer.closeBuffer()."); } try { if (outStream != null) { flushBlock(); if (outStream != System.out && outStream != System.err) { outStream.close(); outStream = null; } } else if (inStream != null) { if (inStream != System.in) { inStream.close(); inStream = null; } } } finally { if (blockBuffer!=null) { BufferPool.releaseByteArray(blockBuffer); blockBuffer = null; } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/provider/TarConstants.java ================================================ package com.mucommander.commons.file.impl.tar.provider; /** * This interface contains all the definitions used in the package. *

    * This class is based off the org.apache.tools.tar package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.1 of Ant. * * @author Apache Ant */ public interface TarConstants { /** * The length of the name field in a header buffer. */ int NAMELEN = 100; /** * The length of the mode field in a header buffer. */ int MODELEN = 8; /** * The length of the user id field in a header buffer. */ int UIDLEN = 8; /** * The length of the group id field in a header buffer. */ int GIDLEN = 8; /** * The length of the checksum field in a header buffer. */ int CHKSUMLEN = 8; /** * The length of the size field in a header buffer. */ int SIZELEN = 12; /** * The maximum size of a file in a tar archive (That's 11 sevens, octal). */ long MAXSIZE = 077777777777L; /** * The length of the magic field in a header buffer. */ int MAGICLEN = 8; /** * The length of the modification time field in a header buffer. */ int MODTIMELEN = 12; /** * The length of the user name field in a header buffer. */ int UNAMELEN = 32; /** * The length of the group name field in a header buffer. */ int GNAMELEN = 32; /** * The length of the devices field in a header buffer. */ int DEVLEN = 8; /** * LF_ constants represent the "link flag" of an entry, or more commonly, * the "entry type". This is the "old way" of indicating a normal file. */ byte LF_OLDNORM = 0; /** * Normal file type. */ byte LF_NORMAL = (byte) '0'; /** * Link file type. */ byte LF_LINK = (byte) '1'; /** * Symbolic link file type. */ byte LF_SYMLINK = (byte) '2'; /** * Character device file type. */ byte LF_CHR = (byte) '3'; /** * Block device file type. */ byte LF_BLK = (byte) '4'; /** * Directory file type. */ byte LF_DIR = (byte) '5'; /** * FIFO (pipe) file type. */ byte LF_FIFO = (byte) '6'; /** * Contiguous file type. */ byte LF_CONTIG = (byte) '7'; /** * The magic tag representing a POSIX tar archive. */ String TMAGIC = "ustar"; /** * The magic tag representing a GNU tar archive. */ String GNU_TMAGIC = "ustar "; /** * The namr of the GNU tar entry which contains a long name. */ String GNU_LONGLINK = "././@LongLink"; /** * Identifies the *next* file on the tape as having a long name. */ byte LF_GNUTYPE_LONGNAME = (byte) 'L'; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/provider/TarEntry.java ================================================ package com.mucommander.commons.file.impl.tar.provider; import java.util.Date; /** * This class represents an entry in a Tar archive. It consists * of the entry's header, as well as the entry's File. Entries * can be instantiated in one of three ways, depending on how * they are to be used. *

    * TarEntries that are created from the header bytes read from * an archive are instantiated with the TarEntry( byte[] ) * constructor. These entries will be used when extracting from * or listing the contents of an archive. These entries have their * header filled in using the header bytes. They also set the File * to null, since they reference an archive entry not a file. *

    * TarEntries that are created from Files that are to be written * into an archive are instantiated with the TarEntry( File ) * constructor. These entries have their header filled in using * the File's information. They also keep a reference to the File * for convenience when writing entries. *

    * Finally, TarEntries can be constructed from nothing but a name. * This allows the programmer to construct the entry by hand, for * instance when only an InputStream is available for writing to * the archive, and the header information is constructed from * other information. In this case the header fields are set to * defaults and the File is set to null. * *

    * The C structure for a Tar Entry's header is: *

     * struct header {
     * char name[NAMSIZ];
     * char mode[8];
     * char uid[8];
     * char gid[8];
     * char size[12];
     * char mtime[12];
     * char chksum[8];
     * char linkflag;
     * char linkname[NAMSIZ];
     * char magic[8];
     * char uname[TUNMLEN];
     * char gname[TGNMLEN];
     * char devmajor[8];
     * char devminor[8];
     * } header;
     * 
    * *

    This class is based off the org.apache.tools.tar package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.1 of Ant. * * @author Apache Ant, Maxence Bernard */ public class TarEntry implements TarConstants { /** The entry's name. */ private StringBuffer name; /** The entry's permission mode. */ private int mode; /** The entry's user id. */ private int userId; /** The entry's group id. */ private int groupId; /** The entry's size. */ private long size; /** The entry's modification time. */ private long modTime; /** The entry's link flag. */ private byte linkFlag; /** The entry's link name. */ private StringBuffer linkName; /** The entry's magic tag. */ private StringBuffer magic; /** The entry's user name. */ private StringBuffer userName; /** The entry's group name. */ private StringBuffer groupName; /** The entry's major device number. */ private int devMajor; /** The entry's minor device number. */ private int devMinor; /** The entry's offset from the start of the archive */ private long offset; /** Maximum length of a user's name in the tar file */ public static final int MAX_NAMELEN = 31; /** Default permissions bits for directories */ public static final int DEFAULT_DIR_MODE = 040755; /** Default permissions bits for files */ public static final int DEFAULT_FILE_MODE = 0100644; /** Convert millis to seconds */ public static final int MILLIS_PER_SECOND = 1000; /** * Construct an empty entry and prepares the header values. */ private TarEntry () { this.magic = new StringBuffer(TMAGIC); this.name = new StringBuffer(); this.linkName = new StringBuffer(); String user = System.getProperty("user.name", ""); if (user.length() > MAX_NAMELEN) { user = user.substring(0, MAX_NAMELEN); } this.userId = 0; this.groupId = 0; this.userName = new StringBuffer(user); this.groupName = new StringBuffer(); } /** * Construct an entry with only a name. This allows the programmer * to construct the entry's header "by hand". File is set to null. * * @param name the entry name */ public TarEntry(String name) { this(); boolean isDir = name.endsWith("/"); this.devMajor = 0; this.devMinor = 0; this.name = new StringBuffer(name); this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE; this.linkFlag = isDir ? LF_DIR : LF_NORMAL; this.userId = 0; this.groupId = 0; this.size = 0; this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND; this.linkName = new StringBuffer(); this.userName = new StringBuffer(); this.groupName = new StringBuffer(); this.devMajor = 0; this.devMinor = 0; } /** * Construct an entry with a name and a link flag. * * @param name the entry name * @param linkFlag the entry link flag. */ public TarEntry(String name, byte linkFlag) { this(name); this.linkFlag = linkFlag; } /** * Construct an entry from an archive's header bytes. File is set * to null. * * @param headerBuf The header bytes from a tar archive entry. */ public TarEntry(byte[] headerBuf) { this(); parseTarHeader(headerBuf); } /** * Determine if the two entries are equal. Equality is determined * by the header names being equal. * * @param it Entry to be checked for equality. * @return True if the entries are equal. */ public boolean equals(TarEntry it) { return getName().equals(it.getName()); } /** * Determine if the two entries are equal. Equality is determined * by the header names being equal. * * @param it Entry to be checked for equality. * @return True if the entries are equal. */ public boolean equals(Object it) { if (it == null || getClass() != it.getClass()) { return false; } return equals((TarEntry) it); } /** * Hashcodes are based on entry names. * * @return the entry hashcode */ public int hashCode() { return getName().hashCode(); } /** * Determine if the given entry is a descendant of this entry. * Descendancy is determined by the name of the descendant * starting with this entry's name. * * @param desc Entry to be checked as a descendent of this. * @return True if entry is a descendant of this. */ public boolean isDescendent(TarEntry desc) { return desc.getName().startsWith(getName()); } /** * Get this entry's name. * * @return This entry's name. */ public String getName() { return name.toString(); } /** * Set this entry's name. * * @param name This entry's new name. */ public void setName(String name) { this.name = new StringBuffer(name); } /** * Set the mode for this entry * * @param mode the mode for this entry */ public void setMode(int mode) { this.mode = mode; } /** * Get this entry's link name. * * @return This entry's link name. */ public String getLinkName() { return linkName.toString(); } /** * Get this entry's user id. * * @return This entry's user id. */ public int getUserId() { return userId; } /** * Set this entry's user id. * * @param userId This entry's new user id. */ public void setUserId(int userId) { this.userId = userId; } /** * Get this entry's group id. * * @return This entry's group id. */ public int getGroupId() { return groupId; } /** * Set this entry's group id. * * @param groupId This entry's new group id. */ public void setGroupId(int groupId) { this.groupId = groupId; } /** * Get this entry's user name. * * @return This entry's user name. */ public String getUserName() { return userName.toString(); } /** * Set this entry's user name. * * @param userName This entry's new user name. */ public void setUserName(String userName) { this.userName = new StringBuffer(userName); } /** * Get this entry's group name. * * @return This entry's group name. */ public String getGroupName() { return groupName.toString(); } /** * Set this entry's group name. * * @param groupName This entry's new group name. */ public void setGroupName(String groupName) { this.groupName = new StringBuffer(groupName); } /** * Convenience method to set this entry's group and user ids. * * @param userId This entry's new user id. * @param groupId This entry's new group id. */ public void setIds(int userId, int groupId) { setUserId(userId); setGroupId(groupId); } /** * Convenience method to set this entry's group and user names. * * @param userName This entry's new user name. * @param groupName This entry's new group name. */ public void setNames(String userName, String groupName) { setUserName(userName); setGroupName(groupName); } /** * Set this entry's modification time. The parameter passed * to this method is in "Java time". * * @param time This entry's new modification time. */ public void setModTime(long time) { modTime = time / MILLIS_PER_SECOND; } /** * Set this entry's modification time. * * @param time This entry's new modification time. */ public void setModTime(Date time) { modTime = time.getTime() / MILLIS_PER_SECOND; } /** * Set this entry's modification time. * * @return time This entry's new modification time. */ public Date getModTime() { return new Date(modTime * MILLIS_PER_SECOND); } /** * Get this entry's mode. * * @return This entry's mode. */ public int getMode() { return mode; } /** * Get this entry's file size. * * @return This entry's file size. */ public long getSize() { return size; } /** * Set this entry's file size. * * @param size This entry's new file size. */ public void setSize(long size) { this.size = size; } /** * Returns this entry's offset from the start of the archive. * * @return this entry's offset from the start of the archive */ public long getOffset() { return offset; } /** * Sets this entry's offset from the start of the archive. * * @param offset this entry's offset from the start of the archive */ public void setOffset(long offset) { this.offset = offset; } /** * Indicate if this entry is a GNU long name block * * @return true if this is a long name extension provided by GNU tar */ public boolean isGNULongNameEntry() { return linkFlag == LF_GNUTYPE_LONGNAME && name.toString().equals(GNU_LONGLINK); } /** * Return whether this entry represents a directory. * * @return True if this entry is a directory. */ public boolean isDirectory() { if (linkFlag == LF_DIR) { return true; } return getName().endsWith("/"); } /** * Write an entry's header information to a header buffer. * * @param outbuf The tar entry header buffer to fill in. */ public void writeEntryHeader(byte[] outbuf) { int offset = 0; offset = TarUtils.getNameBytes(name, outbuf, offset, NAMELEN); offset = TarUtils.getOctalBytes(mode, outbuf, offset, MODELEN); offset = TarUtils.getOctalBytes(userId, outbuf, offset, UIDLEN); offset = TarUtils.getOctalBytes(groupId, outbuf, offset, GIDLEN); offset = TarUtils.getLongOctalBytes(size, outbuf, offset, SIZELEN); offset = TarUtils.getLongOctalBytes(modTime, outbuf, offset, MODTIMELEN); int csOffset = offset; for (int c = 0; c < CHKSUMLEN; ++c) { outbuf[offset++] = (byte) ' '; } outbuf[offset++] = linkFlag; offset = TarUtils.getNameBytes(linkName, outbuf, offset, NAMELEN); offset = TarUtils.getNameBytes(magic, outbuf, offset, MAGICLEN); offset = TarUtils.getNameBytes(userName, outbuf, offset, UNAMELEN); offset = TarUtils.getNameBytes(groupName, outbuf, offset, GNAMELEN); offset = TarUtils.getOctalBytes(devMajor, outbuf, offset, DEVLEN); offset = TarUtils.getOctalBytes(devMinor, outbuf, offset, DEVLEN); while (offset < outbuf.length) { outbuf[offset++] = 0; } long chk = TarUtils.computeCheckSum(outbuf); TarUtils.getCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN); } /** * Parse an entry's header information from a header buffer. * * @param header The tar entry header buffer to get information from. */ public void parseTarHeader(byte[] header) { int offset = 0; name = TarUtils.parseName(header, offset, NAMELEN); offset += NAMELEN; mode = (int) TarUtils.parseOctal(header, offset, MODELEN); offset += MODELEN; userId = (int) TarUtils.parseOctal(header, offset, UIDLEN); offset += UIDLEN; groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN); offset += GIDLEN; size = TarUtils.parseOctal(header, offset, SIZELEN); offset += SIZELEN; modTime = TarUtils.parseOctal(header, offset, MODTIMELEN); offset += MODTIMELEN; offset += CHKSUMLEN; linkFlag = header[offset++]; linkName = TarUtils.parseName(header, offset, NAMELEN); offset += NAMELEN; magic = TarUtils.parseName(header, offset, MAGICLEN); offset += MAGICLEN; userName = TarUtils.parseName(header, offset, UNAMELEN); offset += UNAMELEN; groupName = TarUtils.parseName(header, offset, GNAMELEN); offset += GNAMELEN; devMajor = (int) TarUtils.parseOctal(header, offset, DEVLEN); offset += DEVLEN; devMinor = (int) TarUtils.parseOctal(header, offset, DEVLEN); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/provider/TarInputStream.java ================================================ package com.mucommander.commons.file.impl.tar.provider; import com.mucommander.commons.io.BufferPool; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; /** * The TarInputStream reads a UNIX tar archive as an InputStream. * methods are provided to position at each successive entry in * the archive, and the read each entry as a normal input stream * using read(). * *

    This class is based off the org.apache.tools.tar package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.1 of Ant. * * @author Apache Ant, Maxence Bernard */ public class TarInputStream extends InputStream { private static final int NAME_BUFFER_SIZE = 256; private static final int BYTE_MASK = 0xFF; protected boolean debug; protected boolean hasHitEOF; protected long entrySize; protected long entryOffset; protected byte[] recordBuf; protected byte[] nameBuf; protected int recordBufPos; protected int recordBufLeft; protected TarBuffer buffer; protected TarEntry currEntry; protected boolean closed; /** * This contents of this array is not used at all in this class, * it is only here to avoid repreated object creation during calls * to the no-arg read method. */ protected byte[] oneBuf; /** * Creates a new TarInputStream over the specified input stream using the default block size and * record size and starting at the first entry. * * @param is the input stream providing the actual TAR data * @throws IOException if an error ocurred while initializing the stream */ public TarInputStream(InputStream is) throws IOException { this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE, 0); } /** * Creates a new TarInputStream over the specified input stream, starting at the specified * entry offset. * * @param is the input stream providing the actual TAR data * @param entryOffset offset from the start of the archive to an entry. Must be a multiple of recordSize, or * 0 to start at the first entry. * @throws IOException if an error ocurred while initializing the stream */ public TarInputStream(InputStream is, long entryOffset) throws IOException { this(is, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE, entryOffset); } /** * Creates a new TarInputStream over the specified input stream, using the specified * block size, record size and start offset. * * @param is the input stream to use * @param blockSize the block size to use * @param recordSize the record size to use * @param entryOffset offset from the start of the archive to an entry. Must be a multiple of recordSize, or * 0 to start at the first entry. * @throws IOException if an error ocurred while initializing the stream */ public TarInputStream(InputStream is, int blockSize, int recordSize, long entryOffset) throws IOException { this.buffer = new TarBuffer(is, blockSize, recordSize); this.recordBuf = BufferPool.getByteArray(buffer.getRecordSize()); this.nameBuf = BufferPool.getByteArray(NAME_BUFFER_SIZE); this.oneBuf = BufferPool.getByteArray(1); this.debug = false; this.hasHitEOF = false; if (entryOffset > 0) { if ((entryOffset % recordSize) != 0) throw new IllegalArgumentException("entryOffset ("+entryOffset+") is not a multiple of recordSize ("+recordSize+")"); skipBytes(entryOffset); } } /** * Sets the debugging flag. * * @param debug True to turn on debugging. */ public void setDebug(boolean debug) { this.debug = debug; buffer.setDebug(debug); } /** * Closes this stream. Calls the TarBuffer's close() method. * @throws IOException on error */ @Override public void close() throws IOException { if (!closed) { try { buffer.close(); } finally { BufferPool.releaseByteArray(recordBuf); BufferPool.releaseByteArray(nameBuf); BufferPool.releaseByteArray(oneBuf); closed = true; } } } /** * Get the record size being used by this stream's TarBuffer. * * @return The TarBuffer record size. */ public int getRecordSize() { return buffer.getRecordSize(); } /** * Get the available data that can be read from the current * entry in the archive. This does not indicate how much data * is left in the entire archive, only in the current entry. * This value is determined from the entry's size header field * and the amount of data already read from the current entry. * Integer.MAX_VALUE is returen in case more than Integer.MAX_VALUE * bytes are left in the current entry in the archive. * * @return The number of available bytes for the current entry. */ @Override public int available() { if (entrySize - entryOffset > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } return (int) (entrySize - entryOffset); } /** * Since we do not support marking just yet, we return false. * * @return False. */ @Override public boolean markSupported() { return false; } /** * Since we do not support marking just yet, we do nothing. * * @param markLimit The limit to mark. */ @Override public void mark(int markLimit) { } /** * Since we do not support marking just yet, we do nothing. */ @Override public void reset() { } /** * Reads a whole new record from the {@link TarBuffer} into the {@link #recordBuf record buffer} and resets * {@link #recordBufPos} and {@link #recordBufLeft} fields accordingly. * * @return true if the record has been read, false if EOF has been reached * @throws IOException on error */ public boolean readRecord() throws IOException { boolean ret = buffer.readRecord(recordBuf); recordBufPos = 0; recordBufLeft = ret?recordBuf.length:0; return ret; } /** * Returns the current entry where this TarInputStream is currently positionned. * * @return the current entry where this TarInputStream is currently positionned */ public TarEntry getCurrentEntry() { return currEntry; } /** * Get the next entry in this tar archive. This will skip * over any remaining data in the current entry, if there * is one, and place the input stream at the header of the * next entry, and read the header and instantiate a new * TarEntry from the header bytes and return that entry. * If there are no more entries in the archive, null will * be returned to indicate that the end of the archive has * been reached. * * @return The next TarEntry in the archive, or null. * @throws IOException on error */ public TarEntry getNextEntry() throws IOException { if (hasHitEOF) { return null; } if (currEntry != null) { long numToSkip = entrySize - entryOffset; if (debug) { System.err.println("TarInputStream: SKIP currENTRY '" + currEntry.getName() + "' SZ " + entrySize + " OFF " + entryOffset + " skipping " + numToSkip + " bytes"); } if (numToSkip > 0) { skipBytes(numToSkip); } } // Read the header record if (!readRecord()) { if (debug) { System.err.println("READ NULL RECORD"); } hasHitEOF = true; } else if (buffer.isEOFRecord(recordBuf)) { if (debug) { System.err.println("READ EOF RECORD"); } hasHitEOF = true; } if (hasHitEOF) { currEntry = null; } else { currEntry = new TarEntry(recordBuf); // Offset of the current entry from the start of the archive, // allows to reposition the stream at the start of the entry currEntry.setOffset(buffer.getCurrentBlockNum()*buffer.getBlockSize() + buffer.getCurrentRecordNum()*buffer.getRecordSize()); if (debug) { System.err.println("TarInputStream: SET CURRENTRY '" + currEntry.getName() + "' size = " + currEntry.getSize()); } // Update the current entry offset and size entryOffset = 0; entrySize = currEntry.getSize(); // Consume the rest of the record recordBufPos = 0; recordBufLeft = 0; } if (currEntry != null && currEntry.isGNULongNameEntry()) { // read in the name StringBuilder longName = new StringBuilder(); int length; while ((length = read(nameBuf)) >= 0) { longName.append(new String(nameBuf, 0, length)); } getNextEntry(); if (currEntry == null) { // Bugzilla: 40334 // Malformed tar file - long entry name not followed by entry return null; } // remove trailing null terminator if (longName.length() > 0 && longName.charAt(longName.length() - 1) == 0) { longName.deleteCharAt(longName.length() - 1); } currEntry.setName(longName.toString()); } return currEntry; } /** * Reads a byte from the current tar archive entry. * * This method simply calls read( byte[], int, int ). * * @return The byte read, or -1 at EOF. * @throws IOException on error */ @Override public int read() throws IOException { int num = read(oneBuf, 0, 1); return num == -1 ? -1 : ((int) oneBuf[0]) & BYTE_MASK; } /** * Reads bytes from the current tar archive entry. * * This method is aware of the boundaries of the current * entry in the archive and will deal with them as if they * were this stream's start and EOF. * * @param buf The buffer into which to place bytes read. * @param offset The offset at which to place bytes read. * @param numToRead The number of bytes to read. * @return The number of bytes read, or -1 at EOF. * @throws IOException on error */ @Override public int read(byte[] buf, int offset, int numToRead) throws IOException { int totalRead = 0; // Have we already reached the end of file/entry ? if (entryOffset >= entrySize) { return -1; } // Can't read more than the entry's size if ((numToRead + entryOffset) > entrySize) { numToRead = (int) (entrySize - entryOffset); } // Read data one record (at most) at a time. The record buffer is first emptied before reading a new record. while (numToRead > 0) { // If there is no more data left to read from the current record buffer, // read a new record if(recordBufLeft<=0) { if (!readRecord()) { // Unexpected EOF! throw new EOFException("unexpected EOF with " + numToRead + " bytes unread"); } } int sz = (numToRead > recordBufLeft) ? recordBufLeft : numToRead; System.arraycopy(recordBuf, recordBufPos, buf, offset, sz); recordBufPos += sz; recordBufLeft -= sz; totalRead += sz; numToRead -= sz; offset += sz; entryOffset += sz; } return totalRead; } /** * Skip bytes in the input buffer. This skips bytes in the * current entry's data, not the entire archive, and will * stop at the end of the current entry's data if the number * to skip extends beyond that point. * * @param numToSkip the number of bytes to skip. * @return the number actually skipped * @throws IOException on error */ @Override public long skip(long numToSkip) throws IOException { // Have we already reached the end of file/entry ? if (entryOffset >= entrySize) { return -1; } // Can't read more than the entry's size if ((numToSkip + entryOffset) > entrySize) { numToSkip = (int) (entrySize - entryOffset); } return skipBytes(numToSkip); } /** * Skips the specified number of bytes, without checking for the current entry's boundaries. * * @param numToSkip the number of bytes to skip. * @return the number actually skipped * @throws IOException on error */ private long skipBytes(long numToSkip) throws IOException { int totalSkipped = 0; int recordSize = buffer.getRecordSize(); int blockSize = buffer.getBlockSize(); while (numToSkip > 0) { // If the record buffer has some data left, empty it if (recordBufLeft > 0) { int sz = (numToSkip > recordBufLeft) ? recordBufLeft : (int)numToSkip; recordBufPos += sz; recordBufLeft -= sz; totalSkipped += sz; numToSkip -= sz; entryOffset += sz; } // Skip a whole block if there are enough bytes left to skip, and if we are at the end of the current block else if (numToSkip >= blockSize && buffer.getCurrentRecordNum() == buffer.getRecordsPerBlock()-1) { if (!buffer.skipBlock()) { // Unexpected EOF! throw new EOFException("unexpected EOF with " + numToSkip + " bytes unskipped"); } totalSkipped += blockSize; numToSkip -= blockSize; entryOffset += blockSize; } // Skip a whole record if there are enough bytes left to skip else if (numToSkip >= recordSize) { if (!buffer.skipRecord()) { // Unexpected EOF! throw new EOFException("unexpected EOF with " + numToSkip + " bytes unskipped"); } totalSkipped += recordSize; numToSkip -= recordSize; entryOffset += recordSize; } // There is less than a record to skip -> read the record and skip else { if (!readRecord()) { // Unexpected EOF! throw new EOFException("unexpected EOF with " + numToSkip + " bytes unskipped"); } // if(recordBufLeft>0) will be matched on the next loop } } return totalSkipped; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/provider/TarOutputStream.java ================================================ package com.mucommander.commons.file.impl.tar.provider; import com.mucommander.commons.io.BufferPool; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; /** * The TarOutputStream writes a UNIX tar archive as an OutputStream. * Methods are provided to put entries, and then write their contents * by writing to this stream using write(). * *

    This class is based off the org.apache.tools.tar package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.1 of Ant. * * @author Apache Ant, Maxence Bernard */ public class TarOutputStream extends FilterOutputStream { /** Fail if a long file name is required in the archive. */ public static final int LONGFILE_ERROR = 0; /** Long paths will be truncated in the archive. */ public static final int LONGFILE_TRUNCATE = 1; /** GNU tar extensions are used to store long file names in the archive. */ public static final int LONGFILE_GNU = 2; protected boolean debug; protected long currSize; protected String currName; protected long currBytes; protected byte[] oneBuf; protected byte[] recordBuf; protected int assemLen; protected byte[] assemBuf; protected TarBuffer buffer; protected int longFileMode = LONGFILE_ERROR; private boolean closed = false; /** * Constructor for TarInputStream. * @param os the output stream to use */ public TarOutputStream(OutputStream os) { this(os, TarBuffer.DEFAULT_BLKSIZE, TarBuffer.DEFAULT_RCDSIZE); } /** * Constructor for TarInputStream. * @param os the output stream to use * @param blockSize the block size to use */ public TarOutputStream(OutputStream os, int blockSize) { this(os, blockSize, TarBuffer.DEFAULT_RCDSIZE); } /** * Constructor for TarInputStream. * @param os the output stream to use * @param blockSize the block size to use * @param recordSize the record size to use */ public TarOutputStream(OutputStream os, int blockSize, int recordSize) { super(os); this.buffer = new TarBuffer(os, blockSize, recordSize); this.debug = false; this.assemLen = 0; this.assemBuf = BufferPool.getByteArray(recordSize); this.recordBuf = BufferPool.getByteArray(recordSize); this.oneBuf = BufferPool.getByteArray(1); } /** * Set the long file mode. * This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2). * This specifies the treatment of long file names (names >= TarConstants.NAMELEN). * Default is LONGFILE_ERROR. * @param longFileMode the mode to use */ public void setLongFileMode(int longFileMode) { this.longFileMode = longFileMode; } /** * Sets the debugging flag. * * @param debugF True to turn on debugging. */ public void setDebug(boolean debugF) { this.debug = debugF; } /** * Sets the debugging flag in this stream's TarBuffer. * * @param debug True to turn on debugging. */ public void setBufferDebug(boolean debug) { buffer.setDebug(debug); } /** * Ends the TAR archive without closing the underlying OutputStream. * The result is that the two EOF records of nulls are written. * @throws IOException on error */ public void finish() throws IOException { // See Bugzilla 28776 for a discussion on this // http://issues.apache.org/bugzilla/show_bug.cgi?id=28776 writeEOFRecord(); writeEOFRecord(); } /** * Ends the TAR archive and closes the underlying OutputStream. * This means that finish() is called followed by calling the * TarBuffer's close(). * @throws IOException on error */ @Override public void close() throws IOException { if (!closed) { try { finish(); buffer.close(); } finally { BufferPool.releaseByteArray(assemBuf); BufferPool.releaseByteArray(recordBuf); BufferPool.releaseByteArray(oneBuf); closed = true; } } } /** * Get the record size being used by this stream's TarBuffer. * * @return The TarBuffer record size. */ public int getRecordSize() { return buffer.getRecordSize(); } /** * Put an entry on the output stream. This writes the entry's * header record and positions the output stream for writing * the contents of the entry. Once this method is called, the * stream is ready for calls to write() to write the entry's * contents. Once the contents are written, closeEntry() * MUST be called to ensure that all buffered data * is completely written to the output stream. * * @param entry The TarEntry to be written to the archive. * @throws IOException on error */ public void putNextEntry(TarEntry entry) throws IOException { if (entry.getName().length() >= TarConstants.NAMELEN) { if (longFileMode == LONGFILE_GNU) { // create a TarEntry for the LongLink, the contents // of which are the entry's name TarEntry longLinkEntry = new TarEntry(TarConstants.GNU_LONGLINK, TarConstants.LF_GNUTYPE_LONGNAME); longLinkEntry.setSize(entry.getName().length() + 1); putNextEntry(longLinkEntry); write(entry.getName().getBytes()); write(0); closeEntry(); } else if (longFileMode != LONGFILE_TRUNCATE) { throw new RuntimeException("file name '" + entry.getName() + "' is too long ( > " + TarConstants.NAMELEN + " bytes)"); } } entry.writeEntryHeader(recordBuf); buffer.writeRecord(recordBuf); currBytes = 0; if (entry.isDirectory()) { currSize = 0; } else { currSize = entry.getSize(); } currName = entry.getName(); } /** * Close an entry. This method MUST be called for all file * entries that contain data. The reason is that we must * buffer data written to the stream in order to satisfy * the buffer's record based writes. Thus, there may be * data fragments still being assembled that must be written * to the output stream before this entry is closed and the * next entry written. * @throws IOException on error */ public void closeEntry() throws IOException { if (assemLen > 0) { for (int i = assemLen; i < assemBuf.length; ++i) { assemBuf[i] = 0; } buffer.writeRecord(assemBuf); currBytes += assemLen; assemLen = 0; } if (currBytes < currSize) { throw new IOException("entry '" + currName + "' closed at '" + currBytes + "' before the '" + currSize + "' bytes specified in the header were written"); } } /** * Writes a byte to the current tar archive entry. * * This method simply calls read( byte[], int, int ). * * @param b The byte written. * @throws IOException on error */ @Override public void write(int b) throws IOException { oneBuf[0] = (byte) b; write(oneBuf, 0, 1); } /** * Writes bytes to the current tar archive entry. * * This method simply calls write( byte[], int, int ). * * @param wBuf The buffer to write to the archive. * @throws IOException on error */ @Override public void write(byte[] wBuf) throws IOException { write(wBuf, 0, wBuf.length); } /** * Writes bytes to the current tar archive entry. This method * is aware of the current entry and will throw an exception if * you attempt to write bytes past the length specified for the * current entry. The method is also (painfully) aware of the * record buffering required by TarBuffer, and manages buffers * that are not a multiple of recordsize in length, including * assembling records from small buffers. * * @param wBuf The buffer to write to the archive. * @param wOffset The offset in the buffer from which to get bytes. * @param numToWrite The number of bytes to write. * @throws IOException on error */ @Override public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { if ((currBytes + numToWrite) > currSize) { throw new IOException("request to write '" + numToWrite + "' bytes exceeds size in header of '" + currSize + "' bytes for entry '" + currName + "'"); // // We have to deal with assembly!!! // The programmer can be writing little 32 byte chunks for all // we know, and we must assemble complete records for writing. // REVIEW Maybe this should be in TarBuffer? Could that help to // eliminate some of the buffer copying. // } if (assemLen > 0) { if ((assemLen + numToWrite) >= recordBuf.length) { int aLen = recordBuf.length - assemLen; System.arraycopy(assemBuf, 0, recordBuf, 0, assemLen); System.arraycopy(wBuf, wOffset, recordBuf, assemLen, aLen); buffer.writeRecord(recordBuf); currBytes += recordBuf.length; wOffset += aLen; numToWrite -= aLen; assemLen = 0; } else { System.arraycopy(wBuf, wOffset, assemBuf, assemLen, numToWrite); wOffset += numToWrite; assemLen += numToWrite; numToWrite -= numToWrite; } } // // When we get here we have EITHER: // o An empty "assemble" buffer. // o No bytes to write (numToWrite == 0) // while (numToWrite > 0) { if (numToWrite < recordBuf.length) { System.arraycopy(wBuf, wOffset, assemBuf, assemLen, numToWrite); assemLen += numToWrite; break; } buffer.writeRecord(wBuf, wOffset); int num = recordBuf.length; currBytes += num; numToWrite -= num; wOffset += num; } } /** * Write an EOF (end of archive) record to the tar archive. * An EOF record consists of a record of all zeros. */ private void writeEOFRecord() throws IOException { for (int i = 0; i < recordBuf.length; ++i) { recordBuf[i] = 0; } buffer.writeRecord(recordBuf); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/tar/provider/TarUtils.java ================================================ package com.mucommander.commons.file.impl.tar.provider; /** * This class provides static utility methods to work with byte streams. * *

    This class is based off the org.apache.tools.tar package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.1 of Ant. * * @author Apache Ant */ public class TarUtils { private static final int BYTE_MASK = 255; /** * Parse an octal string from a header buffer. This is used for the * file permission mode value. * * @param header The header buffer from which to parse. * @param offset The offset into the buffer from which to parse. * @param length The number of header bytes to parse. * @return The long value of the octal string. */ public static long parseOctal(byte[] header, int offset, int length) { long result = 0; boolean stillPadding = true; int end = offset + length; for (int i = offset; i < end; ++i) { if (header[i] == 0) { break; } if (header[i] == (byte) ' ' || header[i] == '0') { if (stillPadding) { continue; } if (header[i] == (byte) ' ') { break; } } stillPadding = false; result = (result << 3) + (header[i] - '0'); } return result; } /** * Parse an entry name from a header buffer. * * @param header The header buffer from which to parse. * @param offset The offset into the buffer from which to parse. * @param length The number of header bytes to parse. * @return The header's entry name. */ public static StringBuffer parseName(byte[] header, int offset, int length) { StringBuffer result = new StringBuffer(length); int end = offset + length; for (int i = offset; i < end; ++i) { if (header[i] == 0) { break; } result.append((char) header[i]); } return result; } /** * Determine the number of bytes in an entry name. * * @param name The header name from which to parse. * @param buf The buffer from which to parse. * @param offset The offset into the buffer from which to parse. * @param length The number of header bytes to parse. * @return The number of bytes in a header's entry name. */ public static int getNameBytes(StringBuffer name, byte[] buf, int offset, int length) { int i; for (i = 0; i < length && i < name.length(); ++i) { buf[offset + i] = (byte) name.charAt(i); } for (; i < length; ++i) { buf[offset + i] = 0; } return offset + length; } /** * Parse an octal integer from a header buffer. * * @param value The header value * @param buf The buffer from which to parse. * @param offset The offset into the buffer from which to parse. * @param length The number of header bytes to parse. * @return The integer value of the octal bytes. */ public static int getOctalBytes(long value, byte[] buf, int offset, int length) { int idx = length - 1; buf[offset + idx] = 0; --idx; buf[offset + idx] = (byte) ' '; --idx; if (value == 0) { buf[offset + idx] = (byte) '0'; --idx; } else { for (long val = value; idx >= 0 && val > 0; --idx) { buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7)); val = val >> 3; } } for (; idx >= 0; --idx) { buf[offset + idx] = (byte) ' '; } return offset + length; } /** * Parse an octal long integer from a header buffer. * * @param value The header value * @param buf The buffer from which to parse. * @param offset The offset into the buffer from which to parse. * @param length The number of header bytes to parse. * @return The long value of the octal bytes. */ public static int getLongOctalBytes(long value, byte[] buf, int offset, int length) { byte[] temp = new byte[length + 1]; getOctalBytes(value, temp, 0, length + 1); System.arraycopy(temp, 0, buf, offset, length); return offset + length; } /** * Parse the checksum octal integer from a header buffer. * * @param value The header value * @param buf The buffer from which to parse. * @param offset The offset into the buffer from which to parse. * @param length The number of header bytes to parse. * @return The integer value of the entry's checksum. */ public static int getCheckSumOctalBytes(long value, byte[] buf, int offset, int length) { getOctalBytes(value, buf, offset, length); buf[offset + length - 1] = (byte) ' '; buf[offset + length - 2] = 0; return offset + length; } /** * Compute the checksum of a tar entry header. * * @param buf The tar entry's header buffer. * @return The computed checksum. */ public static long computeCheckSum(byte[] buf) { long sum = 0; for (byte b : buf) { sum += BYTE_MASK & b; } return sum; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/udf/UdfFormatProvider.java ================================================ package com.mucommander.commons.file.impl.udf; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class UdfFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".udf" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {}; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.UDF, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/vsphere/ManagedObjectReferenceWrapper.java ================================================ package com.mucommander.commons.file.impl.vsphere; import com.vmware.vim25.ManagedObjectReference; /** * A wrapper for ManagedObjectReferenceWrapper that adds equals and hashCode for it. * * @author Yuval Kohavi, yuval.kohavi@intigua.com * */ public class ManagedObjectReferenceWrapper { private final ManagedObjectReference mor; public ManagedObjectReferenceWrapper(ManagedObjectReference mor) { this.mor = mor; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mor.getValue() == null) ? 0 : mor.getValue().hashCode()); result = prime * result + ((mor.getType() == null) ? 0 : mor.getType().hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ManagedObjectReferenceWrapper other = (ManagedObjectReferenceWrapper) obj; if (mor.getValue() == null) { if (other.mor.getValue() != null) return false; } else if (!mor.getValue().equals(other.mor.getValue())) return false; if (mor.getType() == null) { return other.mor.getType() == null; } else return mor.getType().equals(other.mor.getType()); } @Override public String toString() { return "ManagedObjectReferenceWrapper [type=" + mor.getType() + ", value=" + mor.getValue() + "]"; } /** * @return the mor */ public ManagedObjectReference getMor() { return mor; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/vsphere/VSphereClient.java ================================================ package com.mucommander.commons.file.impl.vsphere; import com.mucommander.commons.util.StringUtils; import com.vmware.vim25.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import java.io.Closeable; import java.io.IOException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Wrapper over the vSphere API * * @author Yuval Kohavi, yuval.kohavi@intigua.com * */ public class VSphereClient implements Closeable { private static final String TYPE_SERVICE_INSTANCE = "ServiceInstance"; private static final Logger log = LoggerFactory.getLogger(VSphereClient.class); private final String server; private final String user; private final String password; private ServiceContent serviceContent; private VimPortType vimPort; private boolean connected = false; private Integer port; public String getServer() { return server; } boolean isConnected() { return connected; } VSphereClient(String server, String user, String password) { this.server = server; this.user = user; this.password = password; } private String getVSphereServiceUrl() { if (StringUtils.isNullOrEmpty((server))) { log.warn("Can't construct vim service url, vSphere host name is empty"); throw new IllegalArgumentException(); } StringBuilder builder = new StringBuilder(); builder.append("https://"); builder.append(server); if (port != null) { builder.append(":"); builder.append(port); } builder.append("/sdk/vimService"); return builder.toString(); } /** * Establishes session with the vSphere server. * * @throws RuntimeFaultFaultMsg * @throws InvalidLoginFaultMsg * @throws InvalidLocaleFaultMsg * */ public void connect() throws RuntimeFaultFaultMsg, InvalidLocaleFaultMsg, InvalidLoginFaultMsg { String connectionUrl = getVSphereServiceUrl(); doTrust(); ManagedObjectReference serviceInstanceMOR = getServiceInstance(); VimService vimService = new VimService(); log.trace("Getting vimPort from vimService"); //vimPort = vimService.getVimPort(); log.trace("vimPort is gotT successfully"); log.trace("Getting context from vimPort"); //Map requestContext = ((BindingProvider) vimPort).getRequestContext(); log.trace("Context from vimPort is got successfully"); log.trace("URL to connect to vSphere host '{}'", connectionUrl); // requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, connectionUrl); // requestContext.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, true); // do trust ? TODO /* requestContext.put(JAXWSProperties.SSL_SOCKET_FACTORY, getSSLContext() .getSocketFactory()); HostnameVerifier hv = new HostnameVerifier() { @Override public boolean verify(String urlHostName, SSLSession session) { return true; } }; requestContext.put(JAXWSProperties.HOSTNAME_VERIFIER, hv); */ log.trace("Retrieving service content"); serviceContent = vimPort.retrieveServiceContent(serviceInstanceMOR); log.trace("Service content retrieved successfully"); log.trace("Logging in to vSphere host '{}'", server); UserSession userSession = vimPort.login(serviceContent.getSessionManager(), user, password, null); log.trace("Logged in successfully to vSphere host '{}'", server); connected = true; } public ManagedObjectReference getServiceInstance() { ManagedObjectReference serviceInstanceMOR = new ManagedObjectReference(); serviceInstanceMOR.setType(TYPE_SERVICE_INSTANCE); serviceInstanceMOR.setValue(TYPE_SERVICE_INSTANCE); return serviceInstanceMOR; } private void doTrust() { HostnameVerifier hv = (urlHostName, session) -> true; try { trustAllHttpsCertificates(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("SSL init problems", e); } HttpsURLConnection.setDefaultHostnameVerifier(hv); } @Override public void close() throws IOException { try { disconnect(); } catch (RuntimeFaultFaultMsg e) { throw new IOException(e); } } /** * Disconnects the user session. * * @throws RuntimeFaultFaultMsg * */ public void disconnect() throws RuntimeFaultFaultMsg { if (connected) { log.trace("Logging out from vSphere host '{}'", server); vimPort.logout(serviceContent.getSessionManager()); log.trace("Logged out successfully from vSphere host '{}'", server); } connected = false; } public ManagedObjectReference findVmByUuid(String uuid, boolean instanceUuid) throws RuntimeFaultFaultMsg { return vimPort.findByUuid(this.serviceContent.getSearchIndex(), null, uuid, true, instanceUuid); } public ManagedObjectReference findVmByIp(String ip) throws RuntimeFaultFaultMsg { return vimPort.findByIp(this.serviceContent.getSearchIndex(), null, ip, true); } /* taken from vmware samples */ public Object[] getProperties(ManagedObjectReference moRef, String... properties) throws InvalidPropertyFaultMsg, RuntimeFaultFaultMsg { // PropertySpec specifies what properties to // retrieve and from type of Managed Object PropertySpec pSpec = new PropertySpec(); pSpec.setType(moRef.getType()); pSpec.getPathSet().addAll(Arrays.asList(properties)); // ObjectSpec specifies the starting object and // any TraversalSpecs used to specify other objects // for consideration ObjectSpec oSpec = new ObjectSpec(); oSpec.setObj(moRef); // PropertyFilterSpec is used to hold the ObjectSpec and // PropertySpec for the call PropertyFilterSpec pfSpec = new PropertyFilterSpec(); pfSpec.getPropSet().addAll(Collections.singletonList(pSpec)); pfSpec.getObjectSet().addAll(Collections.singletonList(oSpec)); // retrieveProperties() returns the properties // selected from the PropertyFilterSpec List ocs = vimPort.retrieveProperties( serviceContent.getPropertyCollector(), Collections.singletonList(pfSpec)); // Return value, one object for each property specified Object[] ret = new Object[properties.length]; if (ocs != null) { for (ObjectContent oc : ocs) { List dps = oc.getPropSet(); if (dps != null) { for (DynamicProperty dp : dps) { // find property path index for (int p = 0; p < ret.length; ++p) { if (properties[p].equals(dp.getName())) { ret[p] = dp.getVal(); } } } } } } return ret; } private void trustAllHttpsCertificates() throws NoSuchAlgorithmException { javax.net.ssl.SSLContext sc = getSSLContext(); javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc .getSocketFactory()); } private javax.net.ssl.SSLContext getSSLContext() { // create a trust manager that does not validate certificate chains: javax.net.ssl.TrustManager[] trustAllCerts = new javax.net.ssl.TrustManager[1]; javax.net.ssl.TrustManager tm = new TrustAllTrustManager(); trustAllCerts[0] = tm; try { javax.net.ssl.SSLContext sc = javax.net.ssl.SSLContext.getInstance("SSL"); javax.net.ssl.SSLSessionContext sslsc = sc.getServerSessionContext(); sslsc.setSessionTimeout(0); sc.init(null, trustAllCerts, null); return sc; } catch (KeyManagementException | NoSuchAlgorithmException e) { throw new IllegalStateException("SSL init problems", e); } } private class TrustAllTrustManager implements javax.net.ssl.TrustManager, javax.net.ssl.X509TrustManager { @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } @Override public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } } public VimPortType getVimPort() { return this.vimPort; } public ServiceContent getServiceContent() { return this.serviceContent; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/vsphere/VSphereFile.java ================================================ package com.mucommander.commons.file.impl.vsphere; import com.mucommander.commons.file.*; import com.mucommander.commons.file.connection.ConnectionHandler; import com.mucommander.commons.file.connection.ConnectionHandlerFactory; import com.mucommander.commons.file.connection.ConnectionPool; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.io.StreamUtils; import com.vmware.vim25.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.HttpsURLConnection; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import java.io.*; import java.net.*; import java.rmi.RemoteException; import java.util.*; /** * VSphereFile provides access to files on virtual machines on vSphere 5+ * servers. * *

    * The associated {@link FileURL} scheme is {@link FileProtocols#VSPHERE}. The * host part of the URL designates the vSphere server. Credentials must be * specified in the login and password parts as vSphere servers require a login * and password. The path separator is '/', even on windows virtual machines. * * The first path of the path is the identifier of the guest VM (see below) we * want to access. That part may also contain username and password for the * guest VMs - though it is less secure, so it is better to use the GUI dialog. * * Note that the machine credentials cab also be set as a property named * "guestCredentials" of this object. * * The identifier of a VM can be one of the following: - Instance UUID - BIOS * UUID - IP address * * Note that even if you use IP to identify the machine, you don't need network * connectivity for accessing the machine. all access is done via vSphere APIs. * *

    * Here are a few examples of valid vSphere URLs: * vsphere://vsphere5-server/501fc8db-f9dc-562b-6310-3c6b7ace2377/C:
    * vsphere://admin:pass@vsphere5-server/501fc8db-f9dc-562b-6310-3c6b7ace2377/C:
    * vsphere://admin:pass@vsphere5-server/guestadmin:guestpass@501fc8db-f9dc-562b-6310-3c6b7ace2377/C:
    *
    * *

    * Access to vSphere files is provided by the vim25 library * distributed under the VMware Software Development Kit (SDK) License * Agreement. See: http://www.vmware.com/go/vwssdk-redistribution-info * * @see ConnectionPool * @author Yuval Kohavi, yuval.kohavi@intigua.com */ public class VSphereFile extends ProtocolFile implements ConnectionHandlerFactory { private static final Logger LOGGER = LoggerFactory.getLogger(VSphereFile.class); public static final String GUEST_CREDENTIALS = "guestCredentials"; private static final String SEPARATOR = "/"; private String guestPass; private String guestUser; private String vmIdentifier; private String pathInsideVm; private ManagedObjectReference vm; private NamePasswordAuthentication credentials; private ManagedObjectReference guestOperationsManager; private long size = -1; private boolean isFile; private boolean isSymLink; private boolean isDir; private long date; private VSphereFile parent; private String guestOsId; public VSphereFile(FileURL url) throws IOException { super(url); VsphereConnHandler connHandler = null; try { setPath(url.getPath()); connHandler = getConnHandler(); guestOperationsManager = connHandler.getClient() .getServiceContent().getGuestOperationsManager(); getMor(connHandler); fixPathInVmIfNeeded(connHandler); checkAttributes(connHandler); } catch (RuntimeFaultFaultMsg | FileFaultFaultMsg | GuestOperationsFaultFaultMsg | TaskInProgressFaultMsg | InvalidPropertyFaultMsg | URISyntaxException | InvalidStateFaultMsg e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } } private void releaseConnHandler(VsphereConnHandler connHandler) { if (connHandler != null) { connHandler.releaseLock(); } } private VSphereFile(FileURL url, VSphereFile related) throws URISyntaxException, IOException, RuntimeFaultFaultMsg, InvalidPropertyFaultMsg, FileFaultFaultMsg, GuestOperationsFaultFaultMsg, InvalidStateFaultMsg, TaskInProgressFaultMsg { super(url); setPath(url.getPath()); this.vm = related.vm; this.guestOsId = related.guestOsId; this.guestOperationsManager = related.guestOperationsManager; fixPathInVmIfNeeded(null); VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); checkAttributes(connHandler); } finally { releaseConnHandler(connHandler); } } public VSphereFile(FileURL url, VSphereFile parent, GuestFileInfo guestFileInfo) throws RuntimeFaultFaultMsg, IOException, InvalidPropertyFaultMsg, URISyntaxException { super(url); setPath(url.getPath()); this.parent = parent; this.vm = parent.vm; this.guestOsId = parent.guestOsId; this.guestOperationsManager = parent.guestOperationsManager; fixPathInVmIfNeeded(null); updateAttributes(guestFileInfo); } private void getMor(VsphereConnHandler connHandler) throws RuntimeFaultFaultMsg { vm = connHandler.getClient().findVmByIp(vmIdentifier); if (vm == null) { vm = connHandler.getClient().findVmByUuid(vmIdentifier, true); } if (vm == null) { vm = connHandler.getClient().findVmByUuid(vmIdentifier, false); } if (vm == null) { throw new IllegalArgumentException("Machine identifier " + vmIdentifier + " not found."); } } private void fixPathInVmIfNeeded(VsphereConnHandler connHandler) throws RuntimeFaultFaultMsg, RemoteException, InvalidPropertyFaultMsg { if (this.guestOsId == null) { if (connHandler != null) { this.guestOsId = (String) connHandler.getClient() .getProperties(vm, "config.guestId")[0]; } } boolean isWin = false; if (this.guestOsId != null) { // is it a windows machine ? // see possible values for guest id here: // http://www.vmware.com/support/developer/vc-sdk/visdk41pubs/ApiReference/vim.vm.GuestOsDescriptor.GuestOsIdentifier.html isWin = guestOsId.startsWith("win"); } if (pathInsideVm.isEmpty()) { if (isWin) { // we assume that "C:" is a good default for windows pathInsideVm = "C:"; } else { pathInsideVm = "/"; } // set the url to reflect the path inside the vm fileURL.setPath(fileURL.getPath() + pathInsideVm); } } private void setPath(String path) throws URISyntaxException, IOException { // path starts with a / // get first component: int index = path.indexOf('/', 1); String first = path.substring(1, index); String rest = path.substring(index + 1); // the first part of the path is very similar to a url, due to the fact // it // may contain guest credentials. So I use the URI class to help me // parse it. // first as a url of its own. http = dymm URI url2 = new URI("dummy://" + first); String uinfo; credentials = new NamePasswordAuthentication(); vmIdentifier = url2.getHost(); String guestCred = fileURL.getProperty(GUEST_CREDENTIALS); if ((guestCred != null) && (!guestCred.isEmpty())) { uinfo = guestCred; } else { uinfo = url2.getUserInfo(); } if (uinfo == null) { throw new IOException( "No guest credentials provided. please start the connection from the UI"); } int indexOf = uinfo.indexOf(":"); if (indexOf == -1) { throw new IOException( "No guest credentials provided. please start the connection from the UI"); } guestUser = uinfo.substring(0, indexOf); guestPass = uinfo.substring(indexOf + 1); credentials.setInteractiveSession(false); credentials.setUsername(guestUser); credentials.setPassword(guestPass); pathInsideVm = rest; } private ManagedObjectReference getFileManager(VsphereConnHandler connHandler) throws RemoteException, InvalidPropertyFaultMsg, RuntimeFaultFaultMsg { ManagedObjectReference fileManager = (ManagedObjectReference) connHandler .getClient().getProperties(guestOperationsManager, "fileManager")[0]; return fileManager; } private void checkAttributes(VsphereConnHandler connHandler) throws IOException, FileFaultFaultMsg, GuestOperationsFaultFaultMsg, InvalidStateFaultMsg, RuntimeFaultFaultMsg, TaskInProgressFaultMsg, InvalidPropertyFaultMsg { ManagedObjectReference fileManager = getFileManager(connHandler); GuestListFileInfo res; try { res = connHandler .getClient() .getVimPort() .listFilesInGuest(fileManager, vm, credentials, getPathInVm(), null, null, null); } catch (Exception e) { if (isFileNotFound(e)) { return; } throw e; } if (res.getFiles().size() == 1) { // only one result - it's a file GuestFileInfo guestFileInfo = res.getFiles().get(0); updateAttributes(guestFileInfo); } else { // more than one result - it's a directory. // find the entry for "." for (GuestFileInfo f : res.getFiles()) { if (f.getPath().equals(".")) { updateAttributes(f); break; } } } } private boolean isFileNotFound(Exception e) { // Detail detail = e.getFault().getDetail(); // NodeList childNodes = detail.getChildNodes(); // // for (int i = 0; i < childNodes.getLength(); ++i) { // // I hate soap... // if (childNodes.item(i).getNodeName().equals("FileNotFoundFault")) { // return true; // } // } return false; } void updateAttributes(GuestFileInfo guestFileInfo) { if (guestFileInfo.getType().equals(GuestFileType.FILE.value())) { isFile = true; size = guestFileInfo.getSize(); } else if (guestFileInfo.getType().equals(GuestFileType.SYMLINK.value())) { isSymLink = true; } else if (guestFileInfo.getType().equals(GuestFileType.DIRECTORY.value())) { isDir = true; } date = guestFileInfo.getAttributes().getModificationTime().toGregorianCalendar().getTimeInMillis(); } private VsphereConnHandler getConnHandler() throws IOException { VsphereConnHandler connHandler = (VsphereConnHandler) ConnectionPool .getConnectionHandler(this, fileURL, true); try { connHandler.checkConnection(); } catch (RuntimeException | IOException e) { releaseConnHandler(connHandler); throw e; } return connHandler; } @Override public ConnectionHandler createConnectionHandler(FileURL location) { return new VsphereConnHandler(location); } @Override public long getLastModifiedDate() { return date; } @Override public void setLastModifiedDate(long lastModified) throws IOException { VsphereConnHandler connHandler = null; try { GuestFileAttributes gfa = new GuestFileAttributes(); gfa.setModificationTime(getTimeToXmlTime(lastModified)); connHandler = getConnHandler(); connHandler.getClient().getVimPort().changeFileAttributesInGuest( getFileManager(connHandler), vm, credentials, getPathInVm(), gfa); } catch (FileFaultFaultMsg | RuntimeFaultFaultMsg | GuestOperationsFaultFaultMsg | InvalidStateFaultMsg | TaskInProgressFaultMsg | InvalidPropertyFaultMsg | DatatypeConfigurationException e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } } private XMLGregorianCalendar getTimeToXmlTime(long lastModified) throws DatatypeConfigurationException { GregorianCalendar gc = new GregorianCalendar(TimeZone.getTimeZone("UTC")); gc.setTime(new Date(lastModified)); return DatatypeFactory.newInstance().newXMLGregorianCalendar(gc); } @Override public long getSize() { return size; } @Override public AbstractFile getParent() { if (parent != null) { return parent; } String rootPath = getRootPath(); if (rootPath.equals(pathInsideVm) || pathInsideVm.equals("/")) { return null; } try { parent = new VSphereFile(fileURL.getParent(), this); } catch (URISyntaxException | IOException | InvalidPropertyFaultMsg | RuntimeFaultFaultMsg | InvalidStateFaultMsg | TaskInProgressFaultMsg | FileFaultFaultMsg | GuestOperationsFaultFaultMsg e) { return null; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = (VSphereFile) parent; } @Override public boolean exists() { return isDir || isFile || isSymLink; } @Override public FilePermissions getPermissions() { return isDir ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS; } @Override public PermissionBits getChangeablePermissions() { return PermissionBits.EMPTY_PERMISSION_BITS; } @Override @UnsupportedFileOperation public void changePermission(int access, int permission, boolean enabled) throws IOException { throw new UnsupportedFileOperationException( FileOperation.CHANGE_PERMISSION); } @Override public String getOwner() { return null; } @Override public boolean canGetOwner() { return false; } @Override public String getGroup() { return null; } @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { return isDir; } @Override public boolean isSymlink() { return isSymLink; } @Override public boolean isSystem() { return false; } @Override public AbstractFile[] ls() throws IOException { List fileInfos = new ArrayList<>(); int index = 0; VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); ManagedObjectReference fileManager = getFileManager(connHandler); boolean haveRemaining; do { GuestListFileInfo res = connHandler .getClient() .getVimPort() .listFilesInGuest(fileManager, vm, credentials, getPathInVm(), index, null, null); haveRemaining = (res.getRemaining() != 0); fileInfos.addAll(res.getFiles()); index = fileInfos.size(); } while (haveRemaining); String parentPath = PathUtils.removeTrailingSeparator(fileURL .getPath()) + SEPARATOR; Collection res = new ArrayList<>(); for (GuestFileInfo f : fileInfos) { final String name = getFileName(f.getPath()); if (name.equals(".") || name.equals("..")) { continue; } FileURL childURL = (FileURL) fileURL.clone(); childURL.setPath(parentPath + name); AbstractFile newFile = new VSphereFile(childURL, this, f); res.add(newFile); } return res.toArray(new AbstractFile[0]); } catch (FileFaultFaultMsg | GuestOperationsFaultFaultMsg | InvalidStateFaultMsg | RuntimeFaultFaultMsg | TaskInProgressFaultMsg | InvalidPropertyFaultMsg | URISyntaxException e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } // we never get here.. return null; } private String getFileName(String path) { // don't search for the slash as the last character. int lastIndex = path.length() - 2; // find the last "\\" or "/" int index = Math.max(path.lastIndexOf('\\', lastIndex), path.lastIndexOf('/', lastIndex)); if (index == -1) { return path; } return path.substring(index + 1); } @Override public void mkdir() throws IOException { VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); connHandler .getClient() .getVimPort() .makeDirectoryInGuest(getFileManager(connHandler), vm, credentials, getPathInVm(), false); } catch (FileFaultFaultMsg | GuestOperationsFaultFaultMsg | RuntimeFaultFaultMsg | InvalidStateFaultMsg | InvalidPropertyFaultMsg | TaskInProgressFaultMsg e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } } @Override public InputStream getInputStream() throws IOException { VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); ManagedObjectReference fileManager = getFileManager(connHandler); FileTransferInformation fileDlInfo = connHandler .getClient() .getVimPort() .initiateFileTransferFromGuest(fileManager, vm, credentials, getPathInVm()); String fileDlUrl = fileDlInfo.getUrl().replace("*", connHandler.getClient().getServer()); // http://stackoverflow.com/questions/921262/how-to-download-and-save-a-file-from-internet-using-java URL website = new URI(fileDlUrl).toURL(); return website.openStream(); } catch (URISyntaxException | InvalidPropertyFaultMsg | RuntimeFaultFaultMsg | GuestOperationsFaultFaultMsg | FileFaultFaultMsg | TaskInProgressFaultMsg | InvalidStateFaultMsg e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } return null; } private String getPathInVm() { if (isWinPath()) { return pathInsideVm.replace("/", "\\"); } return pathInsideVm; } @Override public void copyStream(InputStream in, boolean append, long length) throws FileTransferException { if (append || length == -1) { super.copyStream(in, append, length); } else { try { doCopyRemoteFileName(in, length); } catch (IOException e) { LOGGER.error("Failed copying stream", e); throw new FileTransferException( FileTransferException.UNKNOWN_REASON); } } } private void doCopyRemoteFileName(InputStream in, long length) throws IOException { VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); ManagedObjectReference fileManager = getFileManager(connHandler); copyFileToRemote(getPathInVm(), in, length, connHandler, fileManager); } catch (InvalidPropertyFaultMsg | RuntimeFaultFaultMsg | IOException e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } } /** * vSphere APIs require the file size when copying a file. * mucommander enables file copy with known size, and also with unknown * size. This class supports the unknown size use case. It saves all the * data to a temp file and copies it on close, when its size is known. * */ public class VSphereOutputStream extends FileOutputStream { private final ManagedObjectReference fileManager; private VsphereConnHandler connHandler; private final String fileName; private final File tmpFile; public VSphereOutputStream(VsphereConnHandler connHandler, ManagedObjectReference fileManager, ManagedObjectReference vm, NamePasswordAuthentication credentials, String fileName) throws IOException { this(connHandler, fileManager, vm, credentials, File .createTempFile("tmpVixCopy", ".tmp"), fileName); } private VSphereOutputStream(VsphereConnHandler connHandler, ManagedObjectReference fileManager, ManagedObjectReference vm, NamePasswordAuthentication credentials, File tmpFile, String fileName) throws IOException { super(tmpFile); this.tmpFile = tmpFile; this.connHandler = connHandler; this.fileManager = fileManager; this.fileName = fileName; } @Override public void close() throws IOException { if (connHandler == null) { return; } super.close(); try (InputStream in = new FileInputStream(tmpFile)) { copyFileToRemote(fileName, in, this.tmpFile.length(), connHandler, fileManager); } finally { connHandler.releaseLock(); connHandler = null; tmpFile.delete(); } } } private void copyFileToRemote(String fileName, InputStream in, long length, VsphereConnHandler connHandler, ManagedObjectReference fileManager) throws IOException { try { String fileUploadUrl = getFileUploadUrl(fileName, length, connHandler, fileManager); URLConnection conn = prepareConnection(fileUploadUrl, length); sendFile(in, conn); parseResponse(conn); } catch (RuntimeFaultFaultMsg | GuestOperationsFaultFaultMsg | FileFaultFaultMsg | TaskInProgressFaultMsg | InvalidStateFaultMsg | URISyntaxException e) { translateandLogException(e); } } private void parseResponse(URLConnection conn) throws IOException { int responseCode = 0; String message = ""; if (conn instanceof HttpURLConnection) { responseCode = ((HttpURLConnection) conn).getResponseCode(); message = ((HttpsURLConnection) conn).getResponseMessage(); } if (responseCode != 200) { throw new IOException("Failed to copy file using vsphere: " + message); } } private void sendFile(InputStream in, URLConnection conn) throws IOException { conn.connect(); try (OutputStream out = conn.getOutputStream()) { StreamUtils.copyStream(in, out, IO_BUFFER_SIZE); } } private URLConnection prepareConnection(String fileUploadUrl, long fileSize) throws TaskInProgressFaultMsg, IOException, URISyntaxException { // http://stackoverflow.com/questions/3386832/upload-a-file-using-http-put-in-java URL url = new URI(fileUploadUrl).toURL(); URLConnection conn = url.openConnection(); conn.setDoInput(true); conn.setDoOutput(true); if (conn instanceof HttpURLConnection) { ((HttpURLConnection) conn).setRequestMethod("PUT"); } else { throw new IllegalStateException("Unknown connection type"); } conn.setRequestProperty("Content-type", "application/octet-stream"); conn.setRequestProperty("Content-length", "" + fileSize); return conn; } private String getFileUploadUrl(String remotePathName, long fileSize, VsphereConnHandler connHandler, ManagedObjectReference fileManager) throws RuntimeFaultFaultMsg, FileFaultFaultMsg, GuestOperationsFaultFaultMsg, InvalidStateFaultMsg, TaskInProgressFaultMsg { GuestFileAttributes gfa = new GuestFileAttributes(); final boolean override = true; String fileUploadUrl = connHandler .getClient() .getVimPort() .initiateFileTransferToGuest(fileManager, vm, credentials, remotePathName, gfa, fileSize, override); // replace * with the address of the server. see vsphere docs. fileUploadUrl = fileUploadUrl.replace("*", connHandler.getClient().getServer()); return fileUploadUrl; } @Override public OutputStream getOutputStream() throws IOException { VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); ManagedObjectReference fileManager = getFileManager(connHandler); VSphereOutputStream c = new VSphereOutputStream(connHandler, fileManager, vm, credentials, getPathInVm()); // passed owner ship connHandler = null; return c; } catch (InvalidPropertyFaultMsg | RuntimeFaultFaultMsg e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } throw new RuntimeException("Should never get here"); } @Override @UnsupportedFileOperation public OutputStream getAppendOutputStream() throws IOException { throw new UnsupportedFileOperationException(FileOperation.APPEND_FILE); } @Override @UnsupportedFileOperation public RandomAccessInputStream getRandomAccessInputStream() throws IOException { throw new UnsupportedFileOperationException( FileOperation.RANDOM_READ_FILE); } @Override @UnsupportedFileOperation public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { throw new UnsupportedFileOperationException(FileOperation.RANDOM_WRITE_FILE); } @Override public void delete() throws IOException { VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); ManagedObjectReference fileManager = getFileManager(connHandler); if (isDirectory()) { connHandler .getClient() .getVimPort() .deleteDirectoryInGuest(fileManager, vm, credentials, getPathInVm(), false); isDir = false; } else { connHandler .getClient() .getVimPort() .deleteFileInGuest(fileManager, vm, credentials, getPathInVm()); isFile = isSymLink = false; } } catch (FileFaultFaultMsg | GuestOperationsFaultFaultMsg | InvalidStateFaultMsg | RuntimeFaultFaultMsg | TaskInProgressFaultMsg | InvalidPropertyFaultMsg e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } } @Override public void renameTo(AbstractFile destFile) throws IOException { // can't copy\rename to a different host // os might be windows, so can't rename with different case. checkRenamePrerequisites(destFile, false, false); VsphereConnHandler connHandler = null; try { connHandler = getConnHandler(); ManagedObjectReference fileManager = getFileManager(connHandler); if (isDirectory()) { connHandler .getClient() .getVimPort() .moveDirectoryInGuest(fileManager, vm, credentials, getPathInVm(), ((VSphereFile) destFile).getPathInVm()); } else { connHandler .getClient() .getVimPort() .moveFileInGuest(fileManager, vm, credentials, getPathInVm(), ((VSphereFile) destFile).getPathInVm(), true); } } catch (FileFaultFaultMsg | GuestOperationsFaultFaultMsg | RuntimeFaultFaultMsg | InvalidStateFaultMsg | InvalidPropertyFaultMsg | TaskInProgressFaultMsg e) { translateandLogException(e); } finally { releaseConnHandler(connHandler); } } private static void translateandLogException(Exception e) throws IOException { LOGGER.error("Error with vsphere remote ops", e); throw new IOException(e); } @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws IOException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } @Override @UnsupportedFileOperation public long getFreeSpace() throws IOException { throw new UnsupportedFileOperationException( FileOperation.GET_FREE_SPACE); } @Override @UnsupportedFileOperation public long getTotalSpace() throws IOException { throw new UnsupportedFileOperationException( FileOperation.GET_TOTAL_SPACE); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } @Override public Object getUnderlyingFileObject() { return null; } @Override public AbstractFile getRoot() { FileURL rootURL = (FileURL) getURL().clone(); String rootPath = getRootPath(); rootURL.setPath("/" + vmIdentifier + "/" + rootPath); return FileFactory.getFile(rootURL); } private String getRootPath() { if (isWinPath()) { return getPathInVm().substring(0, 2); } return ""; } private boolean isWinPath() { return (pathInsideVm.length() >= 2) && (pathInsideVm.charAt(1) == ':'); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/vsphere/VSphereProtocolProvider.java ================================================ package com.mucommander.commons.file.impl.vsphere; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import java.io.IOException; /** * This class is the provider for the VSphere filesystem implemented by * {@link com.mucommander.commons.file.impl.vsphere.VSphereFile}. * * @author Yuval Kohavi yuval.kohavi@intigua.com * @see com.mucommander.commons.file.impl.vsphere.VSphereFile */ public class VSphereProtocolProvider implements ProtocolProvider { // /////////////////////////////////// // ProtocolProvider Implementation // // /////////////////////////////////// public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { return new VSphereFile(url); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/vsphere/VsphereConnHandler.java ================================================ package com.mucommander.commons.file.impl.vsphere; import java.io.IOException; import com.mucommander.commons.file.AuthException; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.connection.ConnectionHandler; import com.vmware.vim25.InvalidLocaleFaultMsg; import com.vmware.vim25.InvalidLoginFaultMsg; import com.vmware.vim25.RuntimeFaultFaultMsg; /** * Manage VSphere connections. * * @author Yuval Kohavi, yuval.kohavi@intigua.com * */ public class VsphereConnHandler extends ConnectionHandler { private VSphereClient client = null; private FileURL location; VSphereClient getClient() { return client; } VsphereConnHandler(FileURL serverURL) { super(serverURL); location = serverURL; } private void initClientIfNeeded() throws RuntimeFaultFaultMsg, InvalidLocaleFaultMsg, InvalidLoginFaultMsg { if (client == null) { client = new VSphereClient(location.getHost(), location .getCredentials().getLogin(), location.getCredentials() .getPassword()); client.connect(); } } @Override public void startConnection() throws IOException { try { initClientIfNeeded(); } catch (RuntimeFaultFaultMsg | InvalidLocaleFaultMsg e) { throw new IOException(e); } catch (InvalidLoginFaultMsg e) { throw new AuthException(location, e.getMessage()); } } @Override public boolean isConnected() { return client != null && client.isConnected(); } @Override public void closeConnection() { try { client.disconnect(); } catch (RuntimeFaultFaultMsg e) { // nothing we can do... ignore.. e.printStackTrace(); } client = null; } @Override public void keepAlive() { if (client != null) { try { doKeepAlive(); } catch (RuntimeFaultFaultMsg e) { client = null; } } } private void doKeepAlive() throws RuntimeFaultFaultMsg { // do nothing, to keep alive client.getVimPort().currentTime(client.getServiceInstance()); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/vsphere/package.html ================================================ Provides an implementation of the vSphere protocol. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/webdav/WebDAVFile.java ================================================ package com.mucommander.commons.file.impl.webdav; import com.github.sardine.DavResource; import com.github.sardine.Sardine; import com.github.sardine.SardineFactory; import com.github.sardine.impl.SardineException; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.PermissionBits; import com.mucommander.commons.file.ProtocolFile; import com.mucommander.commons.file.SimpleFilePermissions; import com.mucommander.commons.file.UnsupportedFileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Mathias */ public class WebDAVFile extends ProtocolFile { private final Sardine sardine = SardineFactory.begin(); private final URI PATH; protected AbstractFile parent; private boolean parentSet; private final static String SEPARATOR = "/"; WebDAVFile(FileURL fileURL) throws URISyntaxException { super(fileURL); String scheme = "http"; if (fileURL.getPort() == 443){ scheme = "https"; } PATH = new URI(scheme, fileURL.getLogin() + ":" + fileURL.getPassword(), fileURL.getHost(), fileURL.getPort(), fileURL.getPath(), null, null); } @Override public long getLastModifiedDate() { return 1L; } @Override public void setLastModifiedDate(long lastModified) { throw new UnsupportedOperationException("Not supported yet."); } @Override public long getSize() { return 1L; } @Override public AbstractFile getParent() { if (!parentSet) { FileURL parentFileURL = this.fileURL.getParent(); if (parentFileURL != null) { try { parent = FileFactory.getFile(parentFileURL, this); } catch (IOException e) { // No parent } } parentSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentSet = true; } @Override public boolean exists() { List resources; try { resources = sardine.list(PATH.toString()); } catch (IOException ex) { Logger.getLogger(WebDAVFile.class.getName()).log(Level.SEVERE, null, ex); return false; } return !resources.isEmpty(); } @Override public FilePermissions getPermissions() { return new SimpleFilePermissions(448); } @Override public PermissionBits getChangeablePermissions() { throw new UnsupportedOperationException("Not supported yet."); } @Override public void changePermission(int access, int permission, boolean enabled) { throw new UnsupportedOperationException("Not supported yet."); } @Override public String getOwner() { return "N/A"; } @Override public boolean canGetOwner() { return false; } @Override public String getGroup() { return "N/A"; } @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { List resources; try { resources = sardine.list(PATH.toString()); } catch (IOException ex) { Logger.getLogger(WebDAVFile.class.getName()).log(Level.SEVERE, null, ex); return true; } return !resources.isEmpty() && resources.getFirst().isDirectory(); } @Override public boolean isSymlink() { return false; } @Override public AbstractFile[] ls() throws IOException { List files; try { files = sardine.list(PATH.toString()); } catch (SardineException e) { return new AbstractFile[]{}; } if (files == null || files.isEmpty()) { return new AbstractFile[]{}; } AbstractFile[] children = new AbstractFile[files.size()]; AbstractFile child; FileURL childURL; String childName; int nbFiles = files.size(); int fileCount = 0; String parentPath = fileURL.getPath(); if (!parentPath.endsWith(SEPARATOR)) { parentPath += SEPARATOR; } for (DavResource file : files) { if (file == null) { continue; } childName = file.getName(); //Skip current path (Like skipping "." and ".." if (parentPath.equals(file.getPath())) { continue; } // Note: properties and credentials are cloned for every children's url childURL = (FileURL) fileURL.clone(); childURL.setPath(parentPath + childName); child = FileFactory.getFile(childURL, this, file); children[fileCount++] = child; } // Create new array of the exact file count if (fileCount < nbFiles) { AbstractFile[] newChildren = new AbstractFile[fileCount]; System.arraycopy(children, 0, newChildren, 0, fileCount); return newChildren; } return children; } @Override public void mkdir() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override public InputStream getInputStream() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override public OutputStream getOutputStream() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override public OutputStream getAppendOutputStream() { throw new UnsupportedOperationException("Not supported yet."); } @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override public RandomAccessOutputStream getRandomAccessOutputStream() { throw new UnsupportedOperationException("Not supported yet."); } @Override public void delete() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override public void renameTo(AbstractFile destFile) throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override public void copyRemotelyTo(AbstractFile destFile) throws IOException { throw new UnsupportedOperationException("Not supported yet."); } @Override @UnsupportedFileOperation public long getFreeSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_FREE_SPACE); } @Override @UnsupportedFileOperation public long getTotalSpace() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_TOTAL_SPACE); } @Override public Object getUnderlyingFileObject() { throw new UnsupportedOperationException("Not supported yet."); } @Override @UnsupportedFileOperation public short getReplication() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_REPLICATION); } @Override @UnsupportedFileOperation public void changeReplication(short replication) throws IOException { throw new UnsupportedFileOperationException(FileOperation.CHANGE_REPLICATION); } @Override @UnsupportedFileOperation public long getBlocksize() throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.GET_BLOCKSIZE); } @Override public boolean isSystem() { return false; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/webdav/WebDAVProvider.java ================================================ package com.mucommander.commons.file.impl.webdav; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.ProtocolProvider; import java.io.IOException; import java.net.URISyntaxException; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Mathias */ public class WebDAVProvider implements ProtocolProvider { @Override public AbstractFile getFile(FileURL url, Object... instantiationParams) throws IOException { try { return new WebDAVFile(url); } catch (URISyntaxException ex) { Logger.getLogger(WebDAVProvider.class.getName()).log(Level.SEVERE, null, ex); } return null; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/wim/WimFormatProvider.java ================================================ package com.mucommander.commons.file.impl.wim; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class WimFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".wim" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {0x4D, 0x53, 0x57, 0x49, 0x4D}; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.WIM, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/xar/XarFormatProvider.java ================================================ package com.mucommander.commons.file.impl.xar; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class XarFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".xar" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {0x78, 0x61, 0x72, 0x21}; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.XAR, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/z/ZFormatProvider.java ================================================ package com.mucommander.commons.file.impl.z; import java.io.IOException; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.impl.SevenZipJBindingROArchiveFile; import net.sf.sevenzipjbinding.ArchiveFormat; public class ZFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = { ".z" }; private final static ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); private final static byte[] SIGNATURE = {0x1F, (byte) 0x9D}; @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new SevenZipJBindingROArchiveFile(file, ArchiveFormat.Z, SIGNATURE); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/JavaUtilZipEntryIterator.java ================================================ package com.mucommander.commons.file.impl.zip; import com.mucommander.commons.file.ArchiveEntry; import com.mucommander.commons.file.ArchiveEntryIterator; import java.io.IOException; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** * An ArchiveEntryIterator that iterates through a {@link ZipInputStream}. * * @author Maxence Bernard */ public class JavaUtilZipEntryIterator implements ArchiveEntryIterator { /** InputStream to the archive file */ private ZipInputStream zin; /** The current entry, where the ZipInputStream is currently positionned */ private ArchiveEntry currentEntry; /** * Creates a new TarEntryIterator that iterates through the entries of the given {@link ZipInputStream}. * * @param zin the TarInputStream to iterate through */ JavaUtilZipEntryIterator(ZipInputStream zin) { this.zin = zin; } /** * Returns the {@link ZipInputStream} instance that was used to create this object. * * @return the {@link ZipInputStream} instance that was used to create this object. */ ZipInputStream getZipInputStream() { return zin; } /** * Returns the current entry, where the ZipInputStream is currently positionned. * * @return the current entry, where the ZipInputStream is currently positionned. */ ArchiveEntry getCurrentEntry() { return currentEntry; } /** * Advances the {@link ZipInputStream} to the next entry and returns the corresponding {@link ArchiveEntry}. * * @return the next ArchiveEntry * @throws java.io.IOException if an I/O error occurred */ private ArchiveEntry getNextEntry() throws IOException { try { ZipEntry entry = zin.getNextEntry(); if(entry==null) return null; return ZipArchiveFile.createArchiveEntry(new com.mucommander.commons.file.impl.zip.provider.ZipEntry(entry)); } catch(Exception e) { // java.util.zip.ZipInputStream can throw an IllegalArgumentException when the filename/comment encoding // is not UTF-8 as expected (ZipInputStream always expects UTF-8). The more general Exception is caught // (just to be safe) and an IOException thrown. throw new IOException(); } catch(Error e) { // ZipInputStream#getNextEntry() will throw a java.lang.InternalError ("invalid compression method") // if the compression method is different from DEFLATED or STORED (happens with IMPLODED for example). throw new IOException(); } } ///////////////////////////////////////// // ArchiveEntryIterator implementation // ///////////////////////////////////////// public ArchiveEntry nextEntry() throws IOException { // Get the next entry, if any this.currentEntry = getNextEntry(); return currentEntry; } public void close() throws IOException { zin.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/ZipArchiveFile.java ================================================ package com.mucommander.commons.file.impl.zip; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.zip.provider.ZipConstants; import com.mucommander.commons.file.impl.zip.provider.ZipEntry; import com.mucommander.commons.file.impl.zip.provider.ZipFile; import com.mucommander.commons.io.FilteredOutputStream; import java.io.*; import java.util.Iterator; import java.util.zip.ZipInputStream; /** * ZipArchiveFile provides read and write access (under certain conditions) to archives in the Zip format. *

    * Two different packages that implement the actual Zip compression format are used: the homemade * com.mucommander.commons.file.impl.zip.provider package and Java's java.util.zip. * com.mucommander.commons.file.impl.zip.provider provides additional functionality and improved performance over * java.util.zip but requires the underlying file to supply a RandomAccessInputStream for read * access and a RandomAccessOutputStream for write access. If the underlying file can't provide at least a * RandomAccessInputStream, the lesser java.util.zip package is used. * * @see com.mucommander.commons.file.impl.zip.ZipFormatProvider * @see com.mucommander.commons.file.impl.zip.provider.ZipFile * @author Maxence Bernard */ public class ZipArchiveFile extends AbstractRWArchiveFile { /** The ZipFile object that actually reads and modifies the entries in the Zip file */ private ZipFile zipFile; /** The date at which the current ZipFile object was created */ private long lastZipFileDate; /** Contents of an empty Zip file, 22 bytes long */ private final static byte EMPTY_ZIP_BYTES[] = { 0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; /** * Creates a new ZipArchiveFile on top of the given file. * * @param file the underlying archive file */ public ZipArchiveFile(AbstractFile file) { super(file); } /** * Checks if the underlying Zip file is up-to-date, i.e. exists and has not changed without this archive file * being aware of it. If one of those 2 conditions are not met, (re)load the ZipFile instance (parse the entries) * and declare the Zip file as up-to-date. * * @throws IOException if an error occurred while reloading */ private void checkZipFile() throws IOException { long currentDate = file.getLastModifiedDate(); if(zipFile==null || currentDate!=lastZipFileDate) { zipFile = new ZipFile(file); declareZipFileUpToDate(); } } /** * Declare the underlying Zip file as up-to-date. Calling this method after the Zip file has been modified prevents * {@link #checkZipFile()} from being reloaded. */ private void declareZipFileUpToDate() { lastZipFileDate = file.getLastModifiedDate(); } /** * Creates and returns a {@link com.mucommander.commons.file.impl.zip.provider.ZipEntry} instance using the attributes * of the given {@link ArchiveEntry}. * * @param entry the object that serves to initialize the attributes of the returned ZipEntry * @return a ZipEntry whose attributes are fetched from the given ZipEntry */ private ZipEntry createZipEntry(ArchiveEntry entry) { boolean isDirectory = entry.isDirectory(); String path = entry.getPath(); if(isDirectory && !path.endsWith("/")) path += "/"; com.mucommander.commons.file.impl.zip.provider.ZipEntry zipEntry = new com.mucommander.commons.file.impl.zip.provider.ZipEntry(path); zipEntry.setMethod(ZipConstants.DEFLATED); zipEntry.setTime(System.currentTimeMillis()); zipEntry.setUnixMode(SimpleFilePermissions.padPermissions(entry.getPermissions(), isDirectory ? FilePermissions.DEFAULT_DIRECTORY_PERMISSIONS : FilePermissions.DEFAULT_FILE_PERMISSIONS).getIntValue()); return zipEntry; } /** * Creates and return an {@link ArchiveEntry()} whose attributes are fetched from the given {@link com.mucommander.commons.file.impl.zip.provider.ZipEntry}. * It is worth noting that the returned entry has the {@link ArchiveEntry#exists exists} flag set to true. * * @param zipEntry the object that serves to initialize the attributes of the returned ArchiveEntry * @return an ArchiveEntry whose attributes are fetched from the given ZipEntry */ static ArchiveEntry createArchiveEntry(ZipEntry zipEntry) { ArchiveEntry entry = new ArchiveEntry(zipEntry.getName(), zipEntry.isDirectory(), zipEntry.getTime(), zipEntry.getSize(), true); if(zipEntry.hasUnixMode()) entry.setPermissions(new SimpleFilePermissions(zipEntry.getUnixMode())); entry.setEntryObject(zipEntry); return entry; } /** * Adds the given {@link ArchiveEntry} to the entries tree and declares the Zip file and entries tree up-to-date. * * @param entry the entry to add to the entries tree * @throws IOException if an error occurred while adding the entry to the tree * @throws UnsupportedFileOperationException if this operation is not supported by the underlying filesystem, * or is not implemented. */ private void finishAddEntry(ArchiveEntry entry) throws IOException, UnsupportedFileOperationException { // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); // Add the new entry to the entries tree addToEntriesTree(entry); } ////////////////////////////////////////// // AbstractROArchiveFile implementation // ////////////////////////////////////////// @Override public synchronized ArchiveEntryIterator getEntryIterator() throws IOException { // If the underlying AbstractFile has random read access, use our own ZipFile implementation to read entries if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { checkZipFile(); final Iterator iterator = zipFile.getEntries(); return new ArchiveEntryIterator() { public ArchiveEntry nextEntry() { ZipEntry entry; if(!iterator.hasNext() || (entry = iterator.next())==null) return null; return createArchiveEntry(entry); } public void close() { } }; } // If the underlying AbstractFile doesn't have random read access, use java.util.zip.ZipInputStream to // read the entries. This is much slower than the former method as the file cannot be sought through and needs // to be traversed. else { return new JavaUtilZipEntryIterator(new ZipInputStream(file.getInputStream())); } } @Override public synchronized InputStream getEntryInputStream(ArchiveEntry entry, ArchiveEntryIterator entryIterator) throws IOException { // If the underlying AbstractFile has random read access, use our own ZipFile implementation to read the entry if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { checkZipFile(); ZipEntry zipEntry = (com.mucommander.commons.file.impl.zip.provider.ZipEntry)entry.getEntryObject(); if(zipEntry==null) // Should not normally happen throw new IOException(); return zipFile.getInputStream(zipEntry); } // If the underlying AbstractFile doesn't have random read access, use java.util.InputStream to // read the entry. This is much slower than the former method as the file cannot be seeked and needs // to be traversed to locate the entry we're interested in. else { // Optimization: first check if the specified iterator is positionned at the beginning of the entry. // This will typically be the case if an iterator is being used to read all the archive's entries // (unpack operation). In that case, we save the cost of looking for the entry in the archive. if(entryIterator!=null && (entryIterator instanceof JavaUtilZipEntryIterator)) { ArchiveEntry currentEntry = ((JavaUtilZipEntryIterator)entryIterator).getCurrentEntry(); if(currentEntry.getPath().equals(entry.getPath())) { // The entry/zip stream is wrapped in a FilterInputStream where #close is implemented as a no-op: // we don't want the ZipInputStream to be closed when the caller closes the entry's stream. return new FilterInputStream(((JavaUtilZipEntryIterator)entryIterator).getZipInputStream()) { @Override public void close() { // No-op } }; } // This is not the one, look for the entry from the beginning of the archive } // Iterate through the archive until we've found the entry java.util.zip.ZipInputStream zin = new java.util.zip.ZipInputStream(file.getInputStream()); java.util.zip.ZipEntry zipEntry; String entryPath = entry.getPath(); // Iterate until we find the entry we're looking for while ((zipEntry=zin.getNextEntry())!=null) if (zipEntry.getName().equals(entryPath)) // That's the one, return it return zin; throw new IOException("Unknown Zip entry: "+entry.getName()); } } ////////////////////////////////////////// // AbstractRWArchiveFile implementation // ////////////////////////////////////////// @Override public synchronized OutputStream addEntry(final ArchiveEntry entry) throws IOException { checkZipFile(); final ZipEntry zipEntry = createZipEntry(entry); if(zipEntry.isDirectory()) { // Add the new directory entry to the zip file (physically) zipFile.addEntry(zipEntry); // Set the ZipEntry object into the ArchiveEntry entry.setEntryObject(zipEntry); // Declare the zip file and entries tree up-to-date and add the new entry to the entries tree finishAddEntry(entry); return null; } else { // Set the ZipEntry object into the ArchiveEntry entry.setEntryObject(zipEntry); return new FilteredOutputStream(zipFile.addEntry(zipEntry)) { @Override public void close() throws IOException { super.close(); // Declare the zip file and entries tree up-to-date and add the new entry to the entries tree finishAddEntry(entry); } }; } } @Override public synchronized void deleteEntry(ArchiveEntry entry) throws IOException { ZipEntry zipEntry = (com.mucommander.commons.file.impl.zip.provider.ZipEntry)entry.getEntryObject(); // Most of the time, the ZipEntry will not be null. However, it can be null in some rare cases, when directory // entries have been created in the entries tree but don't exist in the Zip file. // That is the case when a file entry exists in the Zip file but has no directory entry for the parent. if(zipEntry!=null) { // Entry exists physically in the zip file checkZipFile(); // Delete the entry from the zip file (physically) zipFile.deleteEntry(zipEntry); // Remove the ZipEntry object from the AchiveEntry entry.setEntryObject(null); // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); } // Else entry doesn't physically exist in the zip file, only in the entries tree // Remove the entry from the entries tree removeFromEntriesTree(entry); } @Override public void updateEntry(ArchiveEntry entry) throws IOException { ZipEntry zipEntry = (com.mucommander.commons.file.impl.zip.provider.ZipEntry)entry.getEntryObject(); // Most of the time, the ZipEntry will not be null. However, it can be null in some rare cases, when directory // entries have been created in the entries tree but don't exist in the Zip file. // That is the case when a file entry exists in the Zip file but has no directory entry for the parent. if(zipEntry!=null) { // Entry exists physically in the zip file checkZipFile(); zipEntry.setTime(entry.getLastModifiedDate()); zipEntry.setUnixMode(entry.getPermissions().getIntValue()); // Physically update the entry's attributes in the Zip file zipFile.updateEntry(zipEntry); // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); } } @Override public synchronized void optimizeArchive() throws IOException { checkZipFile(); // Defragment the zip file zipFile.defragment(); // Declare the zip file and entries tree up-to-date declareZipFileUpToDate(); declareEntriesTreeUpToDate(); } //////////////////////// // Overridden methods // //////////////////////// /** * Returns true only if the proxied archive file has random read and write access, as reported * by {@link AbstractFile#isFileOperationSupported(FileOperation)}. If that is not the case, this archive has * read-only access and behaves just like a {@link com.mucommander.commons.file.AbstractROArchiveFile}. * * @return true only if the proxied archive file has random read and write access */ @Override public boolean isWritable() { return file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE) && file.isFileOperationSupported(FileOperation.RANDOM_WRITE_FILE); } /** * Creates an empty, valid Zip file. The resulting file is 22 bytes long. */ @Override public void mkfile() throws IOException { if(exists()) throw new IOException(); copyStream(new ByteArrayInputStream(EMPTY_ZIP_BYTES), false, EMPTY_ZIP_BYTES.length); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/ZipFormatProvider.java ================================================ package com.mucommander.commons.file.impl.zip; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import java.io.IOException; /** * This class is the provider for the 'Zip' archive format implemented by {@link ZipArchiveFile}. * * @see com.mucommander.commons.file.impl.zip.ZipArchiveFile * @author Nicolas Rinaudo, Maxence Bernard */ public class ZipFormatProvider implements ArchiveFormatProvider { private static final String[] EXTENSIONS = {".zip", ".jar", ".war", ".wal", ".wmz", ".xpi", ".ear", ".sar", ".odt", ".ods", ".odp", ".odg", ".odf", ".egg", ".epub", ".cbz", ".kar"}; /** * Static instance of the filename filter that matches archive filenames * */ private static final ExtensionFilenameFilter FILENAME_FILTER = new ExtensionFilenameFilter(EXTENSIONS); @Override public AbstractArchiveFile getFile(AbstractFile file) throws IOException { return new ZipArchiveFile(file); } @Override public FilenameFilter getFilenameFilter() { return FILENAME_FILTER; } @Override public String[] getFileExtensions() { return EXTENSIONS; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/package.html ================================================ Provides an implementation of the ZIP archive format. ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/AsiExtraField.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import java.util.zip.CRC32; import java.util.zip.ZipException; /** * Adds Unix file permission and UID/GID fields as well as symbolic * link handling. * *

    This class uses the ASi extra field in the format: *

     *         Value         Size            Description
     *         -----         ----            -----------
     * (Unix3) 0x756e        Short           tag for this extra block type
     *         TSize         Short           total data size for this block
     *         CRC           Long            CRC-32 of the remaining data
     *         Mode          Short           file permissions
     *         SizDev        Long            symlink'd size OR major/minor dev num
     *         UID           Short           user ID
     *         GID           Short           group ID
     *         (var.)        variable        symbolic link filename
     * 
    * taken from appnote.iz (Info-ZIP note, 981119) found at ftp://ftp.uu.net/pub/archiving/zip/doc/ * *

    Short is two bytes and Long is four bytes in big endian byte and * word order, device numbers are currently not supported. * *

    --------------------------------------------------------------------------------------------------------------
    *
    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public class AsiExtraField implements ZipExtraField, UnixStat, Cloneable { private static final ZipShort HEADER_ID = new ZipShort(0x756E); /** * Standard Unix stat(2) file mode. */ private int mode = 0; /** * User ID. */ private int uid = 0; /** * Group ID. */ private int gid = 0; /** * File this entry points to, if it is a symbolic link. * *

    empty string - if entry is not a symbolic link. */ private String link = ""; /** * Is this an entry for a directory? */ private boolean dirFlag = false; /** * Instance used to calculate checksums. */ private CRC32 crc = new CRC32(); /** Constructor for AsiExtraField. */ public AsiExtraField() { } /** * The Header-ID. * @return the value for the header id for this extrafield */ public ZipShort getHeaderId() { return HEADER_ID; } /** * Length of the extra field in the local file data - without * Header-ID or length specifier. * @return a ZipShort for the length of the data of this extra field */ public ZipShort getLocalFileDataLength() { return new ZipShort(4 // CRC + 2 // Mode + 4 // SizDev + 2 // UID + 2 // GID + getLinkedFile().getBytes().length); } /** * Delegate to local file data. * @return the centralDirectory length */ public ZipShort getCentralDirectoryLength() { return getLocalFileDataLength(); } /** * The actual data to put into local file data - without Header-ID * or length specifier. * @return get the data */ public byte[] getLocalFileDataData() { // CRC will be added later byte[] data = new byte[getLocalFileDataLength().getValue() - 4]; ZipShort.getBytes(getMode(), data, 0); byte[] linkArray = getLinkedFile().getBytes(); ZipLong.getBytes(linkArray.length, data, 2); ZipShort.getBytes(getUserId(), data, 6); ZipShort.getBytes(getGroupId(), data, 8); System.arraycopy(linkArray, 0, data, 10, linkArray.length); crc.reset(); crc.update(data); long checksum = crc.getValue(); byte[] result = new byte[data.length + 4]; ZipLong.getBytes(checksum, result, 0); System.arraycopy(data, 0, result, 4, data.length); return result; } /** * Delegate to local file data. * @return the local file data */ public byte[] getCentralDirectoryData() { return getLocalFileDataData(); } /** * Set the user id. * @param uid the user id */ public void setUserId(int uid) { this.uid = uid; } /** * Get the user id. * @return the user id */ public int getUserId() { return uid; } /** * Set the group id. * @param gid the group id */ public void setGroupId(int gid) { this.gid = gid; } /** * Get the group id. * @return the group id */ public int getGroupId() { return gid; } /** * Indicate that this entry is a symbolic link to the given filename. * * @param name Name of the file this entry links to, empty String * if it is not a symbolic link. */ public void setLinkedFile(String name) { link = name; mode = getMode(mode); } /** * Name of linked file * * @return name of the file this entry links to if it is a * symbolic link, the empty string otherwise. */ public String getLinkedFile() { return link; } /** * Is this entry a symbolic link? * @return true if this is a symbolic link */ public boolean isLink() { return getLinkedFile().length() != 0; } /** * File mode of this file. * @param mode the file mode */ public void setMode(int mode) { this.mode = getMode(mode); } /** * File mode of this file. * @return the file mode */ public int getMode() { return mode; } /** * Indicate whether this entry is a directory. * @param dirFlag if true, this entry is a directory */ public void setDirectory(boolean dirFlag) { this.dirFlag = dirFlag; mode = getMode(mode); } /** * Is this entry a directory? * @return true if this entry is a directory */ public boolean isDirectory() { return dirFlag && !isLink(); } /** * Populate data from this array as if it was in local file data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * @throws ZipException on error */ public void parseFromLocalFileData(byte[] data, int offset, int length) throws ZipException { long givenChecksum = ZipLong.getValue(data, offset); byte[] tmp = new byte[length - 4]; System.arraycopy(data, offset + 4, tmp, 0, length - 4); crc.reset(); crc.update(tmp); long realChecksum = crc.getValue(); if (givenChecksum != realChecksum) { throw new ZipException("bad CRC checksum " + Long.toHexString(givenChecksum) + " instead of " + Long.toHexString(realChecksum)); } int newMode = ZipShort.getValue(tmp, 0); byte[] linkArray = new byte[(int) ZipLong.getValue(tmp, 2)]; uid = ZipShort.getValue(tmp, 6); gid = ZipShort.getValue(tmp, 8); if (linkArray.length == 0) { link = ""; } else { System.arraycopy(tmp, 10, linkArray, 0, linkArray.length); link = new String(linkArray); } setDirectory((newMode & DIR_FLAG) != 0); setMode(newMode); } /** * Get the file mode for given permissions with the correct file type. * @param mode the mode * @return the type with the mode */ protected int getMode(int mode) { int type = FILE_FLAG; if (isLink()) { type = LINK_FLAG; } else if (isDirectory()) { type = DIR_FLAG; } return type | (mode & PERM_MASK); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/DeflatedOutputStream.java ================================================ package com.mucommander.commons.file.impl.zip.provider; import java.io.IOException; import java.io.OutputStream; import java.util.zip.Deflater; /** * DeflatedOutputStream compresses data using the DEFLATED compression method. * *

    *
    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Maxence Bernard */ public class DeflatedOutputStream extends ZipEntryOutputStream { /** Deflater instance that does the actual compression work */ protected Deflater deflater; /** Buffer used to deflate data */ protected byte[] buf; /** * Creates a new DeflatedOutputStream that writes compressed data to the given OutputStream * and automatically updates the supplied CRC32 checksum. * * @param out the OutputStream where the compressed data is sent to * @param deflater the Deflater that compresses data, reset before first use * @param buf the buffer used to deflate data */ public DeflatedOutputStream(OutputStream out, Deflater deflater, byte buf[]) { super(out, ZipConstants.DEFLATED); this.deflater = deflater; this.buf = buf; deflater.reset(); } /** * Writes next block of compressed data to the output stream. * * @throws java.io.IOException on error */ protected void deflate() throws IOException { int len = deflater.deflate(buf, 0, buf.length); if (len > 0) { out.write(buf, 0, len); } } /** * Finishes writing the DEFLATED-compressed data. * * @throws IOException if an I/O occurred */ public void finishDeflate() throws IOException { deflater.finish(); while (!deflater.finished()) { deflate(); } } ///////////////////////////////////////// // ZipEntryOutputStream implementation // ///////////////////////////////////////// @Override public int getTotalIn() { return deflater.getTotalIn(); } @Override public int getTotalOut() { return deflater.getTotalOut(); } ///////////////////////////////// // OutputStream implementation // ///////////////////////////////// /** * Writes the given bytes to the Zip entry. * * @param b the byte array to write * @param offset the start position to write from * @param length the number of bytes to write * @throws java.io.IOException on error */ @Override public void write(byte[] b, int offset, int length) throws IOException { if (length > 0) { if (!deflater.finished()) { deflater.setInput(b, offset, length); while (!deflater.needsInput()) { deflate(); } } } crc.update(b, offset, length); } /** * Completes writing the entry without closing the underlying OutputStream. * * @throws IOException */ @Override public void close() throws IOException { finishDeflate(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ExtraFieldUtils.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.zip.ZipException; /** * ZipExtraField related methods. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public class ExtraFieldUtils { /** * Static registry of known extra fields. */ private static final Map> implementations = new Hashtable<>(); static { register(AsiExtraField.class); register(JarMarker.class); } /** * Register a ZipExtraField implementation. * *

    The given class must have a no-arg constructor and implement * the {@link ZipExtraField ZipExtraField interface}. * @param c the class to register */ public static void register(Class c) { try { ZipExtraField ze = c.getDeclaredConstructor().newInstance(); implementations.put(ze.getHeaderId(), c); } catch (ClassCastException cc) { throw new RuntimeException(c + " doesn't implement ZipExtraField"); } catch (InstantiationException ie) { throw new RuntimeException(c + " is not a concrete class"); } catch (IllegalAccessException | InvocationTargetException ie) { throw new RuntimeException(c + "'s no-arg constructor is not public"); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } /** * create an instance of the appropriate ExtraField, falls back to * {@link UnrecognizedExtraField UnrecognizedExtraField}. * @param headerId the header identifier * @return an instance of the appropriate ExtraField * @exception InstantiationException if unable to instantiate the class * @exception IllegalAccessException if not allowed to instantiate the class */ public static ZipExtraField createExtraField(ZipShort headerId) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Class c = implementations.get(headerId); if (c != null) { return c.getDeclaredConstructor().newInstance(); } UnrecognizedExtraField u = new UnrecognizedExtraField(); u.setHeaderId(headerId); return u; } /** * Split the array into ExtraFields and populate them with the * give data. * @param data an array of bytes * @return an array of ExtraFields * @throws ZipException on error */ public static ZipExtraField[] parse(byte[] data) throws ZipException { List v = new ArrayList<>(); int start = 0; while (start <= data.length - 4) { ZipShort headerId = new ZipShort(data, start); int length = (new ZipShort(data, start + 2)).getValue(); if (start + 4 + length > data.length) { throw new ZipException("data starting at " + start+ " is in unknown format"); } try { ZipExtraField ze = createExtraField(headerId); ze.parseFromLocalFileData(data, start + 4, length); v.add(ze); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException ie) { throw new ZipException(ie.getMessage()); } start += (length + 4); } if (start != data.length) { // array not exhausted throw new ZipException("data starting at " + start + " is in unknown format"); } return v.toArray(new ZipExtraField[0]); } /** * Merges the local file data fields of the given ZipExtraFields. * @param data an array of ExtraFiles * @return an array of bytes */ public static byte[] mergeLocalExtraFields(ZipExtraField[] data) { int sum = 4 * data.length; for (ZipExtraField d : data) { sum += d.getLocalFileDataLength().getValue(); } byte[] result = new byte[sum]; int start = 0; for (ZipExtraField d : data) { System.arraycopy(d.getHeaderId().getBytes(), 0, result, start, 2); System.arraycopy(d.getLocalFileDataLength().getBytes(), 0, result, start + 2, 2); byte[] local = d.getLocalFileDataData(); System.arraycopy(local, 0, result, start + 4, local.length); start += (local.length + 4); } return result; } /** * Merges the central directory fields of the given ZipExtraFields. * @param data an array of ExtraFields * @return an array of bytes */ public static byte[] mergeCentralExtraFields(ZipExtraField[] data) { int sum = 4 * data.length; for (ZipExtraField d : data) { sum += d.getCentralDirectoryLength().getValue(); } byte[] result = new byte[sum]; int start = 0; for (ZipExtraField d : data) { System.arraycopy(d.getHeaderId().getBytes(), 0, result, start, 2); System.arraycopy(d.getCentralDirectoryLength().getBytes(), 0, result, start + 2, 2); byte[] local = d.getCentralDirectoryData(); System.arraycopy(local, 0, result, start + 4, local.length); start += (local.length + 4); } return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/JarMarker.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import java.util.zip.ZipException; /** * If this extra field is added as the very first extra field of the * archive, Solaris will consider it an executable jar file. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public final class JarMarker implements ZipExtraField { private static final ZipShort ID = new ZipShort(0xCAFE); private static final ZipShort NULL = new ZipShort(0); private static final byte[] NO_BYTES = new byte[0]; private static final JarMarker DEFAULT = new JarMarker(); /** No-arg constructor */ public JarMarker() { // empty } /** * Since JarMarker is stateless we can always use the same instance. * @return the DEFAULT jarmaker. */ public static JarMarker getInstance() { return DEFAULT; } /** * The Header-ID. * @return the header id */ public ZipShort getHeaderId() { return ID; } /** * Length of the extra field in the local file data - without * Header-ID or length specifier. * @return 0 */ public ZipShort getLocalFileDataLength() { return NULL; } /** * Length of the extra field in the central directory - without * Header-ID or length specifier. * @return 0 */ public ZipShort getCentralDirectoryLength() { return NULL; } /** * The actual data to put into local file data - without Header-ID * or length specifier. * @return the data */ public byte[] getLocalFileDataData() { return NO_BYTES; } /** * The actual data to put central directory - without Header-ID or * length specifier. * @return the data */ public byte[] getCentralDirectoryData() { return NO_BYTES; } /** * Populate data from this array as if it was in local file data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * * @throws ZipException on error */ public void parseFromLocalFileData(byte[] data, int offset, int length) throws ZipException { if (length != 0) { throw new ZipException("JarMarker doesn't expect any data"); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/StoredOutputStream.java ================================================ package com.mucommander.commons.file.impl.zip.provider; import java.io.IOException; import java.io.OutputStream; /** * StoredOutputStream compresses data using the STORED compression method (i.e. no compression). * *

    --------------------------------------------------------------------------------------------------------------
    *
    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Maxence Bernard */ public class StoredOutputStream extends ZipEntryOutputStream { /** Number of bytes in/out so far */ private int storedCount; /** * Creates a new StoredOutputStream that writes compressed data to the given OutputStream * and automatically updates the supplied CRC32 checksum. * * @param out the OutputStream where the compressed data is sent to */ public StoredOutputStream(OutputStream out) { super(out, ZipConstants.STORED); } ///////////////////////////////////////// // ZipEntryOutputStream implementation // ///////////////////////////////////////// @Override public int getTotalIn() { return storedCount; } @Override public int getTotalOut() { return storedCount; } ///////////////////////////////// // OutputStream implementation // ///////////////////////////////// @Override public void write(byte[] b, int offset, int length) throws IOException { out.write(b, offset, length); storedCount += length; crc.update(b, offset, length); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/UnixStat.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; /** * Constants from stat.h on Unix systems. * *

    --------------------------------------------------------------------------------------------------------------
    *
    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public interface UnixStat { /** * Bits used for permissions (and sticky bit) */ int PERM_MASK = 07777; /** * Indicates symbolic links. */ int LINK_FLAG = 0120000; /** * Indicates plain files. */ int FILE_FLAG = 0100000; /** * Indicates directories. */ int DIR_FLAG = 040000; // ---------------------------------------------------------- // somewhat arbitrary choices that are quite common for shared // installations // ----------------------------------------------------------- /** * Default permissions for symbolic links. */ int DEFAULT_LINK_PERM = 0777; /** * Default permissions for directories. */ int DEFAULT_DIR_PERM = 0755; /** * Default permissions for plain files. */ int DEFAULT_FILE_PERM = 0644; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/UnrecognizedExtraField.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; /** * Simple placeholder for all those extra fields we don't want to deal * with. * *

    Assumes local file data and central directory entries are * identical - unless told the opposite. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public class UnrecognizedExtraField implements ZipExtraField { /** * The Header-ID. */ private ZipShort headerId; /** * Set the header id. * @param headerId the header id to use */ public void setHeaderId(ZipShort headerId) { this.headerId = headerId; } /** * Get the header id. * @return the header id */ public ZipShort getHeaderId() { return headerId; } /** * Extra field data in local file data - without * Header-ID or length specifier. */ private byte[] localData; /** * Set the extra field data in the local file data - * without Header-ID or length specifier. * @param data the field data to use */ public void setLocalFileDataData(byte[] data) { localData = data; } /** * Get the length of the local data. * @return the length of the local data */ public ZipShort getLocalFileDataLength() { return new ZipShort(localData.length); } /** * Get the local data. * @return the local data */ public byte[] getLocalFileDataData() { return localData; } /** * Extra field data in central directory - without * Header-ID or length specifier. */ private byte[] centralData; /** * Set the extra field data in central directory. * @param data the data to use */ public void setCentralDirectoryData(byte[] data) { centralData = data; } /** * Get the central data length. * If there is no central data, get the local file data length. * @return the central data length */ public ZipShort getCentralDirectoryLength() { if (centralData != null) { return new ZipShort(centralData.length); } return getLocalFileDataLength(); } /** * Get the central data. * @return the central data if present, else return the local file data */ public byte[] getCentralDirectoryData() { if (centralData != null) { return centralData; } return getLocalFileDataData(); } /** * @param data the array of bytes. * @param offset the source location in the data array. * @param length the number of bytes to use in the data array. * @see ZipExtraField#parseFromLocalFileData(byte[], int, int) */ public void parseFromLocalFileData(byte[] data, int offset, int length) { byte[] tmp = new byte[length]; System.arraycopy(data, offset, tmp, 0, length); setLocalFileDataData(tmp); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipBuffer.java ================================================ package com.mucommander.commons.file.impl.zip.provider; /** * ZipBuffer is a C struct-like class that holds byte buffers that are used to convert Java values to Big Endian byte * arrays. It allows to reuse the same byte buffers instead of instanciating new ones for each conversion. * * @see ZipShort#getBytes(int, byte[], int) * @see ZipLong#getBytes(long, byte[], int) * @author Maxence Bernard */ public class ZipBuffer { /** 2-byte buffer that can hold a Zip short value */ byte[] shortBuffer = new byte[2]; /** 2-byte buffer that can hold a Zip long value */ byte[] longBuffer = new byte[4]; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipConstants.java ================================================ package com.mucommander.commons.file.impl.zip.provider; import java.util.zip.Deflater; /** * Contains the various constants that are used by several classes of this package. * * @author Maxence Bernard */ public interface ZipConstants { /** * DEFLATED compression method */ int DEFLATED = java.util.zip.ZipEntry.DEFLATED; /** * STORED compression method (raw storage, no compression) */ int STORED = java.util.zip.ZipEntry.STORED; /** * Default compression level for DEFLATED compression */ int DEFAULT_DEFLATER_COMPRESSION = Deflater.DEFAULT_COMPRESSION; /** * Default size of the buffer used by Deflater. */ // /!\ For some unknown reason, using a larger buffer *hurts* performance. int DEFAULT_DEFLATER_BUFFER_SIZE = 512; /** * Maximum size of a Zip32 entry or a Zip32 file as a whole, i.e. (2^32)-1. * */ long MAX_ZIP32_SIZE = 4294967295l; /** * Size of write buffers */ int WRITE_BUFFER_SIZE = 65536; /** * UTF-8 encoding String */ String UTF_8 = "UTF-8"; /** * Local file header signature */ byte[] LFH_SIG = ZipLong.getBytes(0X04034B50L); /** * Data descriptor signature */ byte[] DD_SIG = ZipLong.getBytes(0X08074B50L); /** * Central file header signature */ byte[] CFH_SIG = ZipLong.getBytes(0X02014B50L); /** * End of central dir signature */ byte[] EOCD_SIG = ZipLong.getBytes(0X06054B50L); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipEntry.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import lombok.Getter; import lombok.Setter; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.*; /** * Extension that adds better handling of extra fields and provides * access to the internal and external file attributes. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public class ZipEntry implements Cloneable { /** Name/path of this entry * -- GETTER -- * Returns the name of this entry. */ @Getter protected String name; /** Uncompressed size of the entry data * -- GETTER -- * Returns the uncompressed size of the entry data, or -1 if not known. */ @Getter protected long size = -1; /** Compressed size of the entry data * -- GETTER -- * Returns the size of the compressed entry data, or -1 if not known. In the case of a stored entry, * the compressed size will be the same as the uncompressed size of the entry. */ @Getter private long compressedSize = -1; /** CRC-32 checksum of the uncompressed entry data * -- GETTER -- * Returns the CRC-32 checksum of the uncompressed entry data, or -1 if not known. */ @Getter protected long crc = -1; /** Data/time of this entry, in the DOS time format */ private long dosTime = -1; /** Data/time of this entry, in the Java time format */ private long javaTime = -1; /** Compression method that was used for the entry data * -- GETTER -- * Returns the compression method of the entry, or -1 if not specified. */ @Getter protected int method = -1; /** An optional comment for this entry * -- GETTER -- * Returns the comment string for the entry, or null if there is none. */ @Getter protected String comment; /** Platform, part of the 'version made by' central directory field * -- GETTER -- * Returns the platform specification to put into the 'version made by' part of the central file header. * {@link #PLATFORM_FAT} unless {@link #setUnixMode setUnixMode} has been called, * in which case {@link #PLATFORM_UNIX} will be returned. */ @Getter protected int platform = PLATFORM_FAT; /** Internal attributes (2 bytes) * -- GETTER -- * Retrieves the internal file attributes. * -- SETTER -- * Sets the internal file attributes. */ @Setter @Getter private int internalAttributes = 0; /** External attributes (4 bytes) * -- GETTER -- * Retrieves the external file attributes. * -- SETTER -- * Sets the external file attributes. */ @Setter @Getter private long externalAttributes = 0; /** List of extra fields, as ZipEntraField instances */ private List extraFields; /** Contains info about how this entry is stored in the zip file */ private ZipEntryInfo entryInfo; /** Smallest DOS time (Epoch 1980) */ private final static long MIN_DOS_TIME = 0x00002100L; private static final ZoneId DEFAULT_ZONE = ZoneId.systemDefault(); /** Value of the bit flag that denotes a Unix directory in the external attributes */ private final static int UNIX_DIRECTORY_FLAG = 16384; /** Value of the bit flag that denotes a Unix file in the external attributes */ private final static int UNIX_FILE_FLAG = 32768; /** Value of the bit flag that denotes an MS-DOS directory in the external attributes */ private final static int MSDOS_DIRECTORY_FLAG = 0x10; /** Value of the bit flag that denotes a read-only MS-DOS file in the external attributes */ private final static int MSDOS_READ_ONLY_FLAG = 1; /** Value of the user write permission bit */ private final static int USER_WRITE_PERMISSION_BIT = 128; /** Value of the Unix platform used in the 'version made by' central directory field */ private static final int PLATFORM_UNIX = 3; /** Value of the MSDOS/OS-2 platform (FAT filesystem) used in the 'version made by' central directory field */ static final int PLATFORM_FAT = 0; /** * Creates a new Zip entry with an empty name. */ public ZipEntry() { this(""); } /** * Creates a new Zip entry with the specified name. * * @param name the name of the entry */ public ZipEntry(String name) { this.name = name; } /** * Creates a new Zip entry with fields taken from the specified zip entry. * * @param entry the entry to get fields from */ public ZipEntry(java.util.zip.ZipEntry entry) { this.name = entry.getName(); this.crc = entry.getCrc(); this.size = entry.getSize(); this.compressedSize = entry.getCompressedSize(); this.method = entry.getMethod(); this.comment = entry.getComment(); setExtra(entry.getExtra()); // ZipEntry.getTime() has to do a DOS time to Java time conversion, and we have to do the opposite. // This is inefficient but there is unfortunately no way to retrieve the DOS time field as it is private. setTime(entry.getTime()); } /** * Sets Unix permissions in a way that is understood by Info-Zip's unzip command. * * @param mode an int value */ public void setUnixMode(int mode) { boolean isDirectory = isDirectory(); setExternalAttributes( // Unix directory flag ((isDirectory ? UNIX_DIRECTORY_FLAG : UNIX_FILE_FLAG) << 16) // Unix file permissions | ((long) mode << 16) // MS-DOS read-only attribute | ((mode & USER_WRITE_PERMISSION_BIT) == 0 ? MSDOS_READ_ONLY_FLAG : 0) // MS-DOS directory flag | (isDirectory ? MSDOS_DIRECTORY_FLAG : 0)); platform = PLATFORM_UNIX; } /** * Unix permission. * * @return the unix permissions */ public int getUnixMode() { return (int) ((getExternalAttributes() >> 16) & 0xFFFF); } /** * Returns true if this ZipEntry has Unix mode/permissions. * If that's not the case, the value returned by {@link #getUnixMode()} has no meaning. * * @return true if this ZipEntry has Unix mode/permissions */ public boolean hasUnixMode() { return getPlatform()==PLATFORM_UNIX; } /** * Sets the platform: {@link #PLATFORM_FAT} or {@link #PLATFORM_UNIX}. * * @param platform {@link #PLATFORM_FAT} or {@link #PLATFORM_UNIX} */ protected void setPlatform(int platform) { this.platform = platform; } /** * Replaces all current extra fields with the specified ones. * * @param fields an array of extra fields */ public void setExtraFields(ZipExtraField[] fields) { extraFields = new ArrayList<>(); Collections.addAll(extraFields, fields); } /** * Returns the extra fields of this entry. * * @return the extra fields of this entry */ public ZipExtraField[] getExtraFields() { if (extraFields == null) { return new ZipExtraField[0]; } return extraFields.toArray(new ZipExtraField[0]); } /** * Adds an extra fields, replacing any extra field of the same type previously added. * * @param ze the extra field to add */ public void addExtraField(ZipExtraField ze) { if (extraFields == null) { extraFields = new Vector<>(); } ZipShort type = ze.getHeaderId(); for (int i = 0, nbFields = extraFields.size(); i < nbFields; i++) { if (extraFields.get(i).getHeaderId().equals(type)) { extraFields.set(i, ze); return; } } extraFields.add(ze); } /** * Removes the first extra field corresponding to the given type. * * @param type the type of extra field to remove * @return true if an extra field corresponding to given type was removed, false if no * matching field was found */ public boolean removeExtraField(ZipShort type) { if (extraFields == null) { return false; } for (int i = 0; i < extraFields.size(); i++) { var field = extraFields.get(i); if (field != null && Objects.equals(field.getHeaderId(), type)) { extraFields.remove(i); return true; } } return false; } /** * Returns the data of the local file extra fields. The returned byte array may be empty but never * null. * * @return the data of the local file extra fields */ public byte[] getLocalFileDataExtra() { return ExtraFieldUtils.mergeLocalExtraFields(getExtraFields()); } /** * Returns the data of the central directory extra fields. The returned byte array may be empty but never * null. * * @return the data of the central directory extra fields */ public byte[] getCentralDirectoryExtra() { return ExtraFieldUtils.mergeCentralExtraFields(getExtraFields()); } /** * Sets the name of this entry. * * @param name the new name for this entry */ protected void setName(String name) { this.name = name; } /** * Returns true if the entry is a directory. Directory entries are characterized by their name * ending with a '/' character. * * @return true if the entry is a directory */ public boolean isDirectory() { return getName().endsWith("/"); } /** * Returns the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file. * * @return the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file */ protected ZipEntryInfo getEntryInfo() { return entryInfo; } /** * Sets the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file. * * @param entryInfo the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file */ protected void setEntryInfo(ZipEntryInfo entryInfo) { this.entryInfo = entryInfo; } /** * Sets the uncompressed size of the entry data. * * @param size the uncompressed size in bytes * @throws IllegalArgumentException if the specified size is less than 0 or greater than 0xFFFFFFFF bytes */ public void setSize(long size) { if (!isValidUnsignedInt(size)) { throw new IllegalArgumentException("Invalid entry size"); } this.size = size; } /** * Sets the size of the compressed entry data. * * @param csize the compressed size to set to */ public void setCompressedSize(long csize) { if (!isValidUnsignedInt(csize)) { throw new IllegalArgumentException("Invalid entry size"); } this.compressedSize = csize; } /** * Sets the CRC-32 checksum of the uncompressed entry data. * * @param crc the new CRC-32 value * @throws IllegalArgumentException if the specified CRC-32 value is less than 0 or greater than 0xFFFFFFFF */ public void setCrc(long crc) { if (!isValidUnsignedInt(crc)) { throw new IllegalArgumentException("invalid entry crc-32"); } this.crc = crc; } /** * Returns this entry's date/time expressed in the Java time format, i.e. as a number of milliseconds since * the Epoch. * * @return this entry's date/time expressed in the Java time format */ public long getTime() { return javaTime; } /** * Sets this entry's date/time to the specified one. The time must be expressed in the Java time format, * i.e. as a number of milliseconds since the Epoch. * * @param javaTime the new time of this entry, expressed in the Java time format */ public void setTime(long javaTime) { this.javaTime = javaTime; this.dosTime = javaTime == -1 ? -1 : javaToDosTime(javaTime); } /** * Returns this entry's date/time expressed in the DOS time format. * * @return this entry's date/time expressed in the DOS time format */ protected long getDosTime() { return dosTime; } /** * Sets this entry's date/time to the specified one. The time must be expressed in the DOS time format. * * @param dosTime the new time of this entry, expressed in the DOS time format */ protected void setDosTime(long dosTime) { this.dosTime = dosTime; this.javaTime = dosTime < 0 ? -1 : dosToJavaTime(dosTime); } /** * Sets the compression method for the entry. * * @param method the compression method, either {@link ZipConstants#STORED} or {@link ZipConstants#DEFLATED} * @throws IllegalArgumentException if the specified compression method is invalid */ public void setMethod(int method) { if (method != ZipConstants.STORED && method != ZipConstants.DEFLATED) { throw new IllegalArgumentException("Invalid compression method"); } this.method = method; } /** * Sets the optional comment string for the entry. * * @param comment the comment string * @throws IllegalArgumentException if the length of the specified comment string is greater than 0xFFFF bytes */ public void setComment(String comment) { if (comment != null && comment.length() > 0xffff/3 && getUTF8Length(comment) > 0xffff) { throw new IllegalArgumentException("invalid entry comment length"); } this.comment = comment; } /** * Throws an IllegalArgumentException if byte array cannot be parsed into extra fields. * * @param extra an array of bytes to be parsed into extra fields * @throws IllegalArgumentException if the byte array cannot be parsed into extra fields */ public void setExtra(byte[] extra) throws IllegalArgumentException { if (extra == null || extra.length == 0) { extraFields = null; } else { try { setExtraFields(ExtraFieldUtils.parse(extra)); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage()); } } } /* * Converts DOS time (Epoch=1980) to Java time (Epoch=1970). * * @param dosTime time expressed in the convoluted DOS time format * @return time expressed as the number of milliseconds since the epoch */ protected static long dosToJavaTime(long dosTime) { int year = (int) ((dosTime >> 25) & 0x7f) + 1980; int month = (int) ((dosTime >> 21) & 0x0f) - 1; int day = (int) (dosTime >> 16) & 0x1f; int hour = (int) (dosTime >> 11) & 0x1f; int minute = (int) (dosTime >> 5) & 0x3f; int second = (int) (dosTime << 1) & 0x3e; ZonedDateTime zdt = ZonedDateTime.of(year, month + 1, day, hour, minute, second, 0, DEFAULT_ZONE); return zdt.toInstant().toEpochMilli(); } /** * Converts Java time (Epoch=1970) to DOS time (Epoch=1980). * * @param javaTime number of milliseconds since the epoch * @return time expressed in the convoluted DOS time format */ protected static long javaToDosTime(long javaTime) { Instant instant = Instant.ofEpochMilli(javaTime); ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault()); long year = zdt.getYear(); if (year < 1980) { return MIN_DOS_TIME; } return ((year - 1980) << 25) | ((zdt.getMonthValue()) << 21) | (zdt.getDayOfMonth() << 16) | (zdt.getHour() << 11) | (zdt.getMinute() << 5) | (zdt.getSecond() >> 1); } /* * Returns the length of the given String's UTF-8 representation. */ protected static int getUTF8Length(String s) { // This method is a dup from java.util.ZipOutputStream int count = 0; for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (ch <= 0x7f) { count++; } else if (ch <= 0x7ff) { count += 2; } else { count += 3; } } return count; } /** * Returns true if the given long is a valid unsigned int value, i.e. comprised between 0 and 2^32-1. * * @param l the long value to test * @return true if the given long is a valid unsigned int value, i.e. comprised between 0 and 2^32-1 */ protected boolean isValidUnsignedInt(long l) { return l >= 0 && l <= 0xFFFFFFFFL; } /** * Returns a cloned instance of this entry. * * @return a cloned instance of this entry * @throws CloneNotSupportedException should never happen */ @Override public Object clone() throws CloneNotSupportedException { ZipEntry ze = (ZipEntry)super.clone(); if (extraFields != null) { ze.extraFields = new ArrayList<>(extraFields); } return ze; } /** * Returns a hash of this entry's name. * * @return a hash of this entry's name */ public int hashCode() { return getName().hashCode(); } /** * Returns true if the given object is a ZipEntry that has the same name as this one. * * @param o the object to test for equality * @return true if the given object is a ZipEntry that has the same name as this one */ public boolean equals(Object o) { return (o instanceof ZipEntry) && ((ZipEntry) o).getName().equals(getName()); } @Override public String toString() { return "ZipEntry (" + name + ")"; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipEntryInfo.java ================================================ package com.mucommander.commons.file.impl.zip.provider; /** * ZipEntryInfo is a C struct-like class that holds the information about an entry that is used for parsing and writing * the Zip file. * * @author Maxence Bernard */ public final class ZipEntryInfo { /** Offset to the central file header */ long centralHeaderOffset = -1; /** Length of the central file header */ long centralHeaderLen = -1; /** Offset to the local file header */ long headerOffset = -1; /** Offset to the start of file data */ long dataOffset = -1; /** true if this entry has a data descriptor in the Zip file */ boolean hasDataDescriptor; /** The encoding used for filename and comment fields */ String encoding; /** The filename's bytes */ byte[] filename; /** The comment's bytes */ byte[] comment; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipEntryOutputStream.java ================================================ package com.mucommander.commons.file.impl.zip.provider; import java.io.IOException; import java.io.OutputStream; import java.util.zip.CRC32; /** * ZipEntryOutputStream is an abstract OutputStream used for compressing a Zip entry's data to an * underlying OutputStream. * *

    The CRC32 checksum is calculated on-the-fly as data gets written to the stream, {@link #getCrc()} returns the * current checksum value. The {@link #getTotalIn()} and {@link #getTotalOut()} methods keep track of the uncompressed * and compressed of the supplied data. * *

    There currently are two implementations of this class: *

      *
    • {@link com.mucommander.commons.file.impl.zip.provider.DeflatedOutputStream}: implements the DEFLATED compression method *
    • *
    • {@link com.mucommander.commons.file.impl.zip.provider.StoredOutputStream}: implements the STORED compression method * (i.e. no compression)
    • *
    * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Maxence Bernard */ public abstract class ZipEntryOutputStream extends OutputStream { /** The underlying stream where the compressed data is sent */ protected OutputStream out; /** Compression method (DEFLATED or STORED) */ protected int method; /** The CRC32 instance that calculates the checksum */ protected CRC32 crc = new CRC32(); /** * Creates a new EntryOutputStream that writes compressed data to the given OutputStream * and automatically updates the supplied CRC32 checksum. * * @param out the OutputStream where the compressed data is sent to * @param method the compression method, {@link ZipConstants#DEFLATED} or {@link ZipConstants#STORED} */ public ZipEntryOutputStream(OutputStream out, int method) { this.out = out; this.method = method; } /** * Returns the compression method used for writing the supplied data. * * @return the compression method used for writing the supplied data */ public int getMethod() { return method; } /** * Returns the CRC value of the data written so far. * * @return the CRC value of the data written so far. */ public long getCrc() { return crc.getValue(); } ///////////////////////////////////////// // Partial OutputStream implementation // ///////////////////////////////////////// @Override public void write(int b) throws IOException { byte[] array = new byte[1]; array[0] = (byte) (b & 0xff); write(array, 0, 1); } /** * Flushes the underlying OutputStream. */ @Override public void flush() throws IOException { out.flush(); } ////////////////////// // Abstract methods // ////////////////////// /** * Returns the uncompressed size of the data written so far. * * @return the uncompressed size of the data written so far */ public abstract int getTotalIn(); /** * Returns the compressed size of the data written so far. * * @return the compressed size of the data written so far */ public abstract int getTotalOut(); } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipExtraField.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import java.util.zip.ZipException; /** * General format of extra field data. * *

    Extra fields usually appear twice per file, once in the local * file data and once in the central directory. Usually they are the * same, but they don't have to be. {@link * java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} will * only use the local file data in both places. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public interface ZipExtraField { /** * The Header-ID. * @return the header id */ ZipShort getHeaderId(); /** * Length of the extra field in the local file data - without * Header-ID or length specifier. * @return the length of the field in the local file data */ ZipShort getLocalFileDataLength(); /** * Length of the extra field in the central directory - without * Header-ID or length specifier. * @return the length of the field in the central directory */ ZipShort getCentralDirectoryLength(); /** * The actual data to put into local file data - without Header-ID * or length specifier. * @return the data */ byte[] getLocalFileDataData(); /** * The actual data to put central directory - without Header-ID or * length specifier. * @return the data */ byte[] getCentralDirectoryData(); /** * Populate data from this array as if it was in local file data. * @param data an array of bytes * @param offset the start offset * @param length the number of bytes in the array from offset * * @throws ZipException on error */ void parseFromLocalFileData(byte[] data, int offset, int length) throws ZipException; } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipFile.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.io.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Vector; import java.util.zip.Deflater; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; import java.util.zip.ZipException; /** * This class is a replacement for java.util.ZipFile with some extra functionalities: *

      *
    • Ability to add or remove entries 'on-the-fly', i.e. without rewriting the whole archive. *
    • Advanced encoding support for filenames and comments. UTF-8 is used for parsing entries that explicitely declare * using UTF-8 (as per Zip specs). For entries that do not use UTF-8, the encoding is auto-detected (best effort). * Alternatively, the encoding used for parsing entries can be specified if it is known in advance. For new entries * added with {@link #addEntry(ZipEntry)}, UTF-8 is always used and declared as such in the Zip headers. *
    • Loads the internal/external file attributes and extra fields instead of ignoring them *
    * *

    This class doesn't extend java.util.zip.ZipFile as it would have to reimplement all methods anyway. * Like java.util.ZipFile, it supports compressed (DEFLATED) and uncompressed (STORED) entries. * *

    Random read access is required to instantiate a ZipFile and retrieve its entries. Furthermore, random * write access is required for methods that modify the Zip file. * *

    The method signatures mimic the ones of java.util.zip.ZipFile with a few exceptions: *

      *
    • There is no getName method.
    • *
    • There is no close method: underlying input and output streams are opened and closed automatically * as they are needed.
    • *
    • entries has been renamed to {@link #getEntries()} and returns an Iterator instead of * an Enumeration.
    • *
    • size has been renamed to {@link #getNbEntries()}.
    • *
    * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public class ZipFile implements ZipConstants { private static final Logger LOGGER = LoggerFactory.getLogger(ZipFile.class); /** The underlying archive file */ private AbstractFile file; /** The currently opened RandomAccessInputStream to the zip file (may be null) */ private RandomAccessInputStream rais; /** The currently opened RandomAccessInputStream to the zip file (may be null) */ private RandomAccessOutputStream raos; /** Contains ZipEntry instances corresponding to the archive's entries, in the order they were found in the archive. */ private Vector entries = new Vector<>(); /** Maps entry paths to corresponding ZipEntry instances */ private Map nameMap = new Hashtable<>(); /** Global zip file comment */ private String comment; /** * The default encoding to use for parsing filenames and comments. This value is only used for Zip entries that do * not have the UTF-8 flag set. If not specified (null), then automatic encoding detection is used (default). */ private String defaultEncoding = null; /** Holds byte buffer instance used to convert short and longs, avoids creating lots of small arrays */ private ZipBuffer zipBuffer = new ZipBuffer(); /** * Opens the given Zip file and parses information about the entries it contains. * *

    The given {@link AbstractFile} must have random read access. If not, an IOException will be * thrown. * * @param f the archive file * @throws IOException if an error occurred while reading the Zip file. * @throws ZipException if this file is not a valid Zip file * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ public ZipFile(AbstractFile f) throws IOException, ZipException, UnsupportedFileOperationException { this.file = f; try { openRead(); parseCentralDirectory(); } finally { closeRead(); } } /** * Opens the zip file for random read access. * * @throws IOException if an error occured while opening the zip file for random read access. * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ private void openRead() throws IOException, UnsupportedFileOperationException { if(rais!=null) { LOGGER.info("Warning: an existing RandomAccessInputStream was found, closing it now"); rais.close(); } rais = file.getRandomAccessInputStream(); } /** * Closes the current RandomAccessInputStream to the zip file. * * @throws IOException if an error occurred */ private void closeRead() throws IOException { if(rais!=null) { try { rais.close(); } finally { rais = null; } } } /** * Opens the zip file for random write access. * * @throws IOException if an error occured while opening the zip file for random read access. * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ private void openWrite() throws IOException { if (raos != null) { LOGGER.info("Warning: an existing RandomAccessOutputStream was found, closing it now"); raos.close(); } // create a buffered output stream to improve write performance, as headers are written by small chunks raos = new BufferedRandomOutputStream(file.getRandomAccessOutputStream(), WRITE_BUFFER_SIZE); } /** * Closes the current RandomAccessOutputStream to the zip file. * * @throws IOException if an error occurred */ private void closeWrite() throws IOException { if(raos!=null) { try { raos.close(); } finally { raos = null; } } } /** * Returns the default encoding to use for parsing filenames and comments. This value is not used for Zip entries * that explicitely declare using UTF-8 (in the general purpose bit flag). * *

    By default, this method returns null to indicate that automatic encoding detection is used. * Although it is not 100% accurate, encoding detection is the preferred approach, unless the encoding is known * in advance which is rather uncommon. * *

    Note that this value only affects entries parsing. Written entries are systematically encoded in * UTF-8 and declared as such in the general purpose bit flag so that proper zip unpackers know what * encoding to expect. * * @return the default encoding to use for parsing filenames and comments */ public String getDefaultEncoding() { return defaultEncoding; } /** * Sets the default encoding to use for parsing filenames and comments. This value is not used for Zip entries * that explicitely declare using UTF-8 (in the general purpose bit flag). * *

    By default, the encoding is null to indicate that automatic encoding detection is used. * Although it is not 100% accurate, encoding detection is the preferred approach, unless the encoding is known * in advance which is rather uncommon. * *

    Note that this value only affects entries parsing. Written entries are systematically encoded in * UTF-8 and declared as such in the general purpose bit flag so that proper zip unpackers know what * encoding to expect. * * @param defaultEncoding the default encoding to use for parsing filenames and comments */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** * Returns all entries as an Iterator of {@link ZipEntry} instances. * * @return Returns all entries as an Iterator of ZipEntry instances. */ public Iterator getEntries() { return entries.iterator(); } /** * Returns the number of entries contained by this Zip file. * * @return the number of entries contained by this Zip file */ public int getNbEntries() { return entries.size(); } /** * Returns a named entry or null if no entry by that name exists. * * @param name name of the entry. * @return the ZipEntry corresponding to the given name or null if not present. */ public ZipEntry getEntry(String name) { return nameMap.get(name); } /** * Returns an InputStream for reading the contents of the given entry. * * @param ze the entry to get the stream for. * @return a stream to read the entry from. * @throws IOException if unable to create an input stream from the zipentry * @throws ZipException if the zipentry has an unsupported compression method * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ public InputStream getInputStream(ZipEntry ze) throws IOException, ZipException, UnsupportedFileOperationException { ZipEntryInfo entryInfo = ze.getEntryInfo(); if (entryInfo == null) throw new ZipException("Unknown entry: "+ze.getName()); openRead(); RandomAccessInputStream entryIn = this.rais; // If data offset is -1 (not calculated yet), calculate it now if (entryInfo.dataOffset == -1) calculateDataOffset(entryInfo); this.rais = null; long start = entryInfo.dataOffset; BoundedInputStream bis = new BoundedInputStream(entryIn, start, ze.getCompressedSize()); switch (ze.getMethod()) { case ZipConstants.STORED: return bis; case ZipConstants.DEFLATED: bis.addDummy(); return new InflaterInputStream(bis, new Inflater(true)); default: throw new ZipException("Found unsupported compression method " + ze.getMethod()); } } /** * Deletes the given entry from this zip file. For performance reasons, this method removes the central file * header and zero out the local file header and data so that the entry cannot be retrieved, but it does * not reclaim the freed space and produces fragmentation. In other words, the resulting zip file will not be * smaller after the entry has been deleted and will contain an area of unused space. The {@link #defragment} method * can be called to reclaim the free space. * *

    There is one case where this method reclaims the free space: when the specified entry is the last one in the * zip file. In this case, the resulting zip file will be smaller. * *

    Note that 'fragmented' zip files are perfectly valid zip files, any zip parser should be able to cope with * such files. * *

    The underlying {@link AbstractFile} must have random write access. If not, an IOException will be * thrown. * * @param ze the ZipEntry to delete * @throws IOException if an I/O error occurred * @throws ZipException if the specified ZipEntry cannot be found in this zip file * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ public void deleteEntry(ZipEntry ze) throws IOException, ZipException, UnsupportedFileOperationException { openRead(); openWrite(); try { ZipEntryInfo entryInfo = ze.getEntryInfo(); if (entryInfo == null) { // Fail silently if the entry is a directory as specific directory entries do not always exist // in zip files. if(ze.isDirectory()) return; throw new ZipException("Unknown entry: "+ze.getName()); } // Strip out central file header of deleted entry int entryIndex = entries.indexOf(ze); int nbEntries = entries.size(); long cdStartOffset; long cdEndOffset; if(nbEntries==1) { // Special case if the deleted entry is the only one, the zip file will become empty. // Note: empty zip files must have the central directory start at offset 0. cdStartOffset = 0; cdEndOffset = 0; raos.seek(0); } else { cdStartOffset = entries.elementAt(0).getEntryInfo().centralHeaderOffset; long shift; if(entryIndex==nbEntries-1) { // Lucky case! If the entry to delete is the last one, we can easily/quickly reclaim the space used // by this entry by moving the central directory (minus the last header corresponding to the deleted // entry) to where the entry's local header started. // The entry before the deleted one, will become the last one ZipEntryInfo lastEntryInfo = entries.elementAt(nbEntries-2).getEntryInfo(); // Destination offset long newCdStartOffset = entryInfo.headerOffset; cdEndOffset = lastEntryInfo.centralHeaderOffset + lastEntryInfo.centralHeaderLen; long cdLength = cdEndOffset-cdStartOffset; // Copy the central directory StreamUtils.copyChunk(rais, raos, cdStartOffset, newCdStartOffset, cdLength); // Update central directory header offsets shift = cdStartOffset-newCdStartOffset; for(int i=0; iOutputStream that allows to write * the contents of the entry. The returned OutputStream must always be closed for the zip file to be * properly modified. Not doing will leave this zip file in a inconsistent, corrupted state. * *

    The underlying {@link AbstractFile} must have random write access. If not, an IOException will be * thrown. * * @param entry the entry to add to this zip file * @return an OutputStream to write the contents of the entry * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. * or is not implemented. */ public OutputStream addEntry(final ZipEntry entry) throws IOException, UnsupportedFileOperationException { try { // Open the zip file for random read and write access openRead(); openWrite(); // Write the new entry's local file header right before the central directory start positionAtCentralDirectory(); long centralDirectoryStart = rais.getOffset(); raos.seek(centralDirectoryStart); final ZipEntryInfo entryInfo = new ZipEntryInfo(); entryInfo.encoding = UTF_8; // Always use UTF-8 for new entries entryInfo.headerOffset = centralDirectoryStart; entryInfo.dataOffset = entryInfo.headerOffset + ZipOutputStream.writeLocalFileHeader(entry, raos, entryInfo.encoding, false, zipBuffer); // Add the new entry to the internal lists entry.setEntryInfo(entryInfo); entries.add(entry); nameMap.put(entry.getName(), entry); // create the ZipEntryOutputStream to write the entry's contents // Use BufferPool to avoid excessive memory allocation and garbage collection. final byte[] deflaterBuf = BufferPool.getByteArray(DEFAULT_DEFLATER_BUFFER_SIZE); ZipEntryOutputStream zeos = new DeflatedOutputStream(raos, new Deflater(DEFAULT_DEFLATER_COMPRESSION, true), deflaterBuf) { // Post-data file info and central directory get written when the stream is closed @Override public void close() throws IOException { // Write data info in the local file header ZipOutputStream.finalizeEntryData(entry, this, raos, false, zipBuffer); // Write the central directory that was squashed by the new entry (at least partially) ZipEntry tempZe; ZipEntryInfo tempEntryInfo; int nbEntries = entries.size(); long cdLength = 0; // Length of central directory long cdOffset = raos.getOffset(); // Offset of central directory for(int i=0; iThe underlying {@link AbstractFile} must have random write access. If not, an IOException will be * thrown. * * @param entry the entry to update * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ public void updateEntry(ZipEntry entry) throws IOException { try { // Open the zip file for write openWrite(); ZipEntryInfo entryInfo = entry.getEntryInfo(); /* Local file header */ // Update time and date raos.seek(entryInfo.headerOffset+10); raos.write(ZipLong.getBytes(entry.getDosTime(), zipBuffer.longBuffer)); // Note: external attributes are not present in the local file header /* Central file header */ // Update 'Version made by', platform might have changed if the Zip didn't contain Unix permissions raos.seek(entryInfo.centralHeaderOffset+4); ZipOutputStream.writeVersionMadeBy(entry, raos, zipBuffer); // Update time and date raos.seek(entryInfo.centralHeaderOffset+12); raos.write(ZipLong.getBytes(entry.getDosTime(), zipBuffer.longBuffer)); // Update 'external attributes' for permissions raos.seek(entryInfo.centralHeaderOffset+38); raos.write(ZipLong.getBytes(entry.getExternalAttributes(), zipBuffer.longBuffer)); } finally { closeWrite(); } } /** * Removes free space fragments from this zip file, thus reducing the size of the zip file. If this zip file does * not contain any free space fragments, the zip file is not modified. * *

    Fragmentation occurs when deleting entries with {@link #deleteEntry(ZipEntry)}. When deleting several entries, * this method should be called once after all entries have deleted. * *

    The underlying {@link AbstractFile} must have random write access. If not, an IOException will be * thrown. * * @throws IOException if an I/O error occurred * @throws UnsupportedFileOperationException if a required operation is not supported by the underlying filesystem. */ public void defragment() throws IOException { int nbEntries = entries.size(); if(nbEntries==0) return; try { openRead(); openWrite(); ZipEntry currentEntry, previousEntry; ZipEntryInfo currentEntryInfo, previousEntryInfo; long shift = 0; // Special case for the first entry currentEntry = entries.elementAt(0); currentEntryInfo = currentEntry.getEntryInfo(); // If data offset is -1 (not calculated yet), calculate it now if (currentEntryInfo.dataOffset == -1) calculateDataOffset(currentEntryInfo); if(currentEntryInfo.headerOffset>0) { StreamUtils.copyChunk(rais, raos, currentEntryInfo.headerOffset, 0, (currentEntryInfo.dataOffset- currentEntryInfo.headerOffset)+currentEntry.getCompressedSize()); shift = currentEntryInfo.headerOffset; currentEntryInfo.headerOffset = 0; currentEntryInfo.dataOffset -= shift; } previousEntry = currentEntry; previousEntryInfo = currentEntryInfo; // Process all other entries for(int i=1; iThe ZipEntrys will know all data that can be obtained from * the central directory alone, but not the data that requires the * local file header or additional data to be read. * * @throws IOException if an I/O error occurred * @throws ZipException if this file is not a valid Zip file */ private void parseCentralDirectory() throws IOException { positionAtCentralDirectory(); byte[] cfh = new byte[CFH_LEN]; byte[] signatureBytes = new byte[4]; rais.readFully(signatureBytes); long sig = ZipLong.getValue(signatureBytes); final long cfhSig = ZipLong.getValue(CFH_SIG); boolean defaultEncodingSet = defaultEncoding!=null; ByteArrayOutputStream encodingAccumulator = defaultEncodingSet?null:new ByteArrayOutputStream(); while (sig == cfhSig) { ZipEntryInfo entryInfo = new ZipEntryInfo(); // Set Central directory file header offset entryInfo.centralHeaderOffset = rais.getOffset() - 4; // 4 for the header signature rais.readFully(cfh); ZipEntry ze = new ZipEntry(); int versionMadeBy = ZipShort.getValue(cfh, 0); // off += 2; ze.setPlatform((versionMadeBy >> 8) & 0x0F); // skip version info // off += 2; int gp = ZipShort.getValue(cfh, 4); // General purpose bit flag boolean isUTF8 = (gp&0x800)!=0; // Tests if bit 11 is set, signaling UTF-8 is used for filename and comment if(isUTF8) { entryInfo.encoding = UTF_8; LOGGER.debug("Entry declared as UTF-8"); } else if(defaultEncodingSet) { entryInfo.encoding = defaultEncoding; LOGGER.debug("Using default encoding: "+defaultEncoding); } else { // FileLogger.finest("Encoding will be detected later"); } entryInfo.hasDataDescriptor = (gp&8)!=0; // off += 2; int method = ZipShort.getValue(cfh, 6); // Note: ZipEntry#setMethod(int) will throw a java.lang.InternalError ("invalid compression method") if the // method is different from DEFLATED or STORED (happens with IMPLODED for example). // Thus we check the method ourselves to fail gracefully. if(method!=DEFLATED && method!=STORED) throw new ZipException("Unsupported compression method"); ze.setMethod(method); // off += 2; ze.setDosTime(ZipLong.getValue(cfh, 8)); // off += 4; ze.setCrc(ZipLong.getValue(cfh, 12)); // off += 4; ze.setCompressedSize(ZipLong.getValue(cfh, 16)); // off += 4; ze.setSize(ZipLong.getValue(cfh, 20)); // off += 4; int fileNameLen = ZipShort.getValue(cfh, 24); // off += 2; int extraLen = ZipShort.getValue(cfh, 26); // off += 2; int commentLen = ZipShort.getValue(cfh, 28); // off += 2; // skip disk number // off += 2; ze.setInternalAttributes(ZipShort.getValue(cfh, 32)); // off += 2; ze.setExternalAttributes(ZipLong.getValue(cfh, 34)); // off += 4; // Read filename bytes byte[] filename = new byte[fileNameLen]; rais.readFully(filename); // If the encoding is known already, set the String now if(entryInfo.encoding!=null) { setFilename(ze, getString(filename, entryInfo.encoding)); } else { // Keep the filename bytes, String will be encoded after entryInfo.filename = filename; // Accumulate those unidentified bytes for encoding detection feedEncodingAccumulator(encodingAccumulator, filename); } // Offset to local file header entryInfo.headerOffset = ZipLong.getValue(cfh, 38); // data offset will be filled later // Read and set extra bytes byte extra[] = new byte[extraLen]; rais.readFully(extra); ze.setExtra(extra); // Read comment bytes byte[] comment = new byte[commentLen]; rais.readFully(comment); // If the encoding is known already, set the String now if(entryInfo.encoding!=null) { ze.setComment(getString(comment, entryInfo.encoding)); } else { // Keep the comment bytes, String will be encoded after entryInfo.comment = comment; // Accumulate those unidentified bytes for encoding detection feedEncodingAccumulator(encodingAccumulator, comment); } entryInfo.centralHeaderLen = 46 + fileNameLen + extraLen + commentLen; // Add the new entry to the internal lists ze.setEntryInfo(entryInfo); entries.add(ze); nameMap.put(ze.getName(), ze); // Swallow signature rais.readFully(signatureBytes); sig = ZipLong.getValue(signatureBytes); } if(encodingAccumulator!=null && encodingAccumulator.size()>0) { int nbEntries = entries.size(); // Note: guessedEncoding may be null if no encoding could be detected. // In that case, the default system encoding will be used to create the string String guessedEncoding = EncodingDetector.detectEncoding(encodingAccumulator.toByteArray()); LOGGER.info("Guessed encoding: "+guessedEncoding); ZipEntry entry; ZipEntryInfo entryInfo; for(int i=0; iThis method detects filenames that use '\' as a path separator and replaces occurrences of '\' to '/'. * Zip specifications make it clear that '/' should always be used, even under FAT platforms, but some packers * (IZArc for instance) don't comply with the specs and use '/' anyway. We handle those paths only if the archive * was created under a FAT platform ; '\' is an allowed character under UNIX platforms: replacing them * would be a bad idea. * * @param ze the ZipEntry object in which to set the filename * @param filename the filename to set */ private static void setFilename(ZipEntry ze, String filename) { if (ze.getPlatform() == ZipEntry.PLATFORM_FAT) { filename = filename.replace('\\', '/'); } ze.setName(filename); } /** * Feeds the given bytes to the encoding accumulator (used for encoding detection). The bytes will be ignored if * the accumulator has enough data already. * * @param encodingAccumulator the ByteArrayOutputStream that holds filename and comment bytes * @param bytes the bytes to feed to the encoding accumulator * @throws IOException if an I/O occurs (should never happen) */ private static void feedEncodingAccumulator(ByteArrayOutputStream encodingAccumulator, byte bytes[]) throws IOException { if(encodingAccumulator.size() < EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE) encodingAccumulator.write(bytes); // Else accumulator has enough bytes, ignore the given bytes } /** Minimum possible size for the End Of Central Directory record (no comment) */ private static final int MIN_EOCD_SIZE = /* end of central dir signature */ 4 /* number of this disk */ + 2 /* number of the disk with the */ /* start of the central directory */ + 2 /* total number of entries in */ /* the central dir on this disk */ + 2 /* total number of entries in */ /* the central dir */ + 2 /* size of the central directory */ + 4 /* offset of start of central */ /* directory with respect to */ /* the starting disk number */ + 4 /* zipfile comment length */ + 2 /* zipfile comment */ + 0; /** Maximum possible size for the End Of Central Directory record (max comment size: 65535) */ private static final int MAX_EOCD_SIZE = /* end of central dir signature */ 4 /* number of this disk */ + 2 /* number of the disk with the */ /* start of the central directory */ + 2 /* total number of entries in */ /* the central dir on this disk */ + 2 /* total number of entries in */ /* the central dir */ + 2 /* size of the central directory */ + 4 /* offset of start of central */ /* directory with respect to */ /* the starting disk number */ + 4 /* zipfile comment length */ + 2 /* zipfile comment */ + 65535; private static final int CFD_LOCATOR_OFFSET = /* end of central dir signature */ 4 /* number of this disk */ + 2 /* number of the disk with the */ /* start of the central directory */ + 2 /* total number of entries in */ /* the central dir on this disk */ + 2 /* total number of entries in */ /* the central dir */ + 2 /* size of the central directory */ + 4; /** * Searches for the end of central dir record, parses * it and positions the stream at the first central directory * record. * * @throws IOException if an I/O error occurs * @throws ZipException if the end of central directory signature could not be found. This can be interpreted as the * underlying file not being a Zip file */ private void positionAtCentralDirectory() throws IOException { long length = rais.getLength(); if(length=0) { if (buf[off] == EOCD_SIG[0]) { if (buf[off+1] == EOCD_SIG[1]) { if (buf[off+2] == EOCD_SIG[2]) { if (buf[off+3] == EOCD_SIG[3]) { signatureFound = true; break; } } } } off--; } if (!signatureFound) { throw new ZipException("Invalid Zip stream (EOCD signature not found)"); } // Parse the offset to the central directory start off += CFD_LOCATOR_OFFSET; byte[] cdStart = new byte[4]; System.arraycopy(buf, off, cdStart, 0, 4); off += 4; // Fetch the global zip file comment byte[] commentLen = new byte[2]; System.arraycopy(buf, off, commentLen, 0, 2); off += 2; // Fetch the global zip file comment byte commentBytes[] = new byte[ZipShort.getValue(commentLen)]; System.arraycopy(buf, off, commentBytes, 0, commentBytes.length); // If no default encoding has been specified, try to guess the comment's encoding. // Note that the Zip format doesn't provide any way of knowing the encoding, not even a bit to indicate UTF-8 // like bit 11 in GPBF. comment = getString(commentBytes, defaultEncoding!=null?defaultEncoding:EncodingDetector.detectEncoding(commentBytes)); // Seek to the start of the central directory rais.seek(ZipLong.getValue(cdStart)); } finally { BufferPool.releaseByteArray(buf); } } /** * Creates and returns a String created using the given bytes and encoding. * If the specified encoding isn't supported, the platform's default encoding will be used. * * @param bytes the byte array to transform * @param encoding the encoding to use to instantiate the String * @return String instance that was created with the given encoding */ private static String getString(byte[] bytes, String encoding) { if(bytes.length==0) return ""; if(encoding!=null) { try { return new String(bytes, encoding); } catch(UnsupportedEncodingException e) { LOGGER.info("Error: unsupported encoding: {}, falling back to default encoding", encoding); } } // Fall back to platform's default encoding return new String(bytes); } /////////////////// // Inner classes // /////////////////// /** * InputStream that delegates requests to the underlying RandomAccessFile, making sure that only bytes from a * certain range can be read. */ private static class BoundedInputStream extends InputStream { private final RandomAccessInputStream rais; private long remaining; private long loc; private boolean addDummyByte = false; BoundedInputStream(RandomAccessInputStream rais, long start, long remaining) { this.rais = rais; this.remaining = remaining; loc = start; } @Override public int read() throws IOException { if (remaining-- <= 0) { if (addDummyByte) { addDummyByte = false; return 0; } return -1; } synchronized (rais) { rais.seek(loc++); return rais.read(); } } @Override public int read(byte[] b, int off, int len) throws IOException { if (remaining <= 0) { if (addDummyByte) { addDummyByte = false; b[off] = 0; return 1; } return -1; } if (len <= 0) { return 0; } if (len > remaining) { len = (int) remaining; } int ret; synchronized (rais) { rais.seek(loc); ret = rais.read(b, off, len); } if (ret > 0) { loc += ret; remaining -= ret; } return ret; } @Override public void close() throws IOException { rais.close(); } /** * Inflater needs an extra dummy byte for nowrap - see Inflater's javadocs. */ void addDummy() { addDummyByte = true; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipLong.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; /** * Utility class that represents a four byte integer with conversion * rules for the big endian byte order of ZIP files. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public final class ZipLong implements Cloneable { private long value; /** * create instance from a number. * @param value the long to store as a ZipLong */ public ZipLong(long value) { this.value = value; } /** * create instance from bytes. * @param bytes the bytes to store as a ZipLong */ public ZipLong (byte[] bytes) { this(bytes, 0); } /** * create instance from the four bytes starting at offset. * @param bytes the bytes to store as a ZipLong * @param offset the offset to start */ public ZipLong (byte[] bytes, int offset) { value = ZipLong.getValue(bytes, offset); } /** * Get value as four bytes in big endian byte order. * @return value as four bytes in big endian order */ public byte[] getBytes() { return ZipLong.getBytes(value); } /** * Get value as Java long. * @return value as a long */ public long getValue() { return value; } /** * Converts the given int value as four bytes in big endian byte order. * @param value the unsigned int value (stored as a long) to convert * @return the converted value as a byte array in big endian byte order */ public static byte[] getBytes(long value) { return getBytes(value, new byte[4], 0); } /** * Converts the given int value as four bytes in big endian byte order. The specified byte array is used to store * the result, starting at offset 0. The returned byte array is the same as the given one. * @param value the unsigned int value (stored as a long) to convert * @param result the byte array in which to store the value in big endian byte order * @return the converted value as a byte array in big endian byte order */ public static byte[] getBytes(long value, byte[] result) { return getBytes(value, result, 0); } /** * Converts the given int value as four bytes in big endian byte order. The specified byte array is used to store * the result, starting at the given offset. The returned byte array is the same as the given one. * @param value the unsigned int value (stored as a long) to convert * @param result the byte array in which to store the value in big endian byte order * @param off offset at which to start writing the result in the array * @return the converted value as a byte array in big endian byte order */ public static byte[] getBytes(long value, byte[] result, int off) { result[off] = (byte) ((value & 0xFF)); result[++off] = (byte) ((value & 0xFF00) >> 8); result[++off] = (byte) ((value & 0xFF0000) >> 16); result[off+1] = (byte) ((value & 0xFF000000L) >> 24); return result; } /** * Helper method to get the value as a Java long from four bytes starting at given array offset * @param bytes the array of bytes * @param offset the offset to start * @return the corresponding Java long value */ public static long getValue(byte[] bytes, int offset) { long value = (bytes[offset + 3] << 24) & 0xFF000000L; value += (bytes[offset + 2] << 16) & 0xFF0000; value += (bytes[offset + 1] << 8) & 0xFF00; value += (bytes[offset] & 0xFF); return value; } /** * Helper method to get the value as a Java long from a four-byte array * @param bytes the array of bytes * @return the corresponding Java long value */ public static long getValue(byte[] bytes) { return getValue(bytes, 0); } /** * Override to make two instances with same value equal. * @param o an object to compare * @return true if the objects are equal */ public boolean equals(Object o) { if (o == null || !(o instanceof ZipLong)) { return false; } return value == ((ZipLong) o).getValue(); } /** * Override to make two instances with same value equal. * @return the value stored in the ZipLong */ public int hashCode() { return (int) value; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipOutputStream.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.RandomAccessOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Vector; import java.util.zip.Deflater; import java.util.zip.ZipException; /** * Reimplementation of {@link java.util.zip.ZipOutputStream java.util.zip.ZipOutputStream} that handles the extended * functionality of this package, especially internal/external file attributes and extra fields with different layouts * for local file data and central directory entries. * *

    --------------------------------------------------------------------------------------------------------------
    *
    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public class ZipOutputStream extends OutputStream implements ZipConstants { /** Current entry */ private ZipEntry entry; /** Current ZipEntryOutputStream corresponding to the entry being written */ private ZipEntryOutputStream zeos; /** Additional info about current entry */ private ZipEntryInfo entryInfo; /** The global zip file comment */ private String comment = ""; /** Compression level for zip entries */ private int level = DEFAULT_DEFLATER_COMPRESSION; /** Compression method zip entries */ private int method = DEFLATED; /** Deflater instance that is used to compress DEFLATED entries */ protected Deflater deflater = new Deflater(level, true); /** Buffer used by Deflater to deflate data */ protected byte[] deflaterBuf; /** List of zip entries written so far */ private Vector entries; /** Count the bytes written to out */ private long written = 0; /** The encoding to use for filenames and the file comment, UTF-8 by default */ private String encoding = UTF_8; /** Holds byte buffer instance used to convert short and longs, avoids creating lots of small arrays */ private ZipBuffer zipBuffer = new ZipBuffer(); /** 0 (zero) as ZipShort */ private static final byte[] SHORT_0 = ZipShort.getBytes(0); /** 0 (zero) as ZipLong */ private static final byte[] LONG_0 = ZipLong.getBytes(0); /** Three ZipLong zeros */ private static final byte[] LONG_TRIPLE_0 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; /** 8 as ZipShort */ private static final byte[] SHORT_8 = ZipShort.getBytes(8); /** 10 as ZipShort */ private static final byte[] SHORT_10 = ZipShort.getBytes(10); /** 20 as ZipShort */ private static final byte[] SHORT_20 = ZipShort.getBytes(20); /** 2048 as ZipShort */ private static final byte[] SHORT_2048 = ZipShort.getBytes(2048); /** 2056 as ZipShort */ private static final byte[] SHORT_2056 = ZipShort.getBytes(2056); /** * The underlying stream this ZipOutputStream writes zip-compressed data to. */ protected OutputStream out; /** * Is the underlying stream a RandomAccessOutputStream? Avoids excessive instanceof comparisons. */ private boolean hasRandomAccess; /** * Creates a new ZipOutputStream that writes Zip-compressed data to the given OutputStream. * If a {@link RandomAccessOutputStream} is supplied, the Zip entries will be written without data descriptor, * which will yield a slightly smaller file. * * @param out the underlying OutputStream stream where compressed data is written to */ public ZipOutputStream(OutputStream out) { this.out = out; this.hasRandomAccess = out instanceof RandomAccessOutputStream; // Use BufferPool to avoid excessive memory allocation and garbage collection. deflaterBuf = BufferPool.getByteArray(DEFAULT_DEFLATER_BUFFER_SIZE); entries = new Vector<>(); } /** * This method indicates whether this archive is writing to a {@link RandomAccessOutputStream}. * * @return true if seekable */ public boolean isSeekable() { return hasRandomAccess; } /** * The encoding to use for filenames and the file comment. * *

    For a list of possible values see http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html. * Defaults to the platform's default character encoding. * * @param encoding the encoding value */ public void setEncoding(String encoding) { this.encoding = encoding; } /** * The encoding to use for filenames and the file comment. * * @return null if using the platform's default character encoding. */ public String getEncoding() { return encoding; } /** * Returns true if the given encoding is either "UTF-8", "UTF8" (case-insensitive) or * null. * * @param encoding the encoding String to test * @return true if the given encoding is either "UTF-8", "UTF8" or null */ private static boolean isUTF8(String encoding) { return encoding==null || encoding.equalsIgnoreCase("UTF-8") || encoding.equalsIgnoreCase("UTF8"); } /** * Finishs writing the contents and closes this as well as the * underlying stream. * * @throws IOException on error */ public void finish() throws IOException { closeEntry(); long cdOffset = written; int nbEntries = entries.size(); ZipEntry ze; for (int i=0; i The size and CRC information is written to the given OutputStream, either as a data descriptor or * in the entry's local file header, and is set in the given {@link ZipEntry} instance. * * @param entry the entry * @param zeos the Zip entry's output stream * @param out the * @param useDataDescriptor if true, a data descriptor will be written to out. If false, size and CRC information * will be written in the local file header (requires out to be a RandomAccessOutputStream). * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @throws IOException if an I/O error occurred */ protected static void finalizeEntryData(ZipEntry entry, ZipEntryOutputStream zeos, OutputStream out, boolean useDataDescriptor, ZipBuffer zipBuffer) throws IOException { long crc = zeos.getCrc(); if (entry.getMethod() == DEFLATED) { ((DeflatedOutputStream)zeos).finishDeflate(); entry.setSize(adjustToLong(zeos.getTotalIn())); long compressedSize = adjustToLong(zeos.getTotalOut()); entry.setCompressedSize(compressedSize); entry.setCrc(crc); } else { // Method is STORED long size = zeos.getTotalOut(); entry.setSize(size); entry.setCompressedSize(size); entry.setCrc(crc); } // If random access output, write the local file header containing // the correct CRC and compressed/uncompressed sizes if (!useDataDescriptor) { RandomAccessOutputStream raos = (RandomAccessOutputStream)out; long save = raos.getOffset(); raos.seek(entry.getEntryInfo().headerOffset + 14); raos.write(ZipLong.getBytes(entry.getCrc(), zipBuffer.longBuffer)); raos.write(ZipLong.getBytes(entry.getCompressedSize(), zipBuffer.longBuffer)); raos.write(ZipLong.getBytes(entry.getSize(), zipBuffer.longBuffer)); raos.seek(save); } } /** * Start writing the given entry. The entry is written by calling the write() of this class. * When the entry has finished being written, {@link #closeEntry()} must be called. * * @param ze the entry to write * @throws IOException on error */ public void putNextEntry(ZipEntry ze) throws IOException { closeEntry(); entry = ze; entryInfo = new ZipEntryInfo(); entry.setEntryInfo(entryInfo); entries.addElement(entry); int entryMethod = entry.getMethod(); if (entryMethod == -1) { // method not specified in the entry, use the one set in this ZipOutputStream entryMethod = method; entry.setMethod(method); } if (entry.getTime() == -1) { // date not specified in the entry, set it to now entry.setTime(System.currentTimeMillis()); } if(entryMethod == DEFLATED) { deflater.reset(); deflater.setLevel(level); zeos = new DeflatedOutputStream(out, deflater, deflaterBuf); } else { zeos = new StoredOutputStream(out); } entryInfo.headerOffset = written; written += writeLocalFileHeader(entry, out, encoding, !hasRandomAccess, zipBuffer); entryInfo.dataOffset = written; } /** * Sets the file comment. * * @param comment the comment */ public void setComment(String comment) { this.comment = comment; } /** * Sets the compression level for subsequent entries. * *

    Default is Deflater.DEFAULT_COMPRESSION. * * @param level the compression level. * @throws IllegalArgumentException if an invalid compression level is specified. */ public void setLevel(int level) { if (level < Deflater.DEFAULT_COMPRESSION || level > Deflater.BEST_COMPRESSION) { throw new IllegalArgumentException( "Invalid compression level: " + level); } this.level = level; } /** * Sets the default compression method for subsequent entries. * *

    Default is DEFLATED. * * @param method an int from java.util.zip.ZipEntry */ public void setMethod(int method) { this.method = method; } /** * Writes the local file header entry. * * @param ze the entry to write * @param out the OutputStream to write the header to * @param encoding the encoding to use for writing the entry's filename. If UTF-8 is used, the general purpose bit * flag will be set accordingly. * @param useDataDescriptor indicates whether a data descriptor will follow the file entry's data. The general * purpose bit flag will be set accordingly. * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @return the size (number of bytes) of the written local file header * @throws IOException if an I/O error occurred */ protected static long writeLocalFileHeader(ZipEntry ze, OutputStream out, String encoding, boolean useDataDescriptor, ZipBuffer zipBuffer) throws IOException { out.write(LFH_SIG); // written += 4; int zipMethod = ze.getMethod(); // version needed to extract // general purpose bit flag writeVersionAndGPBF(out, encoding, useDataDescriptor); // nbWritten += 4; // compression method out.write(ZipShort.getBytes(zipMethod, zipBuffer.shortBuffer)); // written += 2; // last mod. time and date out.write(ZipLong.getBytes(ze.getDosTime(), zipBuffer.longBuffer)); // written += 4; // CRC // compressed length // uncompressed length // this information is not known at this stage so it will be set after the data has been written, // either in the data descriptor (if used), or here by seeking (requires random access) out.write(LONG_TRIPLE_0); // 12 zero bytes // written += 12; // file name length byte[] name = getBytes(ze.getName(), encoding); out.write(ZipShort.getBytes(name.length, zipBuffer.shortBuffer)); // written += 2; // extra field length byte[] extra = ze.getLocalFileDataExtra(); out.write(ZipShort.getBytes(extra.length, zipBuffer.shortBuffer)); // written += 2; // Number of bytes written by this method so far long written = 30; // file name out.write(name); written += name.length; // extra field out.write(extra); written += extra.length; return written; } /** * Writes the data descriptor, using the CRC, compressed and uncompressed size attributes contained in the * given ZipEntry. * The length of the field is returned, it is always 16 bytes. * * @param ze the entry for which to write the data descriptor * @param out the OutputStream where to write the data descriptor to * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @return the number of bytes that were written, i.e. the size of the data descriptor (16 bytes) * @throws IOException if an I/O error occurred */ protected static long writeDataDescriptor(ZipEntry ze, OutputStream out, ZipBuffer zipBuffer) throws IOException { out.write(DD_SIG); out.write(ZipLong.getBytes(ze.getCrc(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getCompressedSize(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getSize(), zipBuffer.longBuffer)); return 16; } /** * Writes central file header's 'Version made by' field, using the platform contained in the given ZipEntry. * The length of the field is returned, it is always 2 bytes. * * @param ze the entry for which to write the 'Version made by' field * @param out the OutputStream where to write the field * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @return the number of bytes that were written, i.e. the size of the 'Version made by' field (2 bytes) * @throws IOException if an I/O error occurred */ protected static long writeVersionMadeBy(ZipEntry ze, OutputStream out, ZipBuffer zipBuffer) throws IOException { out.write(ZipShort.getBytes((ze.getPlatform() << 8) | 20, zipBuffer.shortBuffer)); return 2; } /** * Writes the central file header for the given entry. * * @param ze the entry for which to write the central file header * @param out the OutputStream to write the central file header to * @param encoding the encoding to use for writing the filename and optional comment * @param localFileHeaderOffset the offset to the local file header start * @param useDataDescriptor true if a data descriptor is used for the entry * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @throws IOException if an I/O error occurred * @return the number of bytes that were written, i.e. the size of the central file header */ protected static long writeCentralFileHeader(ZipEntry ze, OutputStream out, String encoding, long localFileHeaderOffset, boolean useDataDescriptor, ZipBuffer zipBuffer) throws IOException { out.write(CFH_SIG); // nbWritten += 4; // version made by writeVersionMadeBy(ze, out, zipBuffer); // nbWritten += 2; // version needed to extract // general purpose bit flag writeVersionAndGPBF(out, encoding, useDataDescriptor); // nbWritten += 4; // compression method out.write(ZipShort.getBytes(ze.getMethod(), zipBuffer.shortBuffer)); // nbWritten += 2; // last mod. time and date out.write(ZipLong.getBytes(ze.getDosTime(), zipBuffer.longBuffer)); // nbWritten += 4; // CRC // compressed length // uncompressed length out.write(ZipLong.getBytes(ze.getCrc(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getCompressedSize(), zipBuffer.longBuffer)); out.write(ZipLong.getBytes(ze.getSize(), zipBuffer.longBuffer)); // nbWritten += 12; // file name length byte[] name = getBytes(ze.getName(), encoding); out.write(ZipShort.getBytes(name.length, zipBuffer.shortBuffer)); // nbWritten += 2; // extra field length byte[] extra = ze.getCentralDirectoryExtra(); out.write(ZipShort.getBytes(extra.length, zipBuffer.shortBuffer)); // nbWritten += 2; // file comment length String comm = ze.getComment(); if (comm == null) { comm = ""; } byte[] commentB = getBytes(comm, encoding); out.write(ZipShort.getBytes(commentB.length, zipBuffer.shortBuffer)); // nbWritten += 2; // disk number start out.write(SHORT_0); // nbWritten += 2; // internal file attributes out.write(ZipShort.getBytes(ze.getInternalAttributes(), zipBuffer.shortBuffer)); // nbWritten += 2; // external file attributes out.write(ZipLong.getBytes(ze.getExternalAttributes(), zipBuffer.longBuffer)); // nbWritten += 4; // relative offset of LFH out.write(ZipLong.getBytes(localFileHeaderOffset, zipBuffer.longBuffer)); // nbWritten += 4; long nbWritten = 46; // file name out.write(name); nbWritten += name.length; // extra field out.write(extra); nbWritten += extra.length; // file comment out.write(commentB); nbWritten += commentB.length; return nbWritten; } /** * Writes the 'version needed to extract' (2 bytes) and 'general purpose bit flag' (2 bytes) fields. * * @param out the OutputStream to write the fields to * @param encoding the encoding used for writing the filename and optional comment * @param useDataDescriptor true if a data descriptor is used for the entry * @return the number of bytes that were written, i.e. 4 * @throws IOException if an I/O error occurred */ protected static long writeVersionAndGPBF(OutputStream out, String encoding, boolean useDataDescriptor) throws IOException { boolean isUTF8 = isUTF8(encoding); // General purpose bit flag : // Bit 11 signals UTF-8 is used // Bit 3 signals a data descriptor is used if (useDataDescriptor) { // requires version 2 as we are going to store length info in the data descriptor out.write(SHORT_20); // General purpose bit flag out.write(isUTF8? SHORT_2056 // Bit 3 | Bit 11 = 2056 :SHORT_8 // Bit 3 = 8 ); } else { // Version out.write(SHORT_10); // General purpose bit flag out.write(isUTF8? SHORT_2048 // Bit 11 = 2048 :SHORT_0 // No bit set ); } return 4; } /** * Writes the end of the central directory record. * * @param out the OutputStream to write the end of the central directory record to * @param nbEntries number of entries the Zip file contains * @param cdLength length (in bytes) of the central directory record * @param cdOffset offset from the beginning of the Zip file to the start of the central directory record * @param comment the optional Zip file comment * @param encoding the encoding to use for writing the optional Zip comment * @param zipBuffer a ZipBuffer instance used to convert integer values to Zip variants * @throws IOException if an I/O error occurred */ protected static void writeCentralDirectoryEnd(OutputStream out, int nbEntries, long cdLength, long cdOffset, String comment, String encoding, ZipBuffer zipBuffer) throws IOException { out.write(EOCD_SIG); // disk numbers out.write(LONG_0); // 2x SHORT_0 // number of entries ZipShort.getBytes(nbEntries, zipBuffer.shortBuffer); out.write(zipBuffer.shortBuffer); out.write(zipBuffer.shortBuffer); // length and location of CD out.write(ZipLong.getBytes(cdLength, zipBuffer.longBuffer)); out.write(ZipLong.getBytes(cdOffset, zipBuffer.longBuffer)); // ZIP file comment byte[] data = getBytes(comment, encoding); out.write(ZipShort.getBytes(data.length, zipBuffer.shortBuffer)); out.write(data); } /** * Retrieve the bytes for the given String in the encoding set for * this Stream. * @param name the string to get bytes from * @param encoding the encoding the string is encoded with * @return the bytes as a byte array * @throws ZipException on error */ protected static byte[] getBytes(String name, String encoding) throws ZipException { if (encoding == null) { return name.getBytes(); } else { try { return name.getBytes(encoding); } catch (UnsupportedEncodingException uee) { throw new ZipException(uee.getMessage()); } } } /** * Returns a long that is the unsigned intepretation of the given (signed) int. * * @param i the value to treat as unsigned int * @return the unsigned int as a long */ protected static long adjustToLong(int i) { return i & 0xFFFFFFFFl; } ///////////////////////////////// // OutputStream implementation // ///////////////////////////////// /** * Writes the given bytes to the current Zip entry opened with {@link #putNextEntry(ZipEntry)}, using the entry's * compression method. If no entry is currently open, the bytes will be written as-is to the underlying * OutputStream. * * @param b the byte array to write * @param offset the start position to write from * @param length the number of bytes to write * @throws IOException on error */ @Override public void write(byte[] b, int offset, int length) throws IOException { (zeos==null?out:zeos).write(b, offset, length); } /** * Writes the given bytes to the current Zip entry opened with {@link #putNextEntry(ZipEntry)}, using the entry's * compression method. If no entry is currently open, the bytes will be written as-is to the underlying * OutputStream. * * @param b the byte array to write * @throws IOException on error */ @Override public void write(byte[] b) throws IOException { (zeos==null?out:zeos).write(b, 0, b.length); } /** * Writes a single byte to the current Zip entry opened with {@link #putNextEntry(ZipEntry)}, using the entry's * compression method. If no entry is currently open, the bytes will be written as-is to the underlying * OutputStream. * *

    Delegates to the three arg method. * @param b the byte to write * @throws IOException on error */ @Override public void write(int b) throws IOException { (zeos==null?out:zeos).write(b); } /** * Closes this output stream and releases any system resources associated with the stream. * * @exception IOException if an I/O error occurs. */ @Override public void close() throws IOException { finish(); if(deflaterBuf !=null) { // Only if close() has not already been called already BufferPool.releaseByteArray(deflaterBuf); deflaterBuf = null; } out.close(); } /** * Flushes this output stream and forces any buffered output bytes to be written out to the stream. * * @exception IOException if an I/O error occurs. */ @Override public void flush() throws IOException { out.flush(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/impl/zip/provider/ZipShort.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.impl.zip.provider; /** * Utility class that represents a two byte integer with conversion * rules for the big endian byte order of ZIP files. * *

    * This class is based off the org.apache.tools.zip package of the Apache Ant project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant. * * @author Apache Ant, Maxence Bernard */ public final class ZipShort implements Cloneable { private int value; /** * create instance from a number. * @param value the int to store as a ZipShort */ public ZipShort (int value) { this.value = value; } /** * create instance from bytes. * @param bytes the bytes to store as a ZipShort */ public ZipShort (byte[] bytes) { this(bytes, 0); } /** * create instance from the two bytes starting at offset. * @param bytes the bytes to store as a ZipShort * @param offset the offset to start */ public ZipShort (byte[] bytes, int offset) { value = ZipShort.getValue(bytes, offset); } /** * Get value as two bytes in big endian byte order. * @return the value as a a two byte array in big endian byte order */ public byte[] getBytes() { byte[] result = new byte[2]; result[0] = (byte) (value & 0xFF); result[1] = (byte) ((value & 0xFF00) >> 8); return result; } /** * Get value as Java int. * @return value as a Java int */ public int getValue() { return value; } /** * Converts the given short value as two bytes in big endian byte order. * @param value the short value (stored as an int) to convert * @return the converted value as a byte array in big endian byte order */ public static byte[] getBytes(int value) { return getBytes(value, new byte[2], 0); } /** * Converts the given short value as two bytes in big endian byte order. The specified byte array is used to store * the result, starting at offset 0. The returned byte array is the same as the given one. * @param value the short value (stored as an int) to convert * @param result the byte array in which to store the value in big endian byte order * @return the converted value as a byte array in big endian byte order */ public static byte[] getBytes(int value, byte[] result) { return getBytes(value, result, 0); } /** * Converts the given short value as two bytes in big endian byte order. The specified byte array is used to store * the result, starting at the given offset. The returned byte array is the same as the given one. * @param value the short value (stored as an int) to convert * @param result the byte array in which to store the value in big endian byte order * @param off offset at which to start writing the result in the array * @return the converted value as a byte array in big endian byte order */ public static byte[] getBytes(int value, byte[] result, int off) { result[off] = (byte) (value & 0xFF); result[off+1] = (byte) ((value & 0xFF00) >> 8); return result; } /** * Helper method to get the value as a java int from two bytes starting at given array offset * @param bytes the array of bytes * @param offset the offset to start * @return the corresponding java int value */ public static int getValue(byte[] bytes, int offset) { int value = (bytes[offset + 1] << 8) & 0xFF00; value += (bytes[offset] & 0xFF); return value; } /** * Helper method to get the value as a java int from a two-byte array * @param bytes the array of bytes * @return the corresponding java int value */ public static int getValue(byte[] bytes) { return getValue(bytes, 0); } /** * Override to make two instances with same value equal. * @param o an object to compare * @return true if the objects are equal */ public boolean equals(Object o) { if (o == null || !(o instanceof ZipShort)) { return false; } return value == ((ZipShort) o).getValue(); } /** * Override to make two instances with same value equal. * @return the value stored in the ZipShort */ public int hashCode() { return value; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/package.html ================================================ API used to access, read and write files over any file system. ================================================ FILE: src/main/java/com/mucommander/commons/file/util/C.java ================================================ package com.mucommander.commons.file.util; import com.sun.jna.Native; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class provides access to a static instance of the {@link CLibrary} interface, allowing to invoke selected * functions of the C standard library. * *

    The C standard library and the JNA library (which is used to access native libraries) may not be available on * all OS/CPU architectures: {@link #isAvailable()} can be used to determine that at runtime. * * @see CLibrary * @author Maxence Bernard */ public class C { private static final Logger LOGGER = LoggerFactory.getLogger(C.class); /** Singleton instance */ private static CLibrary INSTANCE; static { try { INSTANCE = Native.load("c", CLibrary.class); } catch(Throwable e) { LOGGER.info("Unable to load C library", e); // java.lang.UnsatisfiedLinkError is thrown if the CPU architecture is not supported by JNA. INSTANCE = null; } } /** * Returns true if the C standard library can be accessed on the current OS/CPU architecture. * * @return true if the C standard library can be accessed on the current OS/CPU architecture */ public static boolean isAvailable() { return INSTANCE!=null; } /** * Returns a static instance of the {@link CLibrary} interface, allowing to invoke selected functions of the C * standard library. null will be returned if {@link #isAvailable()} returned false. * * @return a static instance of the {@link CLibrary} interface, null if it is not available on the * current OS/CPU architecture */ public static CLibrary getInstance() { return INSTANCE; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/CLibrary.java ================================================ package com.mucommander.commons.file.util; import com.sun.jna.Library; import com.sun.jna.Structure; import java.util.Arrays; import java.util.List; /** * Exposes parts of the C standard library using JNA (Java Native Access). * * @author Maxence Bernard */ public interface CLibrary extends Library { ////////////////////// // statvfs function // ////////////////////// /** * Structure that holds the information returned by {@link CLibrary#statvfs(String, STATVFSSTRUCT)}. */ class STATVFSSTRUCT extends Structure { /* file system block size */ public int f_bsize; /* fragment size */ public int f_frsize; /* size of fs in f_frsize units */ public int f_blocks; /* # free blocks */ public int f_bfree; /* # free blocks for non-root */ public int f_bavail; /* # inodes */ public int f_files; /* # free inodes */ public int f_ffree; /* # free inodes for non-root */ public int f_favail; /* file system ID */ public long f_fsid; /* mount flags */ public int f_flag; /* maximum filename length */ public int f_namemax; @Override protected List getFieldOrder() { return Arrays.asList("f_bsize", "f_frsize", "f_blocks", "f_bfree", "f_bavail", "f_files", "f_ffree", "f_favail", "f_fsid", "f_flag", "f_namemax"); } } /** * Returns information about the filesystem on which the specified file resides. * * @param path pathname of any file within the mounted filesystem * @param struct a {@link STATVFSSTRUCT} object * @return 0 on success, -1 on error */ int statvfs(String path, STATVFSSTRUCT struct); } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/Chmod.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; /** * This class is a gateway to the chmod UNIX command. It provides static methods that allow to change a * file's permissions, overcoming the limitations of java.io.File. * *

    The chmod command is available only under {@link com.mucommander.commons.runtime.OsFamily#isUnixBased() UNIX-based} * systems -- a call to any of this class' methods under other OS will likely fail. * * @author Maxence Bernard * @see com.mucommander.commons.file.FilePermissions */ public class Chmod { /** * Attemps to change the permissions of the given file and returns true if the chmod * command reported a success. * * @param file the file whose permissions are to be changed * @param permissions the new permissions * @see com.mucommander.commons.file.FilePermissions * @return true if the chmod command reported a success */ public static boolean chmod(AbstractFile file, int permissions) { return chmod(new AbstractFile[]{file}, Integer.toOctalString(permissions)); } /** * Attemps to change the permissions of the given file and returns true if the chmod * command reported a success. * * @param file the file whose permissions are to be changed * @param permissions the new permissions, in any form accepted by the chmod command * @return true if the chmod command reported a success */ public static boolean chmod(AbstractFile file, String permissions) { return chmod(new AbstractFile[]{file}, permissions); } /** * Attemps to change the permissions of the given files and returns true if the chmod * command reported a success. * * @param files the files whose permissions are to be changed * @param permissions the new permissions * @see com.mucommander.commons.file.FilePermissions * @return true if the chmod command reported a success */ public static boolean chmod(AbstractFile files[], int permissions) { return chmod(files, Integer.toOctalString(permissions)); } /** * Attemps to change the permissions of the given files and returns true if the chmod * command reported a success. * * @param files the files whose permissions are to be changed * @param permissions the new permissions, in any form accepted by the chmod command * @return true if the chmod command reported a success */ public static boolean chmod(AbstractFile files[], String permissions) { // create the command token array String[] tokens = new String[files.length+2]; tokens[0] = "chmod"; tokens[1] = permissions; int fileIndex = 0; for(int i=2; iFileChangeListener instances must register themselves with FileMonitor using * {@link FileMonitor#addFileChangeListener(FileChangeListener)}, in order for {@link #fileChanged(AbstractFile, int)} * to be called whenever a file monitored by a FileMonitor has changed. * * @see FileMonitor * @author Maxence Bernard */ public interface FileChangeListener extends FileMonitorConstants { /** * This method is called whenever a change in one or several attributes of the given file has changed. The * changedAttributes parameter may contain several attributes, use the binary AND operator with * {@link FileMonitor} constant attribute fields to read them. * * @param file the AbstractFile for which an attribute change has been detected * @param changedAttributes a set of attributes that have changed, see FileMonitor constant fields for possible values */ void fileChanged(AbstractFile file, int changedAttributes); } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/FileComparator.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.quicksearch.QuickSearch; import java.util.Comparator; import java.util.regex.Pattern; /** * FileComparator compares {@link AbstractFile} instances using a comparison criterion order (ascending or descending). * Directories can either be mixed with regular files (compared just as regular files), or always precede regular files. *

    FileComparator extends Comparator and thus can be used wherever a Comparator is accepted. In particular, it * can be used with java.util.Arrays sort methods to easily sort an array of files. * *

    The following criteria are available: *

      *
    • {@link #NAME_CRITERION}: compares filenames returned by {@link AbstractFile#getName()} *
    • {@link #SIZE_CRITERION}: compares file sizes returned by {@link AbstractFile#getSize()}. Note: size for * directories is always considered as 0, even if {@link AbstractFile#getSize()} returns something else. *
    • {@link #DATE_CRITERION}: compares file dates returned by {@link AbstractFile#getLastModifiedDate()} *
    • {@link #EXTENSION_CRITERION}: compares file extensions returned by {@link AbstractFile#getExtension()} *
    • {@link #PERMISSIONS_CRITERION}: compares file permissions returned by {@link AbstractFile#getPermissions()} *
    * * @author Maxence Bernard */ public class FileComparator implements Comparator { /** Comparison criterion */ private final int criterion; /** Ascending or descending order ? */ private final boolean ascending; /** Specifies whether directories should precede files or be handled as regular files */ private final boolean directoriesFirst; /** Specifies whether directories always alphabetical sorted */ private final boolean foldersAlwaysAlphabetical; /** Criterion for filename comparison. */ public final static int NAME_CRITERION = 0; /** Criterion for file size comparison. */ public final static int SIZE_CRITERION = 1; /** Criterion for file date comparison. */ public final static int DATE_CRITERION = 2; /** Criterion for file extension comparison. */ public final static int EXTENSION_CRITERION = 3; /** Criterion for file permissions comparison. */ public final static int PERMISSIONS_CRITERION = 4; /** Criterion for owner comparison. */ public final static int OWNER_CRITERION = 5; /** Criterion for group comparison. */ public final static int GROUP_CRITERION = 6; /** Matches filenames that contain a number, like "01 - Do the Joy.mp3" */ private final static Pattern FILENAME_WITH_NUMBER_PATTERN = Pattern.compile("\\d+"); private final QuickSearch quickSearch; /** * Creates a new FileComparator using the specified comparison criterion, order (ascending or descending) and * directory handling rule. * * @param criterion comparison criterion, see constant fields * @param ascending if true, ascending order will be used, descending order otherwise * @param directoriesFirst specifies whether directories should precede files or be handled as regular files * @param foldersAlwaysAlphabetical specifies whether directories are sorted always alphabetical if they stay first */ public FileComparator(int criterion, boolean ascending, boolean directoriesFirst, boolean foldersAlwaysAlphabetical, QuickSearch quickSearch) { this.criterion = criterion; this.ascending = ascending; this.directoriesFirst = directoriesFirst; this.foldersAlwaysAlphabetical = foldersAlwaysAlphabetical && directoriesFirst; this.quickSearch = quickSearch; } public FileComparator(int criterion, boolean ascending, boolean directoriesFirst, boolean foldersAlwaysAlphabetical) { this(criterion, ascending, directoriesFirst, foldersAlwaysAlphabetical, null); } /** * Returns a value for the given character. Using this function in a comparator will separator * symbols for digits and letters and put in the following order: *
      *
    • symbols first
    • *
    • digits second
    • *
    • letters third
    • *
    * *

    This character order was suggested in ticket #282. * * @param c character for which to return a value * @return a value for the given character */ private static int getCharacterValue(int c) { // Note: max char value is 65535 if (Character.isLetter(c)) c += 131070; // yields a value higher than any other symbol or digit else if (Character.isDigit(c)) c += 65535; // yields a value higher than any other symbol // else we have a symbol return c; } /** * Removes leading zeros ('0') from the given string (if it contains any), and returns the trimmed string. * * @param s the string from which to remove leading zeros * @return a string without leading zeros */ private static String removeLeadingZeros(String s) { int len = s.length(); int i = 0; while (i < len && s.charAt(i) == '0') { i++; } if (i > 0) { return s.substring(i, len); } return s; } /** * Compare the specified strings, following the contract of {@link Comparator#compare(Object, Object)}. * * @param s1 first string to compare * @param s2 second string to compare. * @param ignoreCase true to perform a case-insensitive string comparison, false to take * the case into account. * @param nullProtection true if any of s1 or s2 can be null, false * if strings cannot be null. * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater * than the second. */ private static int compareStrings(String s1, String s2, boolean ignoreCase, boolean nullProtection) { // Protect against null values, only if requested if (nullProtection) { if (s1 == null && s2 != null) { // s1 is null, s2 isn't return -1; } else if (s1 != null && s2 == null) { // s2 is null, s1 isn't return 1; // At this point, either both strings are null, or none of them are } else { if (s1 == null) // Both strings are null return 0; // else: Both strings are not null, go on with the comparison } } return compareStrings(s1, s2, ignoreCase); } /** * Compare the specified strings, following the contract of {@link Comparator#compare(Object, Object)}. * * @param s1 first string to compare * @param s2 second string to compare. * @param ignoreCase true to perform a case-insensitive string comparison, false to take * the case into account. * @return a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater * than the second. */ private static int compareStrings(String s1, String s2, boolean ignoreCase) { // Special treatment for strings that contain a number, so they are ordered by the number's value, e.g.: // 1 < 1a < 2 < 10, like Mac OS X Finder and Windows Explorer do. // // This special order applies only if both strings contain a number and have the same prefix. Otherwise, the general order applies. int digitIndex1 = firstDigitPos(s1); if (digitIndex1 >= 0) { int digitIndex2 = firstDigitPos(s2); if (digitIndex2 >= 0) { // So we got two filenames that both contain a number, check if they have the same prefix // Note: compare prefixes only if start indexes match, faster that way if (digitIndex1 == digitIndex2 && (digitIndex1==0 || s1.substring(0, digitIndex1).equals(s2.substring(0, digitIndex2)))) { int g1Len = 0; int l1 = s1.length(); for (int i = digitIndex1; i < l1; i++) { char c = s1.charAt(i); if (c >= '0' && c <= '9') { g1Len++; } else { break; } } int g2Len = 0; int l2 = s2.length(); for (int i = digitIndex2; i < l2; i++) { char c = s2.charAt(i); if (c >= '0' && c <= '9') { g2Len++; } else { break; } } if (g1Len != g2Len) { return g1Len - g2Len; } for (int i = 0; i < g1Len && i < g2Len; i++) { int c1 = s1.charAt(i + digitIndex1); int c2 = s2.charAt(i + digitIndex2); if (c1 != c2) { return c1 - c2; } } } } } int n1 = s1.length(); int n2 = s2.length(); for (int i=0; i= '0' && ch <= '9') { return i; } } return -1; } /////////////////////////////// // Comparator implementation // /////////////////////////////// public int compare(AbstractFile f1, AbstractFile f2) { if (quickSearch != null) { boolean m1 = quickSearch.matches(f1); boolean m2 = quickSearch.matches(f2); if (m1 & !m2) { return -1; } else if (m2 && !m1) { return 1; } } boolean is1Directory = f1.isDirectory(); boolean is2Directory = f2.isDirectory(); long diff; if (directoriesFirst) { if (is1Directory && !is2Directory) { return -1; // ascending has no effect on the result (a directory is always first) so let's return } else if (is2Directory && !is1Directory) { return 1; // ascending has no effect on the result (a directory is always first) so let's return } if (foldersAlwaysAlphabetical && is1Directory) { diff = compareStrings(f1.getName(), f2.getName(), true); if (diff == 0) { // Case-sensitive name comparison diff = compareStrings(f1.getName(), f2.getName(), false); } return (int) diff; } // At this point, either both files are directories or none of them are } if (criterion == SIZE_CRITERION) { // Consider that directories have a size of 0 long fileSize1 = is1Directory ? 0 : f1.getSize(); long fileSize2 = is2Directory ? 0 : f2.getSize(); // Returns file1 size - file2 size, file size of -1 (unavailable) is considered as enormous (max long value) diff = (fileSize1 == -1 ? Long.MAX_VALUE : fileSize1) - (fileSize2 == -1 ? Long.MAX_VALUE : fileSize2); } else if (criterion == DATE_CRITERION) { diff = f1.getLastModifiedDate() - f2.getLastModifiedDate(); } else if (criterion == PERMISSIONS_CRITERION) { diff = f1.getPermissions().getIntValue() - f2.getPermissions().getIntValue(); } else if (criterion == EXTENSION_CRITERION) { diff = compareStrings(f1.getExtension(), f2.getExtension(), true, true); } else if (criterion == OWNER_CRITERION) { diff = compareStrings(f1.getOwner(), f2.getOwner(), true, true); } else if (criterion == GROUP_CRITERION) { diff = compareStrings(f1.getGroup(), f2.getGroup(), true, true); } else { // criterion == NAME_CRITERION diff = compareStrings(f1.getName(), f2.getName(), true); if (diff == 0) { // This should never happen unless the current filesystem allows a directory to have // several files with different case variations of the same name. // AFAIK, no OS/filesystem allows this, but just to be safe. // Case-sensitive name comparison diff = compareStrings(f1.getName(), f2.getName(), false); } } if (criterion != NAME_CRITERION && diff==0) // If both files have the same criterion's value, compare names diff = compareStrings(f1.getName(), f2.getName(), true, false); // Cast long value to int, without overflowing the int if the long value exceeds the min or max int value int intValue; if (diff > Integer.MAX_VALUE) { intValue = Integer.MAX_VALUE; // 2147483647 } else if (diff < Integer.MIN_VALUE+1) { // Need that +1 so that the int is not overflowed if ascending order is enabled (i.e. int is negated) intValue = Integer.MIN_VALUE + 1; // 2147483647 } else { intValue = (int) diff; } return ascending ? intValue : -intValue; // Note: ascending is used more often, more efficient to negate for descending } /** * Returns true only if the given object is a FileComparator using the same criterion and ascending/descending order. */ public boolean equals(Object o) { if (! (o instanceof FileComparator fc)) { return false; } return criterion == fc.criterion && ascending == fc.ascending; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/FileMonitor.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.WeakHashMap; /** * FileMonitor allows to monitor a file and detect changes in the file's attributes and notify registered * {@link FileChangeListener} listeners accordingly. * *

    * FileMonitor detects attributes changes by polling the file's attributes at a given frequency and comparing their * values with the previous ones. If any of the monitored attributes has changed, {@link FileChangeListener#fileChanged(AbstractFile, int)} * is called on each of the registered listeners to notify them of the file attributes that have changed. *
    Here's the list of file attributes that can be monitored: *

      *
    • {@link #DATE_ATTRIBUTE} *
    • {@link #SIZE_ATTRIBUTE} *
    • {@link #PERMISSIONS_ATTRIBUTE} *
    • {@link #IS_DIRECTORY_ATTRIBUTE} *
    • {@link #EXISTS_ATTRIBUTE} *
    * *

    The polling frequency is controlled by the poll period. This parameter determines how often the file's attributes * are checked. The lower this period is, the faster changes will be reported to listeners, but also the higher the * impact on I/O and CPU. This parameter should be carefully specified to avoid hogging resources excessively. * *

    Note that FileMonitor uses file attributes polling because the Java API doesn't currently provide any better way * to do detect file changes. If Java ever does provide a callback mechanism for detecting file changes, this class * will be modified to take advantage of it. Another possible improvement would be to add JNI hooks for platform-specific * filesystem events such as 'inotify' (Linux Kernel), 'kqueue' (BSD, Mac OS X), PAM (Solaris), ... * * @see FileChangeListener * @author Maxence Bernard */ public class FileMonitor implements FileMonitorConstants, Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(FileMonitor.class); /** Monitored file */ private AbstractFile file; /** Monitored attributes */ private int attributes; /** Poll period in milliseconds, i.e. the time to elapse between two file attributes polls */ private long pollPeriod; /** The thread that actually does the file attributes polling and event firing */ private Thread monitorThread; /** * True once this monitor is ready to catch file changes, that is when the monitor thread has been started and * initial file attributes have been fetched. */ private boolean isInitialized; /** Registered FileChangeListener instances, stored as weak references */ private WeakHashMap listeners = new WeakHashMap<>(); /** * Creates a new FileMonitor that monitors the given file for changes, using the default attribute set (as defined * by {@link #DEFAULT_ATTRIBUTES}) and default poll period (as defined by {@link #DEFAULT_POLL_PERIOD}). * *

    See the general constructor {@link #FileMonitor(AbstractFile, int, long)} for more information. * * @param file the AbstractFile to monitor for changes */ public FileMonitor(AbstractFile file) { this(file, DEFAULT_ATTRIBUTES, DEFAULT_POLL_PERIOD); } /** * Creates a new FileMonitor that monitors the given file for changes, using the specified attribute set and * default poll period as defined by {@link #DEFAULT_POLL_PERIOD}. * *

    See the general constructor {@link #FileMonitor(AbstractFile, int, long)} for more information. * * @param file the AbstractFile to monitor for changes * @param attributes the set of attributes to monitor, see constant fields for a list of possible attributes */ public FileMonitor(AbstractFile file, int attributes) { this(file, attributes, DEFAULT_POLL_PERIOD); } /** * Creates a new FileMonitor that monitors the given file for changes, using the specified poll period and * default attribute set as defined by {@link #DEFAULT_ATTRIBUTES}). * *

    See the general constructor {@link #FileMonitor(AbstractFile, int, long)} for more information. * * @param file the AbstractFile to monitor for changes * @param pollPeriod number of milliseconds between two file attributes polls */ public FileMonitor(AbstractFile file, long pollPeriod) { this(file, DEFAULT_ATTRIBUTES, pollPeriod); } /** * Creates a new FileMonitor that monitors the given file for changes, using the specified attribute set * and poll period. * *

    Note that monitoring will only start after {@link #startMonitoring()} has been called. * *

    * The following attributes can be monitored: *

      *
    • {@link #DATE_ATTRIBUTE} *
    • {@link #SIZE_ATTRIBUTE} *
    • {@link #PERMISSIONS_ATTRIBUTE} *
    • {@link #IS_DIRECTORY_ATTRIBUTE} *
    • {@link #EXISTS_ATTRIBUTE} *
    * Several attributes can be specified by combining them with the binary OR operator. * *

    * The poll period specified in the constructor determines how often the file's attributes will be checked. * The lower this period is, the faster changes will be reported to registered listeners, but also the higher the * impact on I/O and CPU. *
    Note that the time spent for polling is taken into account for the poll period. For example, if the poll * period is 1000ms, and polling the file's attributes took 50ms, the next poll will happen in 950ms. * * @param file the AbstractFile to monitor for changes * @param attributes the set of attributes to monitor, see constant fields for a list of possible attributes * @param pollPeriod number of milliseconds between two file attributes polls */ FileMonitor(AbstractFile file, int attributes, long pollPeriod) { this.file = file; this.attributes = attributes; this.pollPeriod = pollPeriod; } /** * Adds the given {@link FileChangeListener} instance to the list of registered listeners. * *

    Listeners are stored as weak references so {@link #removeFileChangeListener(FileChangeListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the FileChangeListener to add to the list of registered listeners. * @see #removeFileChangeListener(FileChangeListener) */ void addFileChangeListener(FileChangeListener listener) { listeners.put(listener, null); } /** * Removes the given {@link FileChangeListener} instance to the list of registered listeners. * * @param listener the FileChangeListener to remove from the list of registered listeners. * @see #addFileChangeListener(FileChangeListener) */ private void removeFileChangeListener(FileChangeListener listener) { listeners.remove(listener); } /** * Starts monitoring the monitored file in a dedicated thread. Does nothing if monitoring has already been started * and not stopped yet. Calling this method after {@link #stopMonitoring()} has been called will resume monitoring. *

    Once started, the monitoring thread will check for changes in the monitored file attributes specified in * the constructor, and call registered {@link FileChangeListener} instances whenever a change in one or several * attributes has been detected. The poll period specified in the constructor determines how often the file's * attributes will be checked. * *

    This method waits until the thread is started effectively and the monitor is ready to monitor file changes. * This guarantees that all changes made to the monitored file after this method returns will be caught and properly * reported to listeners. * *

    FileMonitor will keep monitoring the file until {@link #stopMonitoring()} is called, even if the * monitored file doesn't exist anymore. Thus, it is important not to forget to call {@link #stopMonitoring()} when * monitoring is not needed anymore, in order to prevent unnecessary resource hogging. */ synchronized void startMonitoring() { if(monitorThread ==null) { monitorThread = new Thread(this); monitorThread.start(); isInitialized = false; // Wait until the thread has been started and initial file attributes have been fetched while(!isInitialized) { try { wait(); // run() will notify when initialization is complete } catch(InterruptedException e) {} } } } /** * Stops monitoring the monitored file. Does nothing if monitoring has not yet been started. */ public synchronized void stopMonitoring() { monitorThread = null; } /** * Returns true if this FileMonitor is currently monitoring the file. * * @return true if this FileMonitor is currently monitoring the file. */ public synchronized boolean isMonitoring() { return monitorThread!=null; } /** * Notifies all registered FileChangeListener instances that the monitored file has changed, specifying which * file attributes have changed. * * @param changedAttributes the set of attributes that have changed */ private void fireFileChangeEvent(int changedAttributes) { LOGGER.info("firing an event to registered listeners, changed attributes={}", changedAttributes); // Iterate on all listeners for(FileChangeListener listener : listeners.keySet()) listener.fileChanged(file, changedAttributes); } ///////////////////////////// // Runnable implementation // ///////////////////////////// public void run() { Thread thisThread = monitorThread; long lastDate = (attributes&DATE_ATTRIBUTE)!=0?file.getLastModifiedDate():0; long lastSize = (attributes&SIZE_ATTRIBUTE)!=0?file.getSize():0; int lastPermissions = (attributes&PERMISSIONS_ATTRIBUTE)!=0?file.getPermissions().getIntValue():0; boolean lastIsDirectory = (attributes&IS_DIRECTORY_ATTRIBUTE)!=0 && file.isDirectory(); boolean lastExists = (attributes&EXISTS_ATTRIBUTE)!=0 && file.exists(); synchronized(this) { // We are now ready to detect file changes, notify the thread that started this thread isInitialized = true; notify(); } long now; int changedAttributes; long tempLong; int tempInt; boolean tempBool; while(monitorThread ==thisThread) { changedAttributes = 0; now = System.currentTimeMillis(); if((attributes&DATE_ATTRIBUTE)!=0) { if((tempLong=file.getLastModifiedDate())!=lastDate) { lastDate = tempLong; changedAttributes |= DATE_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes&SIZE_ATTRIBUTE)!=0) { if((tempLong=file.getSize())!=lastSize) { lastSize = tempLong; changedAttributes |= SIZE_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes&PERMISSIONS_ATTRIBUTE)!=0) { if((tempInt=file.getPermissions().getIntValue())!=lastPermissions) { lastPermissions = tempInt; changedAttributes |= PERMISSIONS_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes& IS_DIRECTORY_ATTRIBUTE)!=0) { if((tempBool=file.isDirectory())!=lastIsDirectory) { lastIsDirectory = tempBool; changedAttributes |= IS_DIRECTORY_ATTRIBUTE; } } if(monitorThread ==thisThread && (attributes&EXISTS_ATTRIBUTE)!=0) { if((tempBool=file.exists())!=lastExists) { lastExists = tempBool; changedAttributes |= EXISTS_ATTRIBUTE; } } if(changedAttributes!=0) fireFileChangeEvent(changedAttributes); // Get some well-deserved rest: sleep for the specified poll period minus the time we spent // for this iteration try { Thread.sleep(Math.max(pollPeriod-(System.currentTimeMillis()-now), 0)); } catch(InterruptedException e) { } } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/FileMonitorConstants.java ================================================ package com.mucommander.commons.file.util; /** * Contains constants used by {@link com.mucommander.commons.file.util.FileMonitor}. * * @author Maxence Bernard */ public interface FileMonitorConstants { /** File date attribute, as returned by {@link com.mucommander.commons.file.AbstractFile#getLastModifiedDate()}. */ int DATE_ATTRIBUTE = 1; /** File size attribute, as returned by {@link com.mucommander.commons.file.AbstractFile#getSize()}. */ int SIZE_ATTRIBUTE = 2; /** File permissions attribute, as returned by {@link com.mucommander.commons.file.AbstractFile#getPermissions()}. */ int PERMISSIONS_ATTRIBUTE = 4; /** File 'is directory' attribute, as returned by {@link com.mucommander.commons.file.AbstractFile#isDirectory()}. */ int IS_DIRECTORY_ATTRIBUTE = 8; /** File 'exists' attribute, as returned by {@link com.mucommander.commons.file.AbstractFile#exists()}. */ int EXISTS_ATTRIBUTE = 16; /** Default attribute set: {@link #DATE_ATTRIBUTE}. */ int DEFAULT_ATTRIBUTES = DATE_ATTRIBUTE; /** Designates all attributes. */ int ALL_ATTRIBUTES = DATE_ATTRIBUTE|SIZE_ATTRIBUTE|PERMISSIONS_ATTRIBUTE|IS_DIRECTORY_ATTRIBUTE|EXISTS_ATTRIBUTE; /** Default poll period in milliseconds. */ long DEFAULT_POLL_PERIOD = 10000; } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/FilePool.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import org.apache.commons.collections4.map.ReferenceMap; /** * This class allows {@link AbstractFile} instances to be pooled, so that existing file instances can be reused, * and to guarantee that only one instance of the same file may exist at any given time. * File keys are mapped onto {@link AbstractFile} instances. Any kind of Object may be used as the key, * but a sensible choice is to use the {@link AbstractFile#getURL() file's URL}. * *

    Files are stored as {@link java.lang.ref.WeakReference weak references} so they can be garbage collected * when they are no longer hard-referenced. * *

    This class uses the {@link ReferenceMap} class part of the Apache Commons Collection library. * All accesses to the underlying map is synchronized, making this class thread-safe. * * @author Maxence Bernard */ public class FilePool { /** The actual hash map */ protected final ReferenceMap hashMap = new ReferenceMap<>(ReferenceMap.ReferenceStrength.HARD, ReferenceMap.ReferenceStrength.WEAK); /** * Creates a new file pool. */ public FilePool() { } /** * Adds a new key/file mapping to the pool. If a mapping with the same key exists, it is replaced and the previous * value returned. * * @param key the key that will later allow to retrieve the pooled file instance * @param value the file instance to add to the pool * @return returns the file instance previously mapped onto the given key, null if no * such mapping existed */ public synchronized AbstractFile put(Object key, AbstractFile value) { return hashMap.put(key, value); } /** * Returns the {@link AbstractFile} instance mapped onto the given key if there is one, * null otherwise * * @param key key of the file instance to retrieve * @return the {@link AbstractFile} instance mapped onto the given key if there is one, * null otherwise */ public synchronized AbstractFile get(Object key) { return hashMap.get(key); } /** * Returns true if this pool currently contains a key/file mapping where the given key is used as * the mapping's key. * * @param key key to lookup * @return true if this pool currently contains a key/file mapping where the given key is used as * the mapping's key. */ public synchronized boolean containsKey(Object key) { return hashMap.containsKey(key); } /** * Returns true if this pool currently contains a key/file mapping where the given file is used as * the mapping's value. * * @param file file to lookup * @return true if this pool currently contains a key/file mapping where the given file is used as * the mapping's key. */ public synchronized boolean containsValue(AbstractFile file) { return hashMap.containsValue(file); } /** * Removes all existing key/file mapping from this pool, leaving the pool in the same state as it was right after * its creation. */ public synchronized void clear() { hashMap.clear(); } /** * Returns the number of key/file mapping this pool currently contains. * * @return the number of key/file mapping this pool currently contains. */ public synchronized int size() { return hashMap.size(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/FileSet.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import java.util.Vector; /** * FileSet is a java.util.Vector of {@link AbstractFile} instances, and an optional base folder which is * the folder containing the files. * * @author Maxence Bernard */ public class FileSet extends Vector { /** The base/parent folder of the files this Vector contains */ private AbstractFile baseFolder; /** * Creates a new empty FileSet. */ public FileSet() { } /** * Creates a new empty FileSet with the specified base folder. * * @param baseFolder the folder containing the files */ public FileSet(AbstractFile baseFolder) { this.baseFolder = baseFolder; } /** * Creates a new empty FileSet with the specified base folder. * * @param baseFolder the folder containing the files * @param initialCapacity initial capacity of the vector */ public FileSet(AbstractFile baseFolder, int initialCapacity) { super(initialCapacity); this.baseFolder = baseFolder; } /** * Creates a new empty FileSet with the specified base folder, and adds the given file. * * @param baseFolder the folder containing the files * @param file the file to add */ public FileSet(AbstractFile baseFolder, AbstractFile file) { this.baseFolder = baseFolder; add(file); } /** * Returns the base folder associated with this FileSet, null if there isn't any. * * @return the base folder associated with this FileSet, null if there isn't any. */ public AbstractFile getBaseFolder() { return baseFolder; } /** * Adds all the files in the given array to this FileSet. Does nothing if the specified array is {@code null}. * * @param files the files to add to this FileSet. */ public void addAll(AbstractFile[] files) { if (files == null) { return; } for (AbstractFile file : files) { add(file); } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/Kernel32.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.runtime.OsFamily; import com.sun.jna.Native; import com.sun.jna.win32.W32APIOptions; /** * This class provides access to a static instance of the {@link com.mucommander.commons.file.util.Kernel32API} interface, * allowing to invoke selected Kernel32 Windows DLL functions. * *

    The Kernel32 DLL and the JNA library (which is used to access native libraries) may not be available on * all OS/CPU architectures: {@link #isAvailable()} can be used to determine that at runtime. * @see Kernel32API * @author Maxence Bernard */ public class Kernel32 { /** An instance of the Kernel32 DLL */ private static Kernel32API INSTANCE; static { if(OsFamily.WINDOWS.isCurrent()) { // Don't even bother if we're not running Windows try { INSTANCE = Native.load("Kernel32", Kernel32API.class, W32APIOptions.UNICODE_OPTIONS); } catch(Throwable e) { // java.lang.UnsatisfiedLinkError is thrown if the CPU architecture is not supported by JNA. INSTANCE = null; } } } /** * Returns true if the Kernel32 API can be accessed on the current OS/CPU architecture. * * @return true if the Kernel32 API can be accessed on the current OS/CPU architecture */ public static boolean isAvailable() { return INSTANCE!=null; } /** * Returns a static instance of the {@link com.mucommander.commons.file.util.Kernel32API} interface, allowing to invoke * some Kernel32 Windows DLL functions. null will be returned if {@link #isAvailable()} returned * false. * * @return a static instance of the {@link com.mucommander.commons.file.util.Kernel32API} interface, null * if it is not available on the current OS/CPU architecture */ public static Kernel32API getInstance() { return INSTANCE; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/Kernel32API.java ================================================ package com.mucommander.commons.file.util; import com.sun.jna.Structure; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; import com.sun.jna.win32.StdCallLibrary; import java.nio.CharBuffer; import java.util.Arrays; import java.util.List; /** * Exposes parts of the Windows Kernel32 API using the JNA (Java Native Access) library. * The {@link Kernel32} class should be used to retrieve an instance of this interface. * * @see Kernel32 * @author Maxence Bernard */ public interface Kernel32API extends StdCallLibrary { /** Custom alignment of structures. */ int STRUCTURE_ALIGNMENT = Structure.ALIGN_NONE; /** * Retrieves the calling thread's last-error code value. * The last-error code is maintained on a per-thread basis. Multiple threads do not overwrite each other's last-error code. * @return The return value is the calling thread's last-error code. */ int GetLastError(); /////////////////////////// // SetErrorMode Function // /////////////////////////// /** Use the system default, which is to display all error dialog boxes. */ int SEM_DEFAULT = 0; /** The system does not display the critical-error-handler message box. Instead, the system sends the error to the * calling process. */ int SEM_FAILCRITICALERRORS = 0x0001; /** The system automatically fixes memory alignment faults and makes them invisible to the application. It does this * for the calling process and any descendant processes. This feature is only supported by certain processor * architectures. For more information, see the Remarks sections. After this value is set for a process, subsequent * attempts to clear the value are ignored. */ int SEM_NOALIGNMENTFAULTEXCEPT = 0x0004; /** The system does not display the general-protection-fault message box. This flag should only be set by debugging * applications that handle general protection (GP) faults themselves with an exception handler. */ int SEM_NOGPFAULTERRORBOX = 0x0002; /** The system does not display a message box when it fails to find a file. Instead, the error is returned to the * calling process. */ int SEM_NOOPENFILEERRORBOX = 0x8000; /** * Controls whether the system will handle the specified types of serious errors or whether the process will handle * them. * *

    Remarks: Each process has an associated error mode that indicates to the system how the application is going * to respond to serious errors. A child process inherits the error mode of its parent process. To retrieve the * process error mode, use the GetErrorMode function. * Because the error mode is set for the entire process, you must ensure that multi-threaded applications do not set * different error-mode flags. Doing so can lead to inconsistent error handling. * The system does not make alignment faults visible to an application on all processor architectures. Therefore, * specifying SEM_NOALIGNMENTFAULTEXCEPT is not an error on such architectures, but the system is free to silently * ignore the request. * * @param uMode The process error mode. This parameter can be one or more of the following values: * SEM_DEFAULT (alone), SEM_FAILCRITICALERRORS, SEM_NOALIGNMENTFAULTEXCEPT, SEM_NOGPFAULTERRORBOX and * SEM_NOOPENFILEERRORBOX. * @return the previous state of the error-mode bit flags. */ int SetErrorMode(int uMode); ///////////////////////////////// // GetDiskFreeSpaceEx function // ///////////////////////////////// /** * Retrieves information about the amount of space that is available on a disk volume, which is the total amount of * space, the total amount of free space, and the total amount of free space available to the user that is * associated with the calling thread. * * @param lpDirectoryName A directory on the disk. * If this parameter is NULL, the function uses the root of the current disk. * If this parameter is a UNC name, it must include a trailing backslash, for example, "\\MyServer\MyShare\". * This parameter does not have to specify the root directory on a disk. The function accepts any directory on a disk. * The calling application must have FILE_LIST_DIRECTORY access rights for this directory. * @param lpFreeBytesAvailable A pointer to a variable that receives the total number of free bytes on a disk that * are available to the user who is associated with the calling thread. This parameter can be NULL. * If per-user quotas are being used, this value may be less than the total number of free bytes on a disk. * @param lpTotalNumberOfBytes A pointer to a variable that receives the total number of bytes on a disk that are * available to the user who is associated with the calling thread. This parameter can be NULL. * If per-user quotas are being used, this value may be less than the total number of bytes on a disk. * To determine the total number of bytes on a disk or volume, use IOCTL_DISK_GET_LENGTH_INFO. * @param lpTotalNumberOfFreeBytes A pointer to a variable that receives the total number of free bytes on a disk. * This parameter can be NULL. * @return If the function succeeds, the return value is nonzero. If the function fails, the return value is * zero (0). To get extended error information, call GetLastError. */ boolean GetDiskFreeSpaceEx(String lpDirectoryName, LongByReference lpFreeBytesAvailable, LongByReference lpTotalNumberOfBytes, LongByReference lpTotalNumberOfFreeBytes); /////////////////////////// // GetDriveType function // /////////////////////////// /** The drive type cannot be determined. */ int DRIVE_UNKNOWN = 0; /** The root path is invalid; for example, there is no volume is mounted at the path. */ int DRIVE_NO_ROOT_DIR = 1; /** The drive has removable media; for example, a floppy drive, thumb drive, or flash card reader. */ int DRIVE_REMOVABLE = 2; /** The drive has fixed media; for example, a hard drive or flash drive. */ int DRIVE_FIXED = 3; /** The drive is a remote (network) drive. */ int DRIVE_REMOTE = 4; /** The drive is a CD-ROM drive. */ int DRIVE_CDROM = 5; /** The drive is a RAM disk. */ int DRIVE_RAMDISK = 6; /** * Determines whether a disk drive is a removable, fixed, CD-ROM, RAM disk, or network drive. * * @param lpRootPathName The root directory for the drive. A trailing backslash is required. If this parameter is * NULL, the function uses the root of the current directory. * @return The return value specifies the type of drive, which can be one of the above values. */ int GetDriveType(String lpRootPathName); ///////////////////////// // MoveFileEx function // ///////////////////////// /** If a file named lpNewFileName exists, the function replaces its contents with the contents of the * lpExistingFileName file. This value cannot be used if lpNewFileName or lpExistingFileName names a directory. */ int MOVEFILE_REPLACE_EXISTING = 1; /** If the file is to be moved to a different volume, the function simulates the move by using the CopyFile and * DeleteFile functions.
    * This value cannot be used with MOVEFILE_DELAY_UNTIL_REBOOT. */ int MOVEFILE_COPY_ALLOWED = 2; /** The system does not move the file until the operating system is restarted. The system moves the file immediately * after AUTOCHK is executed, but before creating any paging files. Consequently, this parameter enables the * function to delete paging files from previous startups.
    * This value can be used only if the process is in the context of a user who belongs to the administrators group or * the LocalSystem account.
    * This value cannot be used with MOVEFILE_COPY_ALLOWED.
    * Windows 2000: If you specify the MOVEFILE_DELAY_UNTIL_REBOOT flag for dwFlags, you cannot also prepend * the filename that is specified by lpExistingFileName with "\\?". */ int MOVEFILE_DELAY_UNTIL_REBOOT = 4; /** The function does not return until the file is actually moved on the disk.
    * Setting this value guarantees that a move performed as a copy and delete operation is flushed to disk before the * function returns. The flush occurs at the end of the copy operation.
    * This value has no effect if MOVEFILE_DELAY_UNTIL_REBOOT is set. */ int MOVEFILE_WRITE_THROUGH = 8; /** Reserved for future use. */ int MOVEFILE_CREATE_HARDLINK = 16; /** The function fails if the source file is a link source, but the file cannot be tracked after the move. * This situation can occur if the destination is a volume formatted with the FAT file system. */ int MOVEFILE_FAIL_IF_NOT_TRACKABLE = 32; /** * Moves an existing file or directory, including its children, with various move options. * *

    Warning: this method is NOT available on Windows 95, 98 and Me. * * @param lpExistingFileName The current name of the file or directory on the local computer. If dwFlags specifies * MOVEFILE_DELAY_UNTIL_REBOOT, the file cannot exist on a remote share, because delayed operations are performed * before the network is available. * @param lpNewFileName The new name of the file or directory on the local computer. When moving a file, the * destination can be on a different file system or volume. If the destination is on another drive, you must set the * MOVEFILE_COPY_ALLOWED flag in dwFlags. When moving a directory, the destination must be on the same drive. * @param dwFlags This parameter can be one or more of the 'MOVEFILE_' constant values. * @return If the function succeeds, the return value is nonzero. If the function fails, the return value is * zero (0). To get extended error information, call GetLastError. */ boolean MoveFileEx(String lpExistingFileName, String lpNewFileName, int dwFlags); /////////////////////////////////// // GetVolumeInformation function // /////////////////////////////////// /** The file system preserves the case of file names when it places a name on disk. */ int FILE_CASE_PRESERVED_NAMES = 0x00000002; /** The file system supports case-sensitive file names. */ int FILE_CASE_SENSITIVE_SEARCH = 0x00000001; /** The file system supports file-based compression. */ int FILE_FILE_COMPRESSION = 0x00000010; /** The file system supports named streams. */ int FILE_NAMED_STREAMS = 0x00040000; /** The file system preserves and enforces access control lists (ACL). For example, the NTFS file system preserves * and enforces ACLs, and the FAT file system does not. */ int FILE_PERSISTENT_ACLS = 0x00000008; /** The specified volume is read-only. Windows 2000: This value is not supported.*/ int FILE_READ_ONLY_VOLUME = 0x00080000; /** The volume supports a single sequential write. */ int FILE_SEQUENTIAL_WRITE_ONCE = 0x00100000; /** The file system supports the Encrypted File System (EFS). */ int FILE_SUPPORTS_ENCRYPTION = 0x00020000; /** The file system supports object identifiers. */ int FILE_SUPPORTS_OBJECT_IDS = 0x00010000; /** The file system supports re-parse points. */ int FILE_SUPPORTS_REPARSE_POINTS = 0x00000080; /** The file system supports sparse files. */ int FILE_SUPPORTS_SPARSE_FILES = 0x00000040; /** The volume supports transactions. */ int FILE_SUPPORTS_TRANSACTIONS = 0x00200000; /** The file system supports Unicode in file names as they appear on disk. */ int FILE_UNICODE_ON_DISK = 0x00000004; /** The specified volume is a compressed volume, for example, a DoubleSpace volume. */ int FILE_VOLUME_IS_COMPRESSED = 0x00008000; /** The file system supports disk quotas. */ int FILE_VOLUME_QUOTAS = 0x00000020; /** * Retrieves information about the file system and volume associated with the specified root directory. * * @param lpRootPathName A pointer to a string that contains the root directory of the volume to be described. * If this parameter is NULL, the root of the current directory is used. A trailing backslash is required. * For example, you specify \\MyServer\MyShare as "\\MyServer\MyShare\", or the C drive as "C:\". * @param lpVolumeNameBuffer A pointer to a buffer that receives the name of a specified volume. The maximum buffer * size is MAX_PATH+1. * @param nVolumeNameSize The length of a volume name buffer, in TCHARs. The maximum buffer size is MAX_PATH+1. * This parameter is ignored if the volume name buffer is not supplied. * @param lpVolumeSerialNumber A pointer to a variable that receives the volume serial number. * This parameter can be NULL if the serial number is not required. * @param lpMaximumComponentLength A pointer to a variable that receives the maximum length, in TCHARs, of a file * name component that a specified file system supports. A file name component is the portion of a file name between * backslashes. The value that is stored in the variable that *lpMaximumComponentLength points to is used to * indicate that a specified file system supports long names. For example, for a FAT file system that supports long * names, the function stores the value 255, rather than the previous 8.3 indicator. Long names can also be * supported on systems that use the NTFS file system. * @param lpFileSystemFlags A pointer to a variable that receives flags associated with the specified file system. * This parameter can be one or more of the following flags ; FS_FILE_COMPRESSION and FS_VOL_IS_COMPRESSED * are mutually exclusive: * FILE_CASE_PRESERVED_NAMES, FILE_CASE_SENSITIVE_SEARCH, FILE_FILE_COMPRESSION, FILE_NAMED_STREAMS, * FILE_PERSISTENT_ACLS, FILE_READ_ONLY_VOLUME, FILE_SEQUENTIAL_WRITE_ONCE, FILE_SUPPORTS_ENCRYPTION, * FILE_SUPPORTS_OBJECT_IDS, FILE_SUPPORTS_REPARSE_POINTS, FILE_SUPPORTS_SPARSE_FILES, FILE_SUPPORTS_TRANSACTIONS, * FILE_UNICODE_ON_DISK, FILE_VOLUME_IS_COMPRESSED, FILE_VOLUME_QUOTAS. * @param lpFileSystemNameBuffer A pointer to a buffer that receives the name of the file system, for example, the * FAT file system or the NTFS file system. The maximum buffer size is MAX_PATH+1. * @param nFileSystemNameSize The length of the file system name buffer, in TCHARs. The maximum buffer size is * MAX_PATH+1. This parameter is ignored if the file system name buffer is not supplied. * @return If all the requested information is retrieved, the return value is true. If not all the requested * information is retrieved, the return value is false. To get extended error information, call GetLastError. */ boolean GetVolumeInformation( char[] lpRootPathName, CharBuffer lpVolumeNameBuffer, int nVolumeNameSize, LongByReference lpVolumeSerialNumber, LongByReference lpMaximumComponentLength, LongByReference lpFileSystemFlags, CharBuffer lpFileSystemNameBuffer, int nFileSystemNameSize ); //////////////////////////////// // GetFileAttributes function // //////////////////////////////// /** Failed to retrieve file attributes */ int INVALID_FILE_ATTRIBUTES = -1; /** A file or directory that the operating system uses a part of, or uses exclusively. */ int FILE_ATTRIBUTE_SYSTEM = 0x00000004; /** * Retrieves file system attributes for a specified file or directory. * * @param fileName The name of the file or directory. * @return If the function succeeds, the return value contains the attributes of the specified file or directory. * If the function fails, the return value is INVALID_FILE_ATTRIBUTES. To get extended error information, call GetLastError. */ int GetFileAttributes(String fileName); //////////////////////////// // FindFirstFile function // //////////////////////////// /** Alias class for W32API.HANDLE. */ final class FindFileHandle extends WinNT.HANDLE { public boolean isValid() { return this != WinNT.INVALID_HANDLE_VALUE; } } /** Contains information about the file that is found by the FindFirstFile, FindFirstFileEx, or FindNextFile function. */ class WIN32_FIND_DATA extends Structure { /** The file attributes of a file. */ public int dwFileAttributes; /** A FILETIME structure that specifies when a file or directory was created. */ public long ftCreationTime; /** For a file, the structure specifies when the file was last read from, written to, or for executable files, run. * For a directory, the structure specifies when the directory is created. If the underlying file system does not support last access time, this member is zero. */ public long ftLastAccessTime; /** For a file, the structure specifies when the file was last written to, truncated, or overwritten, for example, when WriteFile or SetEndOfFile are used. The date and time are not updated when file attributes or security descriptors are changed. * For a directory, the structure specifies when the directory is created. If the underlying file system does not support last write time, this member is zero. */ public long ftLastWriteTime; /** The high-order DWORD value of the file size, in bytes. * This value is zero unless the file size is greater than MAXDWORD. * The size of the file is equal to (nFileSizeHigh * (MAXDWORD+1)) + nFileSizeLow. */ public int nFileSizeHigh; /** The low-order DWORD value of the file size, in bytes. */ public int nFileSizeLow; /** If the dwFileAttributes member includes the FILE_ATTRIBUTE_REPARSE_POINT attribute, this member specifies the reparse point tag. * Otherwise, this value is undefined and should not be used. */ public int dwReserved0; /** Reserved for future use. */ public int dwReserved1; /** The name of the file. */ public char[] cFileName = new char[250]; /** An alternative name for the file. * This name is in the classic 8.3 file name format. */ public char[] cAlternateFileName = new char[14]; @Override protected List getFieldOrder() { return Arrays.asList("dwFileAttributes", "ftCreationTime", "ftLastAccessTime", "ftLastWriteTime", "nFileSizeHigh", "nFileSizeLow", "dwReserved0", "dwReserved1", "cFileName", "cAlternateFileName"); } } /** * Searches a directory for a file or subdirectory with a name that matches a specific name (or partial name if wildcards are used). * * @param fileName The directory or path, and the file name, which can include wildcard characters, for example, an asterisk (*) or a question mark (?). * @param findFileData A pointer to the WIN32_FIND_DATA structure that receives information about a found file or directory. * @return If the function succeeds, the return value is a search handle used in a subsequent call to FindNextFile or FindClose, and the lpFindFileData parameter contains information about the first file or directory found. */ FindFileHandle FindFirstFile(String fileName, WIN32_FIND_DATA findFileData); ///////////////////////// // FindClose function // //////////////////////// /** * Closes a file search handle opened by the FindFirstFile, FindFirstFileEx, FindFirstFileNameW, FindFirstFileNameTransactedW, FindFirstFileTransacted, FindFirstStreamTransactedW, or FindFirstStreamW functions. * * @param hFindFile The file search handle. * @return If the function succeeds, the return value is nonzero. * If the function fails, the return value is zero. To get extended error information, call GetLastError. */ boolean FindClose(FindFileHandle hFindFile); } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/OSXFileUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.util; import com.dd.plist.BinaryPropertyListParser; import com.dd.plist.NSString; import com.dd.plist.PropertyListFormatException; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsVersion; import com.sun.jna.platform.mac.XAttrUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.UnsupportedEncodingException; /** * This class contains methods for file operations that are specific to Mac OS X. * * @author Maxence Bernard */ public class OSXFileUtils { /** AppleScript that sets file comment */ public static final String SET_COMMENT_APPLESCRIPT = "tell application \"Finder\" to set comment of file {posix file \"%s\"} to \"%s\""; private static final Logger LOGGER = LoggerFactory.getLogger(OSXFileUtils.class); /** * Returns the Spotlight/Finder comment of the given file. The specified file must be a LocalFile, * or have a LocalFile as an ancestor. * *

    * null is returned in any of the following cases: *

      *
    • if the current OS is not Mac OS X or if the version is not 10.4 or higher (i.e. Spotlight is not available)
    • *
    • if the specified file is not a LocalFile and does not have a LocalFile ancestor
    • *
    • if the specified file has no comment
    • *
    • if the comment could not be retrieved (for any reason)
    • *
    * * @param file a local file * @return the Spotlight/Finder comment of the specified file */ public static String getSpotlightComment(AbstractFile file) { if (!OsVersion.MAC_OS_X_10_4.isCurrentOrHigher()) { return null; } byte[] bytes = XAttrUtils.read(file.getAbsolutePath(), XAttrUtils.COMMENT); if (bytes == null) return null; try { NSString comment = (NSString) BinaryPropertyListParser.parse(bytes); return comment.getContent(); } catch (UnsupportedEncodingException | PropertyListFormatException e) { LOGGER.error("failed to read comment of: " + file.getAbsolutePath()); return null; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/PathTokenizer.java ================================================ package com.mucommander.commons.file.util; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.StringTokenizer; import java.util.Vector; /** * This class allows to break a path into filename tokens. The default separator characters are '/' and '\', but any * other separator characters can be specified. The {@link #hasMoreFilenames()} and {@link #nextFilename()} methods * can be used to iterate through all filename tokens. The {@link #getLastSeparator()} returns the last separator * string that appeared before the last filename token returned by {@link #nextFilename()}. Initially, this method * returns any leading separators the path may contain. * The {@link #getCurrentPath()} returns the current part of the path that has been tokenized. * *

    To illustrate the use of PathTokenizer, the following piece of code iterates through all filename tokens and * reconstructs the original path : * * PathTokenizer pt = new PathTokenizer(path); * String reconstructedPath = pt.getLastSeparator(); * while(pt.hasMoreFilenames()) { * String nextToken = pt.nextFilename(); * String lastSeparator = pt.getLastSeparator(); * reconstructedPath += nextToken+lastSeparator; * } * * *

    Note that PathTokenizer does not enforce valid file paths, that means a path with an incorrect syntax can still * be tokenized. In particular: *

      *
    • A path can contain mixed separators, e.g. C:\temp/file *
    • Filename tokens can be separated by multiple separator characters, e.g. /usr//local, {@link #getLastSeparator()} * will return the complete separator string, in this case "//" for the 'usr' filename token. *
    * * @author Maxence Bernard */ public class PathTokenizer implements Enumeration { /** Separator characters */ private String separators; /** True if this PathTokenizer tokenizes the path in reverse order, from right to left. */ private boolean reverseOrder; /** Path tokens: separators and filenames. */ private String[] tokens; /** Current index in the token array. */ private int currentIndex; /** Path part that has been tokenized. */ private StringBuffer currentPath; /** Last separators token. */ private String lastSeparator; /** Default separator characters. */ public final static String DEFAULT_SEPARATORS = "/\\"; /** * Creates a new PathTokenizer using the given path and default separator characters ({@link #DEFAULT_SEPARATORS}}, * in forward order, tokenizing the path from left to right. * * @param path the path to break into tokens */ public PathTokenizer(String path) { this(path, DEFAULT_SEPARATORS, false); } /** * Creates a new PathTokenizer using the given path and separator character(s). * * @param path the path to break into tokens * @param separators the character(s) that separate tokens * @param reverseOrder if true, the path will be tokenized in reverse order, from right to left */ public PathTokenizer(String path, String separators, boolean reverseOrder) { this.separators = separators; this.reverseOrder = reverseOrder; // Split the path into tokens StringTokenizer st = new StringTokenizer(path, separators, true); Vector tokensV = new Vector<>(); while(st.hasMoreTokens()) { tokensV.add(st.nextToken()); } // Convert Vector into array tokens = new String[tokensV.size()]; int nbTokens = tokens.length; if(reverseOrder) { for(int i=0; itrue if this PathTokenizer has more filename tokens. * @return true if this PathTokenizer has more filename tokens, false otherwise. */ public boolean hasMoreFilenames() { return currentIndex * If this PathTokenizer operates in reverse order, the returned path is the path part that has not yet been * tokenized. * @return the current path part that has been tokenized. */ public String getCurrentPath() { return currentPath.toString(); } /** * Returns the last separator string that appeared in the path after the last filename token returned by * {@link #nextFilename()} and before the next filename, or an empty string "" if there isn't any separator * character after the filename (path ends without a trailing separator).
    * Note: the returned string can be made of several consecutive separator characters. * *

    Initially, before any calls to {@link #nextFilename()} have been made, this method will return any leading * separator string in the path string, or an empty string if the path doesn't start with a separator. * @return the last separator string that appeared in the path. */ public String getLastSeparator() { return lastSeparator; } //////////////////////////////// // Enumeration implementation // //////////////////////////////// /** * Enumeration implementation, returns the same value as {@link #hasMoreFilenames()}. */ public boolean hasMoreElements() { return hasMoreFilenames(); } /** * Enumeration implementation, returns the same value as {@link #nextFilename()}. */ public String nextElement() throws NoSuchElementException { return nextFilename(); } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/PathUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.MalformedURLException; /** * This class contains static helper methods that operate on file paths. * * @author Maxence Bernard */ public class PathUtils { private static final Logger LOGGER = LoggerFactory.getLogger(PathUtils.class); /** * This class represents a destination entered by the user and resolved by {@link PathUtils#resolveDestination(String, com.mucommander.commons.file.AbstractFile)} * into an AbstractFile and a destination type. * * @see PathUtils#resolveDestination(String, com.mucommander.commons.file.AbstractFile) */ public static class ResolvedDestination { /** The destination AbstractFile, may be a regular file or a folder */ private final AbstractFile file; /** The destination's folder, the file itself for {@link #EXISTING_FOLDER}, the destination file's parent for * other types */ private final AbstractFile folder; /** The destination type, see constant values */ private final int type; /** Designates a folder, either a directory or archive, that exists on the filesystem. */ public static final int EXISTING_FOLDER = 0; /** Designates a regular file that exists on the filesystem. The file may be a browsable archive but that was * referred to as a regular file, i.e. without a trailing separator character in the path. */ public static final int EXISTING_FILE = 1; /** Designates a new file that doesn't exist on the filesystem. The file's parent however does always exist. */ public static final int NEW_FILE = 2; /** Designates a new directory that doesn't exist on the filesystem. The file's parent directory does not exist too. */ public static final int NEW_DIRECTORIES = 3; /** * Creates a new ResolvedDestination with the specified destination file and type. * * @param destinationFile the destination file * @param destinationType the destination type * @param destinationFolder the destination folder */ private ResolvedDestination(AbstractFile destinationFile, int destinationType, AbstractFile destinationFolder) { this.file = destinationFile; this.type = destinationType; this.folder = destinationFolder; } /** * Returns the resolved destination file. The returned file may or may not physically exist on the filesystem. * If it exists, the returned file may be a folder (directory or browsable archive) or a regular file. * * @return the resolved destination file * @see #getDestinationType() */ public AbstractFile getDestinationFile() { return file; } /** * Returns the resolved destination's folder. Depending on the {@link #getDestinationType() destination type}, * the destination folder is: *

    *
    for {@link #EXISTING_FOLDER}
    the {@link #getDestinationFile() destination file} itself
    *
    for {@link #EXISTING_FILE} or {@link #NEW_FILE}
    the {@link #getDestinationFile() destination file}'s parent
    *
    * The returned AbstractFile is always a folder that exists. * * @return the resolved destination file * @see #getDestinationType() */ public AbstractFile getDestinationFolder() { return folder; } /** * Returns the type of destination that was resolved. The returned value will be one of the following constant * fields defined in this class: *
    *
    {@link #EXISTING_FOLDER}
    if the path denotes a folder, either a directory or a browsable * archive.
    *
    {@link #EXISTING_FILE}
    if the path denotes a regular file. The file may be a browsable archive, * see below.
    *
    {@link #NEW_FILE}
    if the path denotes a non-existing file whose parent exists.
    *
    * Paths to browsable archives are considered as denoting a folder only if they end with a trailing separator * character. If they don't, they're considered as denoting a regular file. For example, * /existing_folder/existing_archive.zip/ refers to the archive as a folder where as * /existing_folder/existing_archive.zip refers to the archive as a regular file. * * @return the type of destination that was resolved */ public int getDestinationType() { return type; } } /** * Resolves a destination path entered by the user and returns a {@link ResolvedDestination} object that * that contains a {@link AbstractFile} instance corresponding to the path and a type that describes the kind of * destination that was resolved. null is returned if the path is not a valid destination (see below) * or could not be resolved, for example because of I/O or authentication error. *

    * The given path may be either absolute or relative to the specified base folder. If the base folder argument is * null and the path is relative, null will always be returned. * The path may contain '.', '..' and '~' tokens which will be left for the corresponding * {@link com.mucommander.commons.file.SchemeParser} to canonize. * *

    * The path may refer to the following listed destination types. In all cases, the destination's parent folder must * exist, if it doesn't null will always be returned. For example, /non_existing_folder/file * is not a valid destination (provided that '/non_existing_folder' does not exist). *

    *
    {@link ResolvedDestination#EXISTING_FOLDER}
    if the path denotes a folder, either a directory or a * browsable archive.
    *
    {@link ResolvedDestination#EXISTING_FILE}
    if the path denotes a regular file. The file may be a browsable archive, * see below.
    *
    {@link ResolvedDestination#NEW_FILE}
    if the path denotes a non-existing file whose parent exists.
    *
    * Paths to browsable archives are considered as denoting a folder only if they end with a trailing separator * character. If they don't, they're considered as denoting a regular file. For example, * /existing_folder/existing_archive.zip/ refers to the archive as a folder where as * /existing_folder/existing_archive.zip refers to the archive as a regular file. * * @param destPath the destination path to resolve * @param baseFolder the base folder used for relative paths, null to accept only absolute paths * @param requireParentExists if parent directory must exists * @return the object that that contains a {@link AbstractFile} instance corresponding to the path and a type that * describes the kind of destination that was resolved */ public static ResolvedDestination resolveDestination(String destPath, AbstractFile baseFolder, boolean requireParentExists) { FileURL destURL; // Try to resolve the path as a URL try { destURL = FileURL.getFileURL(destPath); // destPath is absolute } catch (MalformedURLException e) { // destPath is relative (or malformed) // Abort now if there is no base folder if (baseFolder == null) { return null; } String separator = baseFolder.getSeparator(); // Start by cloning the base folder's URL, including credentials and properties FileURL baseFolderURL = baseFolder.getURL(); destURL = (FileURL)baseFolderURL.clone(); String basePath = destURL.getPath(); if (!destPath.isEmpty()) { destURL.setPath(basePath + (basePath.endsWith(separator) ? "" : separator) + destPath); } // At this point we have the proper URL, except that the path may contain '.', '..' or '~' tokens. // => parse the URL from scratch to have the SchemeParser canonize them. try { destURL = FileURL.getFileURL(destURL.toString(false)); // Import credentials separately, so that login and passwords that contain URI-unsafe characters // such as '/' are properly parsed. destURL.setCredentials(baseFolderURL.getCredentials()); destURL.importProperties(baseFolderURL); } catch (MalformedURLException e2) { return null; } } // No point in going any further if the URL cannot be resolved into a file AbstractFile destFile = FileFactory.getFile(destURL); if (destFile == null) { LOGGER.info("could not resolve a file for {}", destURL); return null; } // Test if the destination file exists boolean destFileExists = destFile.exists(); if (destFileExists) { // Note: path to archives must end with a trailing separator character to refer to the archive as a folder, // if they don't, they'll refer to the archive as a file. if (destFile.isDirectory() || (destPath.endsWith(destFile.getSeparator()) && destFile.isBrowsable())) { return new ResolvedDestination(destFile, ResolvedDestination.EXISTING_FOLDER, destFile); } } // Test if the destination's parent exists, if not the path is not a valid destination AbstractFile destParent = destFile.getParent(); if (!requireParentExists) { return new ResolvedDestination(destFile, ResolvedDestination.NEW_DIRECTORIES, destParent); } if (destParent == null || !destParent.exists()) { return null; } return new ResolvedDestination(destFile, destFileExists ? ResolvedDestination.EXISTING_FILE : ResolvedDestination.NEW_FILE, destParent); } public static ResolvedDestination resolveDestination(String destPath, AbstractFile baseFolder) { return resolveDestination(destPath, baseFolder, true); } /** * Removes any leading separator character (slash or backslash) from the given path and returns the modified path. * * @param path the path to modify * @return the modified path, free of any leading separator */ public static String removeLeadingSeparator(String path) { char firstChar; if (!path.isEmpty() && ((firstChar=path.charAt(0)) == '/' || firstChar=='\\')) { return path.substring(1); } return path; } /** * Removes any leading separator character from the given path and returns the modified path. * * @param path the path to modify * @param separator the path separator, usually "/" or "\\" * @return the modified path, free of any leading separator */ public static String removeLeadingSeparator(String path, String separator) { if (path.startsWith(separator)) { return path.substring(separator.length()); } return path; } /** * Removes any trailing separator character (slash or backslash) from the given path and returns the modified path. * * @param path the path to modify * @return the modified path, free of any trailing separator */ public static String removeTrailingSeparator(String path) { char lastChar; int len = path.length(); if (len > 0 && ((lastChar = path.charAt(len-1)) =='/' || lastChar=='\\')) { return path.substring(0, len - 1); } return path; } /** * Removes any trailing separator character (slash or backslash) from the given path and returns the modified path. * * @param path the path to modify * @param separator the path separator, usually "/" or "\\" * @return the modified path, free of any trailing separator */ public static String removeTrailingSeparator(String path, String separator) { if (path.endsWith(separator)) { return path.substring(0, path.length() - separator.length()); } return path; } /** * Returns true if both specified paths are equal. The path comparison is case-sensitive but trailing * separator-insensitive: if the sole difference between two paths is a trailing path separator, they will be * considered as equal. For example, /path and /path/ are considered equal, assuming the * path separator is '/'. *

    * If any of the two specified paths is null, then the other one must also be null for * this method to return true. The given separator must never be null or * a {@link NullPointerException} will be thrown. * * @param path1 first path to test * @param path2 second path to test * @param separator path separator for both paths * @throws NullPointerException if the given separator is null * @return true if both paths are equal */ public static boolean pathEquals(String path1, String path2, String separator) { if (path1 == null) { return path2 == null; } if (path2 == null) { return false; } if (path1.equals(path2)) { return true; } int len1 = path1.length(); int len2 = path2.length(); int separatorLen = separator.length(); // If the difference between the 2 strings is just a trailing path separator, we consider the paths as equal if (Math.abs(len1-len2)==separatorLen && (len1>len2 ? path1.startsWith(path2) : path2.startsWith(path1))) { String diff = len1>len2 ? path1.substring(len1-separatorLen) : path2.substring(len2-separatorLen); return separator.equals(diff); } return false; } /** * Returns a hashcode for the given path. The returned hashcode is consistent with * {@link #pathEquals(String, String, String)} in that hashcodes are trailing separator-invariant: * path1.equals(path2) implies path1Hashcode==path2Hashcode.hashCode(), even if path1 * ends with a separator and path2 does not or vice-versa. * * @param path the path for which to return a hashcode * @param separator separator of the given path * @return a trailing separator-insensitive hashcode */ public static int getPathHashCode(String path, String separator) { // #equals(Object) is trailing separator insensitive, so the hashCode must be trailing separator invariant return path.endsWith(separator) ?path.substring(0, path.length()-separator.length()).hashCode() :path.hashCode(); } /** * Removes the specified number of fragments from the beginning of the given path and returns the modified path, * free of a leading separator. Returns an empty string ("") if the path does not contain less or * exactly that many fragments. * *

    * For instance, calling this method with *

      *
    • ("/home/maxence/, "/", 0) will return "home/maxence/"
    • *
    • ("/home/maxence/, "/", 1) will return "maxence/"
    • *
    • ("/home/maxence/, "/", 2) will return ""
    • *
    • ("/home/maxence/, "/", 3) will return ""
    • *
    * * @param path the path to modify * @param separator the path separator, usually "/" or "\\" * @param nbFragments number of path fragments to remove from the path * @return the modified path, free of any leading separator */ public static String removeLeadingFragments(String path, String separator, int nbFragments) { path = removeLeadingSeparator(path, separator); if (nbFragments == 0) { return path; } int pos = -1; for (int i = 0; i < nbFragments && (pos=path.indexOf(separator, pos+1)) >= 0; i++); if (pos < 0 || pos == path.length()-1) { return ""; } return path.substring(pos+1); } /** * Returns the depth of the specified path, based on the number of path separators it contains, excluding those * occurring at the beginning and at the end. The minimum depth of a path is 0.
    * Here are a few examples when the path separator is "/": *
    *
    /
    0
    *
    /home
    1
    *
    /home/maxence
    2
    *
    * *

    * It is worth noting that this method relies strictly on the occurences of path separators and nothing else. * Therefore, Windows-like paths that start with a drive letter will always have a minimum depth * of 1.
    * Here are a few examples when the path separator is {@code "\\"}: *

    *
    C:\\
    1
    *
    C:\\home
    2
    *
    C:\\home\\maxence
    1
    *
    * * @param path the path for which to calculate the depth * @param separator the path separator, usually "/" or "\\" * @return the depth of the given path */ public static int getDepth(String path, String separator) { if (path.isEmpty() || path.equals(separator)) { return 0; } int pos = path.startsWith(separator) ? 1 : 0; int depth = 1; while ((pos=path.indexOf(separator, pos+1)) >= 0) { depth++; } if (path.endsWith(separator)) { depth--; } return depth; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/ResourceLoader.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.impl.local.LocalFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; /** * This class provides methods to load resources located within the reach of a ClassLoader. Those resources * can reside either in a JAR file or in a local directory that are in the classpath -- all methods of this class are * agnostic to either type of location. * *

    The getResourceAsURL and getResourceAsStream methods are akin to those of * java.lang.Class and java.lang.ClassLoader, except that they are not sensitive to the * presence of a leading forward-slash separator in the resource path, and that they allow the search to be limited * to a particular classpath location. * *

    But the real fun lies in the getResourceAsFile methods, which allow to manipulate resources as * regular files -- again, whether they be in a regular directory or in a JAR file. Likewise, * the {@link #getRootPackageAsFile(Class)} allows to dynamically explore and manipulate the resource files contained * in a particular classpath's location. * * @author Maxence Bernard */ public class ResourceLoader { private static final Logger LOGGER = LoggerFactory.getLogger(ResourceLoader.class); /** the default ClassLoader that is used by methods without a ClassLoader argument */ private static ClassLoader defaultClassLoader = ResourceLoader.class.getClassLoader(); /** * Returns the default ClassLoader that is used by methods without a ClassLoader argument. * This default ClassLoader is the one that loaded this class, and not the system ClassLoader. * * @return the default ClassLoader that is used by methods without a ClassLoader argument */ public static ClassLoader getDefaultClassLoader() { // We do not use the system class loader because it does not work with JNLP/Webstart applications. // A quote from a FAQ at java.sun.com: // // Java Web Start uses a user-level classloader to load all the application resources specified in the JNLP file. // This classloader implements the security model and the downloading model defined by the JNLP specification. // This is no different than how the AppletViewer or the Java Plug-In works. // This has the, unfortunate, side-effect that Class.forName will not find any resources that are defined in the // JNLP file. The same is true for looking up resources and classes using the system class loader // (ClassLoader.getSystemClassLoader). // return defaultClassLoader; } /** * This method is similar to {@link #getResourceAsURL(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @return a URL pointing to the resource, or null if the resource couldn't be located */ public static URL getPackageResourceAsURL(Package ppackage, String name) { return getPackageResourceAsURL(ppackage, name, getDefaultClassLoader(), null); } /** * This method is similar to {@link #getResourceAsURL(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param classLoader the ClassLoader used for locating the resource. May not be null. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * null to look for the resource in the whole class path. * @param name name of the resource in the package. This is a filename only, not a path. * @return a URL pointing to the resource, or null if the resource couldn't be located * @see #getRootPackageAsFile(Class) */ public static URL getPackageResourceAsURL(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) { return ResourceLoader.getResourceAsURL(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile); } /** * Shorthand for {@link #getResourceAsURL(String, ClassLoader, AbstractFile)} called with the * {@link #getDefaultClassLoader() default class loader} and a null root package file. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @return a URL pointing to the resource, or null if the resource couldn't be located */ public static URL getResourceAsURL(String path) { return getResourceAsURL(path, getDefaultClassLoader(), null); } /** * Finds the resource with the given path and returns a URL pointing to its location, or null * if the resource couldn't be located. The given ClassLoader is used for locating the resource. * *

    The given resource path must be forward slash (/) separated. It may or may not start with a * leading forward slash character, this doesn't affect the way it is interpreted. * *

    The rootPackageFile argument can be used to limit the scope of the search to a specific * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched. * This avoids potential ambiguities that can arise if the specified resource path exists in several locations. * If this parameter is null, the resource is looked up in the whole class path. In that case and if * several resources with the specified path exist, the choice of the resource to return is arbitrary. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @param classLoader the ClassLoader used for locating the resource. May not be null. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * null to look for the resource in the whole class path. * @return a URL pointing to the resource, or null if the resource couldn't be located */ public static URL getResourceAsURL(String path, ClassLoader classLoader, AbstractFile rootPackageFile) { path = removeLeadingSlash(path); if (rootPackageFile == null) { return classLoader.getResource(path); } String separator = rootPackageFile.getSeparator(); String nativePath = separator.equals("/") ? path : path.replace("/", separator); try { // Iterate through all resources that match the given path, and return the one located inside the // given root package file. Enumeration resourceEnum = classLoader.getResources(path); String rootPackagePath = rootPackageFile.getAbsolutePath(); String resourcePath = rootPackageFile.getAbsolutePath(true)+nativePath; URL resourceURL; while(resourceEnum.hasMoreElements()) { resourceURL = resourceEnum.nextElement(); if ("jar".equals(resourceURL.getProtocol())) { if (getJarFilePath(resourceURL).equals(rootPackagePath)) { return resourceURL; } } else { if (normalizeUrlPath(getDecodedURLPath(resourceURL)).equals(resourcePath)) { return resourceURL; } } } } catch(IOException e) { LOGGER.info("Failed to lookup resource {}", path, e); return null; } return null; } /** * This method is similar to {@link #getResourceAsStream(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @return an InputStream that allows to read the resource, or null if the resource couldn't be located */ public static InputStream getPackageResourceAsStream(Package ppackage, String name) { return getPackageResourceAsStream(ppackage, name, getDefaultClassLoader(), null); } /** * This method is similar to {@link #getResourceAsStream(String, ClassLoader, AbstractFile)} except that it looks * for a resource with a given name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @param classLoader the ClassLoader used for locating the resource. May not be null. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * null to look for the resource in the whole class path. * @return an InputStream that allows to read the resource, or null if the resource couldn't be located */ public static InputStream getPackageResourceAsStream(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) { return getResourceAsStream(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile); } /** * Shorthand for {@link #getResourceAsStream(String, ClassLoader, AbstractFile)} called with the * {@link #getDefaultClassLoader() default class loader} and a null root package file. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @return an InputStream that allows to read the resource, or null if the resource couldn't be located */ public static InputStream getResourceAsStream(String path) { return getResourceAsStream(path, getDefaultClassLoader(), null); } /** * Finds the resource with the given path and returns an InputStream to read it, or null * if the resource couldn't be located. The given ClassLoader is used for locating the resource. * *

    The given resource path must be forward slash (/) separated. It may or may not start with a * leading forward slash character, this doesn't affect the way it is interpreted. * *

    The rootPackageFile argument can be used to limit the scope of the search to a specific * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched. * This avoids potential ambiguities that can arise if the specified resource path exists in several locations. * If this parameter is null, the resource is looked up in the whole class path. In that case and if * several resources with the specified path exist, the choice of the resource to return is arbitrary. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @param classLoader the ClassLoader used for locating the resource. May not be null. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * null to look for the resource in the whole class path. * @return an InputStream that allows to read the resource, or null if the resource couldn't be located */ public static InputStream getResourceAsStream(String path, ClassLoader classLoader, AbstractFile rootPackageFile) { try { URL resourceURL = getResourceAsURL(path, classLoader, rootPackageFile); return resourceURL == null ? null : resourceURL.openStream(); } catch(IOException e) { return null; } } /** * This method is similar to {@link #getResourceAsFile(String)} except that it looks for a resource with a given * name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @return an AbstractFile that represents the resource, or null if the resource couldn't be located */ public static AbstractFile getPackageResourceAsFile(Package ppackage, String name) { return getPackageResourceAsFile(ppackage, name, getDefaultClassLoader(), null); } /** * This method is similar to {@link #getResourceAsFile(String, ClassLoader, AbstractFile)} except that it looks for * a resource with a given name in a specific package. * * @param ppackage package serving as a base folder for the resource to retrieve * @param name name of the resource in the package. This is a filename only, not a path. * @param classLoader the ClassLoader used for locating the resource. May not be null. * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * null to look for the resource in the whole class path. * @return an AbstractFile that represents the resource, or null if the resource couldn't be located */ public static AbstractFile getPackageResourceAsFile(Package ppackage, String name, ClassLoader classLoader, AbstractFile rootPackageFile) { return ResourceLoader.getResourceAsFile(getRelativePackagePath(ppackage)+"/"+name, classLoader, rootPackageFile); } /** * Shorthand for {@link #getResourceAsFile(String, ClassLoader, AbstractFile)} called with the * {@link #getDefaultClassLoader() default class loader} and a null root package file. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @return an AbstractFile that represents the resource, or null if the resource couldn't be located */ public static AbstractFile getResourceAsFile(String path) { return getResourceAsFile(removeLeadingSlash(path), getDefaultClassLoader(), null); } /** * Finds the resource with the given path and returns an {@link AbstractFile} that gives full access to it, * or null if the resource couldn't be located. The given ClassLoader is used for locating * the resource. * *

    The given resource path must be forward slash (/) separated. It may or may not start with a * leading forward slash character, this doesn't affect the way it is interpreted. * *

    It is worth noting that this method may be slower than {@link #getResourceAsStream(String)} if * the resource is located inside a JAR file, because the Zip file headers will have to be parsed the first time * the archive is accessed. Therefore, the latter approach should be favored if the file is simply used for * reading the resource. * *

    The rootPackageFile argument can be used to limit the scope of the search to a specific * location (JAR file or directory) in the classpath: resources located outside of this location will not be matched. * This avoids potential ambiguities that can arise if the specified resource path exists in several locations. * If this parameter is null, the resource is looked up in the whole class path. In that case and if * several resources with the specified path exist, the choice of the resource to return is arbitrary. * * @param path forward slash-separated path to the resource to look for, relative to the parent classpath * location (directory or JAR file) that contains it. * @param classLoader the ClassLoader is used for locating the resource * @param rootPackageFile root package location (JAR file or directory) that limits the scope of the search, * null to look for the resource in the whole class path. * @return an AbstractFile that represents the resource, or null if the resource couldn't be located */ public static AbstractFile getResourceAsFile(String path, ClassLoader classLoader, AbstractFile rootPackageFile) { if(classLoader==null) classLoader = getDefaultClassLoader(); path = removeLeadingSlash(path); URL aClassURL = getResourceAsURL(path, classLoader, rootPackageFile); if(aClassURL==null) return null; // no resource under that path if("jar".equals(aClassURL.getProtocol())) { try { return ((AbstractArchiveFile)FileFactory.getFile(getJarFilePath(aClassURL))).getArchiveEntryFile(path); } catch(Exception e) { // Shouldn't normally happen, unless the JAR file is corrupt or cannot be parsed by the file API return null; } } return FileFactory.getFile(getLocalFilePath(aClassURL)); } /** * Returns an {@link AbstractFile} to the root package of the given Class. For example, if the * specified Class is java.lang.Object's, the returned file will be the Java runtime * JAR file, which on most platforms is $JAVA_HOME/lib/jre/rt.jar.
    * The returned file can be used to list or manipulate all resource files contained in a particular classpath's * location, including the .class files. * * @param aClass the class for which to locate the root package. * @return an AbstractFile to the root package of the given Class */ public static AbstractFile getRootPackageAsFile(Class aClass) { ClassLoader classLoader = aClass.getClassLoader(); if(classLoader==null) classLoader = getDefaultClassLoader(); String aClassRelPath = getRelativeClassPath(aClass); URL aClassURL = getResourceAsURL(aClassRelPath, classLoader, null); if(aClassURL==null) return null; // no resource under that path if("jar".equals(aClassURL.getProtocol())) return FileFactory.getFile(getJarFilePath(aClassURL)); String aClassPath = getLocalFilePath(aClassURL); return FileFactory.getFile(aClassPath.substring(0, aClassPath.length()-aClassRelPath.length())); } /** * Returns a path to the given package. The returned path is relative, forward slash-separated and does not end * with a trailing separator. For example, if the package com.mucommander.commons.file is passed, the returned * path will be com/mucommander/commons/file. * * @param ppackage the package for which to return a path * @return a path to the given package */ public static String getRelativePackagePath(Package ppackage) { return ppackage.getName().replace('.', '/'); } /** * Returns a path to the given class. The returned path is relative, forward slash-separated. For example, if the * class com.mucommander.commons.file.AbstractFile is passed, the returned path will be * com/mucommander/commons/file/AbstractFile.class. * * @param cclass the class for which to return a path * @return a path to the given package */ public static String getRelativeClassPath(Class cclass) { return cclass.getName().replace('.', '/')+".class"; } ///////////////////// // Private methods // ///////////////////// /** * Extracts and returns the path to the JAR file from a URL that points to a resource inside a JAR file. * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}. * * @param url a URL that points to a resource inside a JAR file * @return returns the path to the JAR file */ private static String getJarFilePath(URL url) { // URL-decode the path String path = getDecodedURLPath(url); // Here are a couple examples of such paths: // file:/System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Classes/classes.jar!/java/lang/Object.class // http://www.mucommander.com/webstart/nightly/mucommander.jar!/com/mucommander/RuntimeConstants.class int pos = path.indexOf(".jar!"); if(pos==-1) return path; // Strip out the part after ".jar" and normalize the path return normalizeUrlPath(path.substring(0, pos+4)); } /** * Extracts and returns the path to a local file represented by the given URL. * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}. * * @param url a URL that points to a resource inside a JAR file * @return returns the path to the JAR file */ private static String getLocalFilePath(URL url) { // Here's an example of such a path under Windows: // /C:/cygwin/home/Administrator/mucommander/tmp/compile/classes/ // URL-decode the path and normalize it return normalizeUrlPath(getDecodedURLPath(url)); } /** * Removes any leading slash from the given path and returns it. Does nothing if the path does not have a * leading path. * * @param path the path to normalize * @return the path without a leading slash */ private static String removeLeadingSlash(String path) { return PathUtils.removeLeadingSeparator(path, "/"); } /** * Normalizes the specified path issued from a java.net.URL and returns it. * The returned path is in a format that {@link FileFactory} can turn into an {@link AbstractFile}. * * @param path the URL path to normalize * @return the normalized path */ private static String normalizeUrlPath(String path) { // Don't touch http/https URLs if(path.startsWith("http:") || path.startsWith("https:")) return path; // Remove the leading "file:" (if any) if(path.startsWith("file:")) path = path.substring(5); // Under platforms that use root drives (Windows and OS/2), strip out the leading '/' if(LocalFile.hasRootDrives() && path.startsWith("/")) path = removeLeadingSlash(path); // Use the local file separator String separator = LocalFile.SEPARATOR; if(!"/".equals(separator)) path = path.replace("/", separator); return path; } /** * Returns the URL-decoded path of the given java.net.URL. The encoding used for URL-decoding is * UTF-8. * * @param url the URL for which to decode the path * @return the URL-decoded path of the given URL */ private static String getDecodedURLPath(URL url) { try { // Decode the URL's path which may contain URL-encoded characters such as %20 for spaces, or non-ASCII // characters. // Note: the Java API's javadoc doesn't specify which encoding has been used to encoded URL paths. // The only indication is in URLDecoder#decode(String, String) javadoc which says: // "The World Wide Web Consortium Recommendation states that UTF-8 should be used. Not doing so may // introduce incompatibilites." // Also Note that URLDecoder#decode(String) uses System.getProperty("file.encoding") as the default encoding, // using this value has been tested without luck under Mac OS X where the value equals "MacRoman" but // URL are actually encoded in UTF-8. The bottom line is that we blindly use UTF-8 to decode resource URLs. return URLDecoder.decode(url.getPath(), "UTF-8"); } catch(UnsupportedEncodingException e) { // This should never happen, UTF-8 is necessarily supported by the Java runtime return null; } } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/Shell32.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.runtime.OsFamily; import com.sun.jna.Native; import com.sun.jna.win32.W32APIOptions; /** * This class provides access to a static instance of the {@link com.mucommander.commons.file.util.Shell32API} interface, * allowing to invoke selected Shell32 Windows DLL functions. * *

    The Kernel32 DLL and the JNA library (which is used to access native libraries) may not be available on * all OS/CPU architectures: {@link #isAvailable()} can be used to determine that at runtime. * * @see Shell32API * @author Maxence Bernard */ public class Shell32 { /** An instance of the Shell32 DLL */ private static Shell32API INSTANCE; static { if (OsFamily.WINDOWS.isCurrent()) { // Don't even bother if we're not running Windows try { INSTANCE = Native.load("shell32", Shell32API.class, W32APIOptions.UNICODE_OPTIONS); } catch(Throwable e) { // java.lang.UnsatisfiedLinkError is thrown if the CPU architecture is not supported by JNA. INSTANCE = null; } } } /** * Returns true if the Shell32 API can be accessed on the current OS/CPU architecture. * * @return true if the Shell32 API can be accessed on the current OS/CPU architecture */ public static boolean isAvailable() { return INSTANCE != null; } /** * Returns a static instance of the {@link com.mucommander.commons.file.util.Shell32API} interface, allowing to invoke * some Shell32 Windows DLL functions. null will be returned if {@link #isAvailable()} returned * false. * * @return a static instance of the {@link com.mucommander.commons.file.util.Shell32API} interface, null if it * is not available on the current OS/CPU architecture */ public static Shell32API getInstance() { return INSTANCE; } } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/Shell32API.java ================================================ package com.mucommander.commons.file.util; import com.mucommander.commons.runtime.JavaVersion; import com.sun.jna.Pointer; import com.sun.jna.Structure; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.win32.StdCallLibrary; import java.util.Arrays; import java.util.List; /** * Exposes parts of the Windows Shell32 API using the JNA (Java Native Access) library. * The {@link Shell32} class should be used to retrieve an instance of this interface. * * @see Shell32 * @author Maxence Bernard */ public interface Shell32API extends StdCallLibrary { // // Note that the C header "shellapi.h" includes "pshpack1.h", which disables automatic alignment of structure fields. // /** Custom alignment of structures. */ int STRUCTURE_ALIGNMENT = JavaVersion.isAmd64Architecture() ? Structure.ALIGN_DEFAULT : Structure.ALIGN_NONE; // Allowed wFunc values /** Copies the files specified in the pFrom member to the location specified in the pTo member. */ int FO_MOVE = 1; /** Copies the files specified in the pFrom member to the location specified in the pTo member. */ int FO_COPY = 2; /** Deletes the files specified in pFrom. */ int FO_DELETE = 3; /** Renames the file specified in pFrom. You cannot use this flag to rename multiple files with a single * function call. Use FO_MOVE instead. */ int FO_RENAME = 4; // Allowed fFlags values /** Not supported. */ int FOF_MULTIDESTFILES = 1; /** Not supported. */ int FOF_CONFIRMMOUSE = 2; /** Do not display a progress dialog box. */ int FOF_SILENT = 4; /** Give the file being operated on a new name in a move, copy, or rename operation if a file with the target * name already exists. */ int FOF_RENAMEONCOLLISION = 8; /** Respond with "Yes to All" for any dialog box that is displayed. */ int FOF_NOCONFIRMATION = 16; /** Not supported. */ int FOF_WANTMAPPINGHANDLE = 32; /** Preserve Undo information, if possible. If pFrom does not contain fully qualified path and file names, this * flag is ignored. */ int FOF_ALLOWUNDO = 64; /** Not supported. */ int FOF_FILESONLY = 128; /** Display a progress dialog box but do not show the file names. */ int FOF_SIMPLEPROGRESS = 256; /** Do not confirm the creation of a new directory if the operation requires one to be created. */ int FOF_NOCONFIRMMKDIR = 512; /** Do not display a user interface if an error occurs. */ int FOF_NOERRORUI = 1024; /** Not supported. */ int FOF_NOCOPYSECURITYATTRIBS = 2048; /** * This structure contains information that the SHFileOperation function uses to perform file operations. */ class SHFILEOPSTRUCT extends Structure { /** Window handle to the dialog box to display information about the status of the file operation. */ public WinNT.HANDLE hwnd; /** Value that indicates which operation to perform. The following values are accepted: * FO_COPY, FO_DELETE, FO_MOVE or FO_RENAME */ public int wFunc; /** Specifies one or more source file names. These names must be fully qualified paths. Standard * Microsoft MS-DOS wildcards, such as "*", are permitted in the file name position. Although this member * is declared as a null-terminated string, it is used as a buffer to hold multiple file names. Each file * name must be terminated by a single NULL character. An additional NULL character must be appended to the * end of the final name to indicate the end of pFrom. */ public String pFrom; /** Contain the name of the destination file or directory. This parameter must be set to NULL if it is not * used. Like pFrom, the pTo member is also a double-null terminated string and is handled in much the same * way. */ public String pTo; /** Flags that control the file operation (see constant fields for allowed values). */ public short fFlags; /** Not supported. */ public boolean fAnyOperationsAborted; /** Not supported. */ public Pointer pNameMappings; /** String to use as the title of a progress dialog box. This member is used only if fFlags includes the * FOF_SIMPLEPROGRESS flag. */ public String lpszProgressTitle; /** * Encodes pFrom/pTo paths, terminating them with NUL characters as required. * * @param paths a list of paths to encode * @return the encoded path */ public String encodePaths(String[] paths) { StringBuilder encodedPaths = new StringBuilder(); for (String path : paths) { encodedPaths.append(path); encodedPaths.append('\0'); } encodedPaths.append('\0'); return encodedPaths.toString(); } protected List getFieldOrder() { return Arrays.asList("hwnd", "wFunc", "pFrom", "pTo", "fFlags", "fAnyOperationsAborted", "pNameMappings", "lpszProgressTitle"); } } /** * This function can be used to copy, move, rename, or delete a file system object. * *

    Remarks: You should use fully qualified path names with this function. Using it with relative path names * is not thread-safe.
    * When used to delete a file, SHFileOperation attempts to place the deleted file in the Recycle Bin. If you * wish to delete a file and guarantee that it is not placed in the Recycle Bin, use the DeleteFile function. * * @param lpFileOp a SHFILEOPSTRUCT structure that contains information this function needs to carry out the * specified operation. * @return Returns zero if successful, or nonzero otherwise. */ int SHFileOperation(SHFILEOPSTRUCT lpFileOp); //////////////////////////////// // SHEmptyRecycleBin function // //////////////////////////////// /** No dialog box confirming the deletion of the objects will be displayed. */ int SHERB_NOCONFIRMATION = 0x00000001; /** No dialog box indicating the progress will be displayed. */ int SHERB_NOPROGRESSUI = 0x00000002; /** No sound will be played when the operation is complete. */ int SHERB_NOSOUND = 0x00000004; /** * Empties the Recycle Bin on the specified drive. * * @param hwnd A handle to the parent window of any dialog boxes that might be displayed during the operation. * This parameter can be NULL. * @param pszRootPath a null-terminated string of maximum length MAX_PATH that contains the path of the root * drive on which the Recycle Bin is located. This parameter can contain a string formatted with the drive, * folder, and subfolder names, for example c:\windows\system\, etc. It can also contain an empty string or * NULL. If this value is an empty string or NULL, all Recycle Bins on all drives will be emptied. * @param dwFlags a bitwise combination of SHERB_NOCONFIRMATION, SHERB_NOPROGRESSUI and SHERB_NOSOUND. * @return Returns S_OK (0) if successful, or a COM-defined error value otherwise. */ int SHEmptyRecycleBin(WinNT.HANDLE hwnd, String pszRootPath, int dwFlags); //////////////////////////////// // SHQueryRecycleBin function // //////////////////////////////// /** * Contains the size and item count information retrieved by the SHQueryRecycleBin function. */ class SHQUERYRBINFO extends Structure { /** The size of the structure, in bytes. This member must be filled in prior to calling SHQueryRecycleBin. */ public int cbSize = 20; // 1 DWORD + 2 DWORDLONG = 4 + 2*8 = 20 bytes /** The total size of all the objects in the specified Recycle Bin, in bytes. */ public long i64Size; /** The total number of items in the specified Recycle Bin. */ public long i64NumItems; @Override protected List getFieldOrder() { return Arrays.asList("cbSize", "i64Size", "i64NumItems"); } } /** * Retrieves the size of the Recycle Bin and the number of items in it, for a specified drive. * *

    Remarks: With Microsoft Windows 2000, if NULL is passed in the pszRootPath parameter, the function fails * and returns an E_INVALIDARG error code. In earlier versions of the operating system, you can pass an empty * string or NULL. If pszRootPath contains an empty string or NULL, information is retrieved for all * Recycle Bins on all drives. * * @param pszRootPath a null-terminated string of maximum length MAX_PATH to contain the path of the root drive * on which the Recycle Bin is located. This parameter can contain a string formatted with the drive, folder, * and subfolder names (C:\Windows\System...). * @param pSHQueryRBInfo a SHQUERYRBINFO structure that receives the Recycle Bin information. The cbSize member * of the structure must be set to the size of the structure before calling this API. * @return Returns S_OK (0) if successful, or a COM-defined error value otherwise. */ int SHQueryRecycleBin(String pszRootPath, SHQUERYRBINFO pSHQueryRBInfo); } ================================================ FILE: src/main/java/com/mucommander/commons/file/util/SymLinkUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.commons.file.util; import com.mucommander.commons.file.AbstractFile; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; /** * @author Oleg Trifonov * Created on 10/11/14. */ public class SymLinkUtils { /** * Returns symbolic link target value * @param symLink symbolic link path * @return symbolic link target path */ public static String getTargetPath(AbstractFile symLink) { Path path = FileSystems.getDefault().getPath(symLink.getAbsolutePath(), ""); if (!Files.isSymbolicLink(path)) { return symLink.getAbsolutePath(); } try { Path linkTargetPath = Files.readSymbolicLink(path); return linkTargetPath.toString(); } catch (IOException e) { e.printStackTrace(); return symLink.getAbsolutePath(); } } public static boolean createSymlink(AbstractFile symLink, String target) { Path linkPath = FileSystems.getDefault().getPath(symLink.getAbsolutePath(), ""); Path targetPath = FileSystems.getDefault().getPath(target, ""); try { Files.createSymbolicLink(linkPath, targetPath); return true; } catch (IOException e) { e.printStackTrace(); return false; } } /** * * @param symLink symlink path * @param target target file/directory path * @throws IOException if an I/O error occurs. * java.nio.file.AccessDeniedException * java.nio.file.FileAlreadyExistsException */ public static void createSymlink(String symLink, String target) throws IOException { Path linkPath = FileSystems.getDefault().getPath(symLink, ""); Path targetPath = FileSystems.getDefault().getPath(target, ""); Files.createSymbolicLink(linkPath, targetPath); } public static boolean editSymlink(AbstractFile symLink, String target) { Path linkPath = FileSystems.getDefault().getPath(symLink.getAbsolutePath(), ""); Path targetPath = FileSystems.getDefault().getPath(target, ""); try { Files.delete(linkPath); Files.createSymbolicLink(linkPath, targetPath); return true; } catch (IOException e) { e.printStackTrace(); return false; } } } ================================================ FILE: src/main/java/com/mucommander/commons/io/BinaryDetector.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import com.mucommander.commons.io.bom.BOM; import com.mucommander.commons.io.bom.BOMConstants; import com.mucommander.commons.io.bom.BOMInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; /** * This class provides methods to determine whether some data is binary data or text data. * As there is no formal characterization of what binary data really is, this method is an approximation at best * and should not be trusted for anything critical. * *

    The {@link #RECOMMENDED_BYTE_SIZE} field indicates how many bytes should be provided for the detector to be * confident enough. * * @see com.mucommander.commons.io.EncodingDetector * @author Maxence Bernard */ public class BinaryDetector { /** Provides an indication as to the number of bytes that should fed to the detector for it to have enough * confidence. */ private final static int RECOMMENDED_BYTE_SIZE = 1024*16; /** * This method is a shorthand for {@link #guessBinary(byte[], int, int) guessBinary(b, 0, b.length)}. * * @param b the data to analyze * @return true if BinaryDetector thinks that the specified data is binary */ public static boolean guessBinary(byte b[]) { return guessBinary(b, 0, b.length); } /** * Tries and detect whether the given bytes correspond to binary or text data. The specified bytes can typically * be the beginning of a file.
    * This method returns true if it thinks that the bytes correspond to binary data. * * @param b the data to analyze * @param off specifies where to start reading the array * @param len specifies where to stop reading the array * @return true if BinaryDetector thinks that the specified data is binary */ private static boolean guessBinary(byte b[], int off, int len) { // binary .torrent files etc. doesn't contains any 0x0A, 0x0D or 0x00 bytes int x0Acnt = 0; int x0Dcnt = 0; boolean containsZero = false; for (int i = 0; i < len; i++) { byte v = b[i+off]; if (v == 0x0A) { x0Acnt++; if (x0Acnt > 16) { break; } } else if (v == 0x0D) { x0Dcnt++; if (x0Dcnt > 16) { break; } } else if (v == 0) { containsZero = true; } } if (x0Acnt == 0 && x0Dcnt == 0 && len > 1024*32) { return true; } try { // Returns true if any of the bytes are the NUL character. The NUL character is usually not found in a text // file, except for UTF-16 and UTF-32 streams. // So first, we try and look for a BOM (byte-order mark) to see if the stream is UTF-16 or UTF-32 encoded. BOMInputStream bin = new BOMInputStream(new ByteArrayInputStream(b, off, len)); BOM bom = bin.getBOM(); if (bom != null) { if (bom.equals(BOMConstants.UTF16_BE_BOM) || bom.equals(BOMConstants.UTF16_LE_BOM) || bom.equals(BOMConstants.UTF32_BE_BOM) || bom.equals(BOMConstants.UTF32_LE_BOM)) { return false; } } } catch (IOException e) { // Can never happen in practice with a ByteArrayInputStream. } // No BOM, start looking for zeros return containsZero; } /** * Tries and detect whether the given stream contains binary or text data.
    * This method returns true if it thinks that the bytes correspond to binary data. * *

    A maximum of {@link #RECOMMENDED_BYTE_SIZE} will be read from the InputStream. The * stream will not be closed and will not be repositioned after the bytes have been read. It is up to the calling * method to use the InputStream#mark() and InputStream#reset() methods (if supported) * or reopen the stream if needed. * * @param in the stream to analyze * @return true if BinaryDetector thinks that the specified data is binary * @throws IOException if an error occurred while reading the InputStream. */ public static boolean guessBinary(InputStream in) throws IOException { byte[] bytes = new byte[RECOMMENDED_BYTE_SIZE]; return guessBinary(bytes, 0, StreamUtils.readUpTo(in, bytes)); } public static boolean guessBinary(PushbackInputStream in) throws IOException { byte[] bytes = new byte[RECOMMENDED_BYTE_SIZE]; int read = StreamUtils.readUpTo(in, bytes); boolean result = guessBinary(bytes, 0, read); in.unread(bytes, 0, read); return result; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/BlockRandomInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * BlockRandomInputStream is a specialized-yet-still-abstract RandomAccessInputStream that * is geared towards resources that are read block by block, either because of a particular constrain or for performance * reasons. This class typically comes in handy for network resources such as HTTP which have to request a block range * for reading the resource. * *

    Seeking inside the file is implemented transparently by reading a block starting at the seek offset. * If {@link #seek(long)} is called with an offset that is within the current block, no read occurs. * The block size should be carefully chosen as it affects seek performance and thus overall performance greatly: * the larger the block size, the more data is fetched when seeking outside the current block and consequently the * longer it takes to reposition the stream. On the other hand, a larger block size will yield better performance when * reading the resource sequentially, as it lessens the overhead of requesting a particular block. * * @author Maxence Bernard */ public abstract class BlockRandomInputStream extends RandomAccessInputStream { /** Block size, i.e. length of the {@link #block} array */ protected final int blockSize; /** Contains the current file block. Data may end before the array does. */ private final byte block[]; /** Current offset within the block array to the next byte to return */ private int blockOff; /** Length of the current block */ private int blockLen; /** Global offset within the file */ private long offset; /** * Creates a new BlockRandomInputStream using the specified block size. * *

    The block size should be carefully chosen as it affects seek performance and thus overall performance greatly: * the larger the block size, the more data is fetched when seeking outside the current block and consequently the * longer it takes to reposition the stream. On the other hand, a larger block size will yield better performance * when reading the resource sequentially, as it lessens the overhead of requesting a particular block. * * @param blockSize controls the amount of data requested when reading a block */ protected BlockRandomInputStream(int blockSize) { this.blockSize = blockSize; block = new byte[blockSize]; } /** * Returns true if the end of file has been reached. * * @return true if the end of file has been reached. * @throws IOException if an I/O error occurred */ private boolean eofReached() throws IOException { return offset>=getLength(); } /** * Checks if the current buffered block has been read completely (i.e. no more data is available) and if it has, * calls {@link #readBlock(long, byte[], int)} to fetch the next block. * * @throws IOException if an I/O error occurred */ private void checkBuffer() throws IOException { if(blockOff >= blockLen) // True initially readBlock(); } /** * Calls {@link #readBlock(long, byte[], int)} to read a block of up to blockSize, less if the * the end of file is near. * * @throws IOException if an I/O error occurred */ private void readBlock() throws IOException { int len = Math.min((int)(getLength()-offset), blockSize); // update len with the number of bytes actually read len = readBlock(offset, block, len); // Note: these fields won't be updated if an I/O error occurs this.blockOff = 0; this.blockLen = len; } //////////////////////////////////////////// // RandomAccessInputStream implementation // //////////////////////////////////////////// @Override public int read() throws IOException { if(eofReached()) return -1; checkBuffer(); int ret = block[blockOff]; blockOff++; offset ++; return ret; } @Override public int read(byte b[], int off, int len) throws IOException { if(len==0) return 0; if(eofReached()) return -1; checkBuffer(); int nbBytes = Math.min(len, blockLen - blockOff); System.arraycopy(block, blockOff, b, off, nbBytes); blockOff += nbBytes; offset += nbBytes; return nbBytes; } public long getOffset() { return offset; } public void seek(long newOffset) throws IOException { // If the new offset is within the current buffer's range, simply reposition the offsets if(newOffset>=offset && newOffsetfileOffset to fileOffset+blockLen, an returns * the number of bytes that could be read, normally blockLen but can be less. * *

    Note that blockLen may be smaller than {@link #blockSize} if the end of file is near, to prevent * EOF from being reached. In other words, fileOffset+blockLen should theoretically not * exceed the file's length, but this could happen in the unlikely event that the file just shrunk after * {@link #getLength()} was last called. So this method's implementation should handle the case where * EOF is reached prematurely and return the number of bytes that were actually read. * * @param fileOffset global file offset that marks the beginning of the block * @param block the array to fill with data, starting at 0 * @param blockLen number of bytes to read * @return the number of bytes that were actually read, normally blockLen unless * @throws IOException if an I/O error occurred */ protected abstract int readBlock(long fileOffset, byte block[], int blockLen) throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/io/Bounded.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; /** * This interface defines methods that are common to bounded streams, whether they be input streams or output streams. * * @author Maxence Bernard * @see BoundedInputStream * @see BoundedOutputStream */ public interface Bounded { /** * Returns the total number of bytes that are allowed to be processed (read or written) by the stream, * -1 if the stream is not bounded. * * @return the total number of bytes that are allowed to be processed (read or written) by the stream, * -1 if the stream is not bounded. */ long getAllowedBytes(); /** * Returns the total number of bytes that have been processed (read or written) by the stream thus far. * * @return the total number of bytes that have been processed (read or written) by the stream thus far. */ long getProcessedBytes(); /** * Returns the remaining number of bytes that are allowed to be processed (read or written) by the stream, * {@link Long#MAX_VALUE} if this stream is not bounded. * * @return the remaining number of bytes that are allowed to be processed (read or written) by the stream, * {@link Long#MAX_VALUE} if this stream is not bounded. */ long getRemainingBytes(); } ================================================ FILE: src/main/java/com/mucommander/commons/io/BoundedInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; /** * BoundedInputStream is an InputStream that has a set limit to the number of bytes that can be read or * skipped from it. What happens when the limit is reached is controlled at creation time: read and * skip methods can either throw a {@link StreamOutOfBoundException} or simply return -1. * *

    The limit has no effect if it is set to a value that is higher than the number of bytes remaining in the * underlying stream. * *

    This class is particularly useful for reading archives that are a concatenation of files, tarballs for instance. * * @author Maxence Bernard * @see BoundedReader * @see BoundedOutputStream * @see StreamOutOfBoundException */ public class BoundedInputStream extends FilterInputStream implements Bounded { private long totalRead; private long allowedBytes; private boolean throwStreamOutOfBoundException; /** * Creates a new BoundedInputStream over the specified stream, allowing a maximum of * allowedBytes to be read or skipped. If allowedBytes is equal to -1, this * stream is not bounded and acts as a normal stream. * *

    If the throwStreamOutOfBoundException parameter is true, read and * skip methods will throw a {@link StreamOutOfBoundException} when an attempt to read or skip beyond * that limit is made. If false, -1 will be returned. * * @param in the stream to be bounded * @param allowedBytes the total number of bytes that are allowed to be read or skipped, -1 for no limit * @param throwStreamOutOfBoundException true to throw when an attempt to read or skip beyond the byte * limit is made, false to simply return -1 */ public BoundedInputStream(InputStream in, long allowedBytes, boolean throwStreamOutOfBoundException) { super(in); this.allowedBytes = allowedBytes; this.throwStreamOutOfBoundException = throwStreamOutOfBoundException; } /** * Called when an attempt to read out of the stream's bound has been made. This method will either throw a * {@link StreamOutOfBoundException} or return -1, depending on how the BoundedInputStream * was created. * * @return -1 if this BoundedInputStream was configured not to throw a StreamOutOfBoundException * @throws StreamOutOfBoundException if this BoundedInputStream was configured to throw a StreamOutOfBoundException */ protected int handleStreamOutOfBound() throws StreamOutOfBoundException { if(throwStreamOutOfBoundException) throw new StreamOutOfBoundException(allowedBytes); return -1; } //////////////////////////// // Bounded implementation // //////////////////////////// public long getAllowedBytes() { return allowedBytes; } public synchronized long getProcessedBytes() { return totalRead; } public synchronized long getRemainingBytes() { return allowedBytes<=-1?Long.MAX_VALUE:allowedBytes-totalRead; } //////////////////////// // Overridden methods // //////////////////////// @Override public synchronized int read() throws IOException { if(getRemainingBytes()==0) return handleStreamOutOfBound(); int i = in.read(); totalRead++; return i; } @Override public int read(byte b[]) throws IOException { return read(b, 0, b.length); } @Override public synchronized int read(byte b[], int off, int len) throws IOException { int canRead = (int)Math.min(getRemainingBytes(), len); if(canRead==0) return handleStreamOutOfBound(); int nbRead = in.read(b, off, canRead); if(nbRead>0) totalRead += nbRead; return nbRead; } @Override public synchronized long skip(long n) throws IOException { int canSkip = (int)Math.min(getRemainingBytes(), n); if(canSkip==0) return handleStreamOutOfBound(); long nbSkipped = in.skip(canSkip); if(nbSkipped>0) totalRead += nbSkipped; return nbSkipped; } @Override public synchronized int available() throws IOException { return Math.min(in.available(), (int)getRemainingBytes()); } // Methods not implemented /** * Always returns false, even if the underlying stream supports it. * * @return always returns false, even if the underlying stream supports it */ @Override public boolean markSupported() { // Todo: in theory we could support mark/reset return false; } /** * Implemented as a no-op: the call is *not* delegated to the underlying stream. */ @Override public synchronized void mark(int readlimit) { // Todo: in theory we could support mark/reset // No-op } /** * Always throws an IOException: the call is *not* delegated to the underlying stream. */ @Override public synchronized void reset() { // Todo: in theory we could support mark/reset // No-op } } ================================================ FILE: src/main/java/com/mucommander/commons/io/BoundedOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.OutputStream; /** * BoundedOutputStream is an OutputStream that has a set limit to the number of bytes that can * be written to it. When that limit is reached, write methods throw a {@link StreamOutOfBoundException}. * * @author Maxence Bernard * @see BoundedInputStream * @see StreamOutOfBoundException */ public class BoundedOutputStream extends FilteredOutputStream implements Bounded { protected long totalWritten; protected long allowedBytes; /** * Creates a new BoundedInputStream over the specified stream, allowing a maximum of * allowedBytes to be written to it. If allowedBytes is equal to -1, this * stream is not bounded and acts as a normal stream. * * @param out the stream to be bounded * @param allowedBytes the total number of bytes that are allowed to written, -1 for no limit */ public BoundedOutputStream(OutputStream out, long allowedBytes) { super(out); this.allowedBytes = allowedBytes; } //////////////////////////// // Bounded implementation // //////////////////////////// public synchronized long getAllowedBytes() { return allowedBytes; } public synchronized long getProcessedBytes() { return totalWritten; } public synchronized long getRemainingBytes() { return allowedBytes<=-1?Long.MAX_VALUE:allowedBytes-totalWritten; } //////////////////////// // Overridden methods // //////////////////////// @Override public synchronized void write(int b) throws IOException { if(getRemainingBytes()==0) throw new StreamOutOfBoundException(allowedBytes); out.write(b); totalWritten++; } @Override public synchronized void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public synchronized void write(byte[] b, int off, int len) throws IOException { int canWrite = (int)Math.min(getRemainingBytes(), len); if(canWrite==0) throw new StreamOutOfBoundException(allowedBytes); out.write(b, off, canWrite); totalWritten += canWrite; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/BoundedReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.FilterReader; import java.io.IOException; import java.io.Reader; /** * A Reader that has a set limit to the number of characters that can be read from it before the EOF * is reached. The limit has no effect if it is set higher than the number of characters remaining in the * underlying reader. * * @author Maxence Bernard * @see StreamOutOfBoundException */ public class BoundedReader extends FilterReader { private long totalRead; private long allowedCharacters; private IOException outOfBoundException; /** * Equivalent to {@link #BoundedReader(java.io.Reader, long, java.io.IOException)} called with a * null IOException. * * @param reader the reader to limit * @param allowedCharacters the total number of characters this reader allows to be read or skipped, -1 * for no limitation */ public BoundedReader(Reader reader, long allowedCharacters) { this(reader, allowedCharacters, null); } /** * Creates a new BounderReader over the specified reader, allowing a maximum of * allowedCharacters to be read or skipped. If allowedCharacters is equal to -1, * this reader is not bounded and acts as a normal stream. *

    * The specified IOException will be thrown when an attempt to read or skip beyond that is made. * If it is null, read and skip methods will return -1 instead of throwing an * IOException. * * @param reader the reader to bind * @param allowedCharacters the total number of characters this reader allows to be read or skipped, -1 * for no limitation * @param outOfBoundException the IOException to throw when an attempt to read or skip beyond allowedBytes * is made, null to return -1 instead * @see StreamOutOfBoundException */ public BoundedReader(Reader reader, long allowedCharacters, IOException outOfBoundException) { super(reader); this.allowedCharacters = allowedCharacters; this.outOfBoundException = outOfBoundException; } /** * Returns the total number of characters that this reader allows to be read, -1 is this reader is * not bounded. * * @return the total number of characters that this reader allows to be read, -1 is this reader is * not bounded */ public long getAllowedCharacters() { return allowedCharacters; } /** * Returns the total number of characters that have been read or skipped thus far. * * @return the total number of characters that have been read or skipped thus far */ public synchronized long getReadCounter() { return totalRead; } /** * Returns the remaining number of characters that this reader allows to be read, {@link Long#MAX_VALUE} if this * reader is not bounded. * * @return the remaining number of characters that this reader allows to be read, {@link Long#MAX_VALUE} if this * reader is not bounded. */ public synchronized long getRemainingCharacters() { return allowedCharacters<=-1 ? Long.MAX_VALUE : allowedCharacters-totalRead; } /////////////////////////// // Reader implementation // /////////////////////////// @Override public synchronized int read(char[] cbuf, int off, int len) throws IOException { int canRead = (int)Math.min(getRemainingCharacters(), len); if(canRead==0) { if(outOfBoundException==null) return -1; throw outOfBoundException; } int nbRead = in.read(cbuf, off, canRead); if(nbRead>0) totalRead += nbRead; return nbRead; } //////////////////////// // Overridden methods // //////////////////////// @Override public synchronized int read() throws IOException { if(getRemainingCharacters()==0) { if(outOfBoundException==null) return -1; throw outOfBoundException; } int i = in.read(); totalRead++; return i; } @Override public synchronized long skip(long n) throws IOException { int canSkip = (int)Math.min(getRemainingCharacters(), n); if(canSkip==0) { if(outOfBoundException==null) return -1; throw outOfBoundException; } long nbSkipped = in.skip(canSkip); if(nbSkipped>0) totalRead += nbSkipped; return nbSkipped; } /** * Always returns false, even if the underlying reader supports it. * * @return always returns false, even if the underlying reader supports it */ @Override public boolean markSupported() { // Todo: in theory we could support mark/reset return false; } /** * Implemented as a no-op: the call is *not* delegated to the underlying reader. */ @Override public synchronized void mark(int readlimit) { // Todo: in theory we could support mark/reset // No-op } /** * Always throws an IOException: the call is *not* delegated to the underlying reader. */ @Override public synchronized void reset() { // Todo: in theory we could support mark/reset // No-op } } ================================================ FILE: src/main/java/com/mucommander/commons/io/BufferPool.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * This class allows to share and reuse byte buffers to avoid excessive memory allocation and garbage collection. * Methods that use byte buffers and that are called repeatedly will benefit from using this class. * *

    This class works with two types of byte buffers indifferently: *

      *
    • Byte array buffers (byte[])
    • *
    • java.nio.ByteBuffer
    • *
    * *

    * Usage of this class is similar to malloc/free: *

      *
    • Call #get*Buffer(int) to retrieve a buffer instance of a specified size
    • *
    • Use the buffer
    • *
    • When finished using the buffer, call #release*Buffer(byte[]) to make this buffer available for * subsequent calls to #get*Buffer(int). Failing to call this method will prevent the buffer from being * used again and from being garbage-collected.
    • *
    * *

    Note: this class is thread safe and thus can safely be used by concurrent threads. * * @author Maxence Bernard, Nicolas Rinaudo * @see com.mucommander.commons.io.StreamUtils */ public class BufferPool { /** Logger used by this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(BufferPool.class); /** List of BufferContainer instances that wraps available buffers */ private static final List bufferContainers = new ArrayList<>(); private static final ByteArrayFactory BYTE_ARRAY_FACTORY = new ByteArrayFactory(); private static final CharArrayFactory CHAR_ARRAY_FACTORY = new CharArrayFactory(); /** The initial default buffer size */ final static int INITIAL_DEFAULT_BUFFER_SIZE = 65536; /** Size of buffers returned by get*Buffer methods without a size argument */ private static int defaultBufferSize = INITIAL_DEFAULT_BUFFER_SIZE; /** The initial max pool size */ final static long INITIAL_POOL_LIMIT = 10485760; /** Maximum combined size of all pooled buffers, in bytes */ private static long maxPoolSize = INITIAL_POOL_LIMIT; /** Current combined size of all pooled buffers, in bytes */ private static long poolSize; /** * Convenience method that has the same effect as calling {@link #getByteArray(int)} with * a length equal to {@link #getDefaultBufferSize()}. * * @return a byte array with a length of {@link #getDefaultBufferSize()} */ public static synchronized byte[] getByteArray() { return getByteArray(getDefaultBufferSize()); } /** * Returns a byte array of the specified length. This method first checks if a byte array of the specified length * exists in the pool. If one is found, it is removed from the pool and returned. If not, a new instance is created * and returned. * *

    This method won't return the same buffer instance until it has been released with * {@link #releaseByteArray(byte[])}. * *

    This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called * with a {@link com.mucommander.commons.io.BufferPool.ByteArrayFactory} instance. * * @param length length of the byte array * @return a byte array of the specified size */ public static synchronized byte[] getByteArray(int length) { return (byte[])getBuffer(BYTE_ARRAY_FACTORY, length); } /** * Convenience method that has the same effect as calling {@link #getCharArray(int)} with * a length equal to {@link #getDefaultBufferSize()}. * * @return a char array with a length of {@link #getDefaultBufferSize()} */ public static synchronized char[] getCharArray() { return getCharArray(getDefaultBufferSize()); } /** * Returns a char array of the specified length. This method first checks if a char array of the specified length * exists in the pool. If one is found, it is removed from the pool and returned. If not, a new instance is created * and returned. * *

    This method won't return the same buffer instance until it has been released with * {@link #releaseCharArray(char[])}. * *

    This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called * with a {@link com.mucommander.commons.io.BufferPool.CharArrayFactory} instance. * * @param length length of the char array * @return a char array of the specified length */ public static synchronized char[] getCharArray(int length) { return (char[])getBuffer(CHAR_ARRAY_FACTORY, length); } /** * Convenience method that has the same effect as calling {@link #getByteBuffer(int)} with * a buffer capacity of {@link #getDefaultBufferSize()}. * * @return a ByteBuffer with a capacity equal to {@link #getDefaultBufferSize()} */ public static synchronized ByteBuffer getByteBuffer() { return getByteBuffer(getDefaultBufferSize()); } /** * Returns a ByteBuffer of the specified capacity. This method first checks if a ByteBuffer instance of the * specified capacity exists in the pool. If one is found, it is removed from the pool and returned. If not, * a new instance is created and returned. * *

    This method won't return the same buffer instance until it has been released with * {@link #releaseByteBuffer(ByteBuffer)}. * *

    This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called * with a {@link com.mucommander.commons.io.BufferPool.ByteBufferFactory} instance. * @param capacity capacity of the ByteBuffer * @return a ByteBuffer with the specified capacity */ public static synchronized ByteBuffer getByteBuffer(int capacity) { return (ByteBuffer)getBuffer(new ByteBufferFactory(), capacity); } /** * Convenience method that has the same effect as calling {@link #getCharBuffer(int)} with * a buffer capacity of {@link #getDefaultBufferSize()}. * * @return a CharBuffer with a capacity equal to {@link #getDefaultBufferSize()} */ public static synchronized CharBuffer getCharBuffer() { return getCharBuffer(getDefaultBufferSize()); } /** * Returns a CharBuffer of the specified capacity. This method first checks if a CharBuffer instance of the * specified capacity exists in the pool. If one is found, it is removed from the pool and returned. If not, * a new instance is created and returned. * *

    This method won't return the same buffer instance until it has been released with * {@link #releaseCharBuffer(CharBuffer)}. * *

    This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called * with a {@link com.mucommander.commons.io.BufferPool.CharBufferFactory} instance. * @param capacity capacity of the CharBuffer * @return a CharBuffer with the specified capacity */ public static synchronized CharBuffer getCharBuffer(int capacity) { return (CharBuffer)getBuffer(new CharBufferFactory(), capacity); } /** * Convenience method that has the same effect as calling {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory, int)} * with a size equal to {@link #getDefaultBufferSize()}. * * @param factory BufferFactory used to identify the target buffer class and create a new buffer (if necessary) * @return a buffer with a size equal to {@link #getDefaultBufferSize()} */ public static synchronized Object getBuffer(BufferFactory factory) { return getBuffer(factory, getDefaultBufferSize()); } /** * Returns a byte array of the specified size. This method first checks if a buffer the same size as the specified * one and a class compatible with the specified factory exists in the pool. If one is found, it is removed from the * pool and returned. * If not, a new instance is created and returned using {@link BufferFactory#newBuffer(int)}. * *

    This method won't return the same buffer instance until it has been released with * {@link #releaseBuffer(Object, BufferFactory)}. * * @param factory BufferFactory used to identify the target buffer class and create a new buffer (if necessary) * @param size size of the buffer * @return a buffer of the specified size */ public static synchronized Object getBuffer(BufferFactory factory, int size) { // Looks for a buffer container in the pool that matches the specified size and buffer class. Iterator it = bufferContainers.iterator(); while (it.hasNext()) { BufferContainer bufferContainer = it.next(); Object buffer = bufferContainer.getBuffer(); // Caution: mind the difference between BufferContainer#getLength() and BufferContainer#getSize() if (bufferContainer.getLength() == size && (factory.matchesBufferClass(buffer.getClass()))) { it.remove(); //bufferContainers.removeElementAt(i); poolSize -= bufferContainer.getSize(); return buffer; } } LOGGER.trace("Creating new buffer with {} size={}", factory, size); // No buffer with the same class and size found in the pool, create a new one and return it return factory.newBuffer(size); } /** * Makes the given buffer available for further calls to {@link #getByteArray(int)} with the same buffer length. * Returns true if the buffer was added to the pool, false if the buffer was already in * the pool. * *

    After calling this method, the given buffer instance must not be used, otherwise it could get * corrupted if other threads were using it. * * @param buffer the buffer instance to make available for further use * @return true if the buffer was added to the pool, false if the buffer was already in the pool * @throws IllegalArgumentException if specified buffer is null */ public static synchronized boolean releaseByteArray(byte[] buffer) { return releaseBuffer(buffer, BYTE_ARRAY_FACTORY); } /** * Makes the given buffer available for further calls to {@link #getCharArray(int)} with the same buffer length. * Returns true if the buffer was added to the pool, false if the buffer was already in * the pool. * *

    After calling this method, the given buffer instance must not be used, otherwise it could get * corrupted if other threads were using it. * * @param buffer the buffer instance to make available for further use * @return true if the buffer was added to the pool, false if the buffer was already in the pool * @throws IllegalArgumentException if specified buffer is null */ public static synchronized boolean releaseCharArray(char[] buffer) { return releaseBuffer(buffer, CHAR_ARRAY_FACTORY); } /** * Makes the given buffer available for further calls to {@link #getByteBuffer(int)} with the same buffer capacity. * Returns true if the buffer was added to the pool, false if the buffer was already in * the pool. * *

    After calling this method, the given buffer instance must not be used, otherwise it could get * corrupted if other threads were using it. * * @param buffer the buffer instance to make available for further use * @return true if the buffer was added to the pool, false if the buffer was already in the pool * @throws IllegalArgumentException if specified buffer is null */ public static synchronized boolean releaseByteBuffer(ByteBuffer buffer) { return releaseBuffer(buffer, new ByteBufferFactory()); } /** * Makes the given buffer available for further calls to {@link #getCharBuffer(int)} with the same buffer capacity. * Returns true if the buffer was added to the pool, false if the buffer was already in * the pool. * *

    After calling this method, the given buffer instance must not be used, otherwise it could get * corrupted if other threads were using it. * * @param buffer the buffer instance to make available for further use * @return true if the buffer was added to the pool, false if the buffer was already in the pool * @throws IllegalArgumentException if specified buffer is null */ public static synchronized boolean releaseCharBuffer(CharBuffer buffer) { return releaseBuffer(buffer, new CharBufferFactory()); } /** * Makes the given buffer available for further calls to {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} with the same buffer * size and factory. * Returns true if the buffer was added to the pool, false if the buffer was already in * the pool or the pool size limit has been reached. * *

    After calling this method, the given buffer instance must not be used, otherwise it could get * corrupted if other threads were using it. * * @param buffer the buffer instance to make available for further use * @param factory the BufferFactory that was used to create the buffer * @return true if the buffer was added to the pool, false if the buffer was already in the pool or the pool size limit has been reached * @throws IllegalArgumentException if specified buffer is null */ public static synchronized boolean releaseBuffer(Object buffer, BufferFactory factory) { if (buffer == null) { throw new IllegalArgumentException("specified buffer is null"); } BufferContainer bufferContainer = factory.newBufferContainer(buffer); if (bufferContainers.contains(bufferContainer)) { LOGGER.info("Warning: specified buffer is already in the pool: {}", buffer); return false; } long bufferSize = bufferContainer.getSize(); // size in bytes (!= length) if (maxPoolSize >= 0 && poolSize + bufferSize > maxPoolSize) { LOGGER.info("Warning: maximum pool size reached, buffer not added to the pool of type {}. Enable trace to get the buffer.", buffer.getClass()); LOGGER.trace("Warning: maximum pool size reached, buffer not added to the pool of type {} : {}", buffer.getClass(), buffer); return false; } bufferContainers.add(bufferContainer); poolSize += bufferSize; return true; } /** * Returns true if the specified buffer is currently in the pool. * *

    Note that it is not necessary (and thus not recommended for performance reasons) to call this method before * calling release*Buffer as it already performs this test before adding a buffer to the pool. * * @param buffer the buffer to look for in the pool * @param factory the BufferFactory that was used to create the buffer * @return true if the specified buffer is already in the pool */ public static boolean containsBuffer(Object buffer, BufferFactory factory) { return bufferContainers.contains(factory.newBufferContainer(buffer)); } /** * Returns the number of buffers that currently are in the pool. This method is provided for debugging * purposes only. * * @return the number of buffers currently in the pool */ public static int getBufferCount() { return bufferContainers.size(); } /** * Returns the number of buffers that currently are in the pool and whose Class are the same as the specified * factory's. This method is provided for debugging purposes only. * * @param factory the BufferFactory * @return the number of buffers currently in the pool */ public static int getBufferCount(BufferFactory factory) { int count = 0; for (BufferContainer bufferContainer : bufferContainers) { if (factory.matchesBufferClass(bufferContainer.getBuffer().getClass())) { count ++; } } return count; } /** * Returns the default size of buffers returned by get*Buffer methods without a size * argument. * *

    The default buffer size is initially set to {@link #INITIAL_DEFAULT_BUFFER_SIZE}. * * @return the default size of buffers returned by get*Buffer methods without a size argument */ public static int getDefaultBufferSize() { return defaultBufferSize; } /** * Sets the default size of buffers returned by get*Buffer methods without a size argument. * * @param bufferSize the new buffer size */ public static synchronized void setDefaultBufferSize(int bufferSize) { BufferPool.defaultBufferSize = bufferSize; } /** * Returns the combined size in bytes of all buffers that are currently in the pool. * * @return the combined size in bytes of all buffers that are currenty in the pool */ public static long getPoolSize() { return poolSize; } /** * Returns the maximum combined size in bytes for all buffers in the pool, -1 for no limit. * Before adding a buffer to the pool, release*Buffer methods ensure that the pool size will * not be exceeded. If and only if that is the case, the buffer is added to the pool. * *

    The max pool size is initially set to {@link #INITIAL_POOL_LIMIT}. * * @return the maximum combined size in bytes for all buffers in the pool */ public static long getMaxPoolSize() { return maxPoolSize; } /** * Sets the maximum combined size in bytes for all buffers in the pool, -1 for no limit. * Before adding a buffer to the pool, release*Buffer methods ensure that the pool size will * not be exceeded. If and only if that is the case, the buffer is added to the pool. * * @param maxPoolSize the maximum combined size in bytes for all buffers in the pool */ public static synchronized void setMaxPoolSize(long maxPoolSize) { BufferPool.maxPoolSize = maxPoolSize; } /** * Wraps a buffer instance and provides information about the wrapped buffer. */ public static abstract class BufferContainer { /** The wrapped buffer instance */ protected Object buffer; /** * Creates a new BufferContainer that wraps the given buffer. * * @param buffer the buffer instance to wrap */ protected BufferContainer(Object buffer) { this.buffer = buffer; } /** * Returns the wrapped buffer instance. * * @return the wrapped buffer instance */ protected Object getBuffer() { return buffer; } /** * Implements a shallow equal comparison. */ public boolean equals(Object o) { // Note: this method is used by Vector.contains() return (o instanceof BufferContainer) && buffer == ((BufferContainer)o).buffer; } /** * Returns the length of the wrapped buffer instance. * * @return the length of the wrapped buffer instance */ protected abstract int getLength(); /** * Returns the size of the wrapped buffer instance, expressed in bytes. * * @return the size of the wrapped buffer instance, expressed in bytes */ protected abstract int getSize(); } /** * A BufferFactory is responsible for creating buffer and {@link BufferContainer} instances, and for returning the buffer * Class. The Class returned by {@link #getBufferClass()} may be a superclass or superinterface of the actual * objects returned by {@link #newBuffer(int)}. */ public static abstract class BufferFactory { /** * Returns true if the class returned by {@link #getBufferClass()} is equal or a * superclass/superinterface of the specified buffer class. * * @param bufferClass the buffer Class to test * @return true if the class returned by #getBufferClass() is equal or a superclass/superinterface * of the specified buffer class */ public boolean matchesBufferClass(Class bufferClass) { return getBufferClass().isAssignableFrom(bufferClass); } /** * Creates and returns a buffer instance of the specified size. * * @param size size of the buffer to create * @return a buffer instance of the specified size */ public abstract Object newBuffer(int size); /** * Creates and returns a {@link BufferContainer} for the specified buffer instance. * * @param buffer the buffer to wrap in a BufferContainer * @return returns a BufferContainer for the specified buffer instance */ public abstract BufferContainer newBufferContainer(Object buffer); /** * Returns the Class of buffer instances this factory creates. * * @return the Class of buffer instances this factory creates */ public abstract Class getBufferClass(); } /** * This class is a {@link BufferFactory} implementation for byte array (byte[]) buffers. */ public static class ByteArrayFactory extends BufferFactory { @Override public Object newBuffer(int size) { return new byte[size]; } @Override public BufferContainer newBufferContainer(Object buffer) { return new BufferContainer(buffer) { @Override protected int getLength() { return ((byte[])buffer).length; } @Override protected int getSize() { return getLength(); } }; } @Override public Class getBufferClass() { return byte[].class; } } /** * This class is a {@link BufferFactory} implementation for char array (char[]) buffers. */ public static class CharArrayFactory extends BufferFactory { @Override public Object newBuffer(int size) { return new char[size]; } @Override public BufferContainer newBufferContainer(Object buffer) { return new BufferContainer(buffer) { @Override protected int getLength() { return ((char[])buffer).length; } @Override protected int getSize() { return 2*getLength(); } }; } @Override public Class getBufferClass() { return char[].class; } } /** * This class is a {@link BufferFactory} implementation for java.nio.ByteBuffer buffers. * ByteBuffer instances created by {@link #newBuffer(int)} are direct ; the actually Class of those instances may be actually * be java.nio.DirectByteBuffer and not java.nio.ByteBuffer as returned by * {@link #getBufferClass()}. */ public static class ByteBufferFactory extends BufferFactory { @Override public Object newBuffer(int size) { // Note: the returned instance is actually a java.nio.DirectByteBuffer, this is why it's important to // compare classes using Class#isAssignableFrom(Class) return ByteBuffer.allocateDirect(size); } @Override public BufferContainer newBufferContainer(Object buffer) { return new BufferContainer(buffer) { @Override protected int getLength() { return ((ByteBuffer)buffer).capacity(); } @Override protected int getSize() { return getLength(); } }; } @Override public Class getBufferClass() { return ByteBuffer.class; } } /** * This class is a {@link BufferFactory} implementation for java.nio.CharBuffer buffers. */ public static class CharBufferFactory extends BufferFactory { @Override public Object newBuffer(int size) { return CharBuffer.allocate(size); } @Override public BufferContainer newBufferContainer(Object buffer) { return new BufferContainer(buffer) { @Override protected int getLength() { return ((CharBuffer)buffer).capacity(); } @Override protected int getSize() { return 2*getLength(); } }; } @Override public Class getBufferClass() { return CharBuffer.class; } } public static synchronized void releaseAll() { bufferContainers.clear(); poolSize = 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/BufferedRandomOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * BufferedRandomOutputStream is a buffered output stream for {@link RandomAccessOutputStream} which, unlike a regular * java.io.BufferedOutputStream, makes it safe to seek in the underlying RandomAccessOutputStream. * *

    This class uses {@link BufferPool} to create the internal buffer, to avoid excessive memory allocation and * garbage collection. The buffer is released when this stream is closed. * * @author Maxence Bernard */ public class BufferedRandomOutputStream extends RandomAccessOutputStream { /** * The underlying random access output stream */ private final RandomAccessOutputStream raos; /** * The buffer where written bytes are accumulated before being sent to the underlying output stream */ private byte[] buffer; /** * The current number of bytes waiting to be flushed to the underlying output stream */ private int count; /** * The default buffer size if none is specified */ public final static int DEFAULT_BUFFER_SIZE = 65536; /** * Creates a new BufferedRandomOutputStream on top of the given {@link RandomAccessOutputStream}. * An internal buffer of {@link #DEFAULT_BUFFER_SIZE} bytes is created. * * @param raos the underlying RandomAccessOutputStream used by this buffered output stream */ public BufferedRandomOutputStream(RandomAccessOutputStream raos) { this(raos, DEFAULT_BUFFER_SIZE); } /** * Creates a new BufferedRandomOutputStream on top of the given {@link RandomAccessOutputStream}. * An internal buffer of the specified size is created. * * @param raos the underlying RandomAccessOutputStream used by this buffered output stream * @param size size of the buffer in bytes */ public BufferedRandomOutputStream(RandomAccessOutputStream raos, int size) { this.raos = raos; this.buffer = BufferPool.getByteArray(size); } /** * Flushes the internal buffer. * * @throws IOException if an error occurs */ private void flushBuffer() throws IOException { if (count > 0) { raos.write(buffer, 0, count); count = 0; } } /** * Writes the specified byte to this buffered output stream. * * @param b the byte to be written * @throws IOException if an I/O error occurs */ @Override public synchronized void write(int b) throws IOException { if (count >= buffer.length) flushBuffer(); buffer[count++] = (byte) b; } /** * Writes the specified byte array to this buffered output stream. * * @param b the bytes to be written * @throws IOException if an I/O error occurs */ @Override public synchronized void write(byte[] b) throws IOException { write(b, 0, b.length); } /** * Writes len bytes from the specified byte array starting at offset off to this * buffered output stream. * *

    Usually this method stores bytes from the given array into this * stream's buffer, flushing the buffer to the underlying output stream as * needed. However, if the requested data length is equal or larger than this stream's * buffer, then this method will flush the buffer and write the * bytes directly to the underlying output stream. Thus, redundant * RandomBufferedOutputStreams will not copy data unnecessarily. * * @param b the data. * @param off the start offset in the data. * @param len the number of bytes to write. * @throws IOException if an I/O error occurs. */ @Override public synchronized void write(byte[] b, int off, int len) throws IOException { if (len >= buffer.length) { /* If the request length exceeds the size of the output buffer, flush the output buffer and then write the data directly. In this way buffered streams will cascade harmlessly. */ flushBuffer(); raos.write(b, off, len); return; } if (len > buffer.length - count) flushBuffer(); System.arraycopy(b, off, buffer, count, len); count += len; } /** * Flushes this buffered output stream. This forces any buffered * output bytes to be written out to the underlying output stream. * * @throws IOException if an I/O error occurs. */ @Override public synchronized void flush() throws IOException { flushBuffer(); raos.flush(); } public synchronized long getOffset() throws IOException { // Add the buffered byte count return raos.getOffset() + count; } public synchronized void seek(long offset) throws IOException { // Flush any buffered bytes before seeking, otherwise buffered bytes would be written at the wrong offset flush(); raos.seek(offset); } public synchronized long getLength() throws IOException { // Anticipate if the file is to be expanded by the bytes awaiting in the buffer return Math.max(raos.getLength(), getOffset()); } @Override public synchronized void setLength(long newLength) throws IOException { // Flush before changing the file's length, otherwise the behavior of setLength() would be modified, especially // when truncating the file flush(); raos.setLength(newLength); } /** * This method is overridden to release the internal buffer when this stream is closed. */ @Override public synchronized void close() throws IOException { if (buffer != null) { // buffer is null if close() was already called try { flush(); } catch (IOException ignore) { } // Release the buffer BufferPool.releaseByteArray(buffer); buffer = null; } raos.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/ByteCounter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; /** * Contains a number of bytes which have been read/written from/to a {@link CounterInputStream}/{@link CounterOutputStream}. * *

    Provided methods allow to read the the byte count, add a number bytes to it or reset it (make it zero). * *

    This class is thread safe, ensuring that the counter is always in a consistent state. * * @see com.mucommander.commons.io.CounterInputStream * @see com.mucommander.commons.io.CounterOutputStream * @author Maxence Bernard */ public class ByteCounter { /** Byte count */ private long count; /** Byte counter to add to the value returned by {@link #getByteCount()} */ private ByteCounter addedCounter; /** * Creates a new ByteCounter with an initial byte count equal to zero. */ public ByteCounter() { } /** * Creates a new ByteCounter with an initial byte count equal to zero and using the given ByteCounter. * *

    The value returned by {@link #getByteCount()} will be the sum of the internal byte count and the one from * the specified ByteCounter, as returned by its {@link #getByteCount()} method. Resetting this ByteCounter's value * will only affect the internal byte count and not the one from the specified ByteCounter. */ public ByteCounter(ByteCounter counter) { this.addedCounter = counter; } /** * Return the number of bytes which have been accounted for. * @return the number of bytes which have been accounted for */ public synchronized long getByteCount() { if (addedCounter != null) { return count + addedCounter.getByteCount(); } return this.count; } /** * Increases the byte counter by the provided number of bytes. If the specified number is negative, * the byte counter will be left unchanged (won't be decreased). * * @param nbBytes number of bytes to add to the byte counter, will be ignored if negative */ public synchronized void add(long nbBytes) { if (nbBytes > 0) { this.count += nbBytes; } } /** * Increases the byte counter by the number of bytes contained in the specified counter (as returned by its * {@link #getByteCount()} method) and resets its byte counter after (if specified). * * @param counter the Bytecounter to add to this one, and reset after (if specified). * @param resetAfter if true, the specified counter will be reset after its byte count has been added to this ByteCounter */ public synchronized void add(ByteCounter counter, boolean resetAfter) { // Hold a lock on the provided counter to make sure that it is not modified or accessed // while this operation is carried out synchronized(counter) { add(counter.getByteCount()); if(resetAfter) counter.reset(); } } /** * Resets the byte counter (make it zero). */ public synchronized void reset() { this.count = 0; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/ByteUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; /** * This class provides convenience static methods that operate on bits and bytes. * * @author Maxence Bernard */ public class ByteUtils { /** * Sets/unsets a bit in the given integer. * * @param i the permission int * @param bit the bit to set * @param enabled true to enable the bit, false to disable it * @return the modified permission int */ public static int setBit(int i, int bit, boolean enabled) { if (enabled) i |= bit; else i &= ~bit; return i; } /** * Returns an hexadecimal string representation of the given byte array, where each byte is represented by two * hexadecimal characters and padded with a zero if its value is comprised between 0 and 15 (inclusive). * As an example, this method will return "6d75636f0a" when called with the byte array {109, 117, 99, 111, 10}. * * @param bytes the array of bytes for which to get an hexadecimal string representation * @return an hexadecimal string representation of the given byte array */ public static String toHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte aByte : bytes) { String hexByte = Integer.toHexString(aByte & 0xFF); if (hexByte.length() == 1) sb.append('0'); sb.append(hexByte); } return sb.toString(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/ChecksumInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.InputStream; import java.security.DigestInputStream; import java.security.MessageDigest; /** * This class extends java.security.DigestInputStream and adds convenience methods that return the * digest/checksum expressed in various forms. * * @see com.mucommander.commons.io.ChecksumOutputStream * @author Maxence Bernard */ public class ChecksumInputStream extends DigestInputStream { public ChecksumInputStream(InputStream stream, MessageDigest digest) { super(stream, digest); } /** * Returns this stream's digest, expressed as a byte array. * * @return this stream's digest, expressed as a byte array */ public byte[] getChecksumBytes() { return getMessageDigest().digest(); } /** * Returns this stream's digest, expressed as an hexadecimal string. * * @return this stream's digest, expressed as an hexadecimal string */ public String getChecksumString() { return ByteUtils.toHexString(getChecksumBytes()); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/ChecksumOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.OutputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; /** * This class extends java.security.DigestOutputStream and adds convenience methods that return the * digest/checksum expressed in various forms. * * @see com.mucommander.commons.io.ChecksumInputStream * @author Maxence Bernard */ public class ChecksumOutputStream extends DigestOutputStream { public ChecksumOutputStream(OutputStream stream, MessageDigest digest) { super(stream, digest); } /** * Returns this stream's digest, expressed as a byte array. * * @return this stream's digest, expressed as a byte array */ public byte[] getChecksumBytes() { return getMessageDigest().digest(); } /** * Returns this stream's digest, expressed as an hexadecimal string. * * @return this stream's digest, expressed as an hexadecimal string */ public String getChecksumString() { return ByteUtils.toHexString(getChecksumBytes()); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/CounterInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.InputStream; /** * An InputStream that keeps track of the number of bytes that have been read from it. Bytes that are skipped (using * {@link #skip(long)} are by default accounted for, {@link #setCountSkippedBytes(boolean)} can be used to change this. * *

    The actual number of bytes can be retrieved from the {@link ByteCounter} instance returned by {@link #getCounter()}. * The {@link #CounterInputStream(InputStream, ByteCounter)} constructor can be used to specify an existing * ByteCounter instance instead of creating a new one. The ByteCounter will always remain accessible, even * after this stream has been closed. * * @see ByteCounter * @author Maxence Bernard */ public class CounterInputStream extends InputStream { /** Underlying InputStream */ private final InputStream in; /** Byte counter */ private final ByteCounter counter; /** Should skipped bytes be accounted for ? (enabled by default) */ private boolean countSkippedBytes = true; /** * Creates a new CounterInputStream using the specified InputStream. A new {@link ByteCounter} will be created. * * @param in the underlying InputStream the data will be read from */ public CounterInputStream(InputStream in) { this.in = in; this.counter = new ByteCounter(); } /** * Creates a new CounterInputStream using the specified InputStream and {@link ByteCounter}. * The provided ByteCounter will NOT be reset, whatever value it contains will be kept. * * @param in the underlying InputStream the data will be read from */ public CounterInputStream(InputStream in, ByteCounter counter) { this.in = in; this.counter = counter; } /** * Returns the ByteCounter that holds the number of bytes that have been read (and optionally skipped) from this * InputStream. */ public ByteCounter getCounter() { return this.counter; } /** * Specifies whether skipped bytes (using {@link #skip(long)} should be accounted for. * This is by default enabled, bytes that are skipped are added to the ByteCounter. * * @param countSkippedBytes if true, skipped bytes will be accounted for, the ByteCounter will be increased * by the number of skipped bytes */ public void setCountSkippedBytes(boolean countSkippedBytes) { this.countSkippedBytes = countSkippedBytes; } /** * Returns true if skipped bytes (using {@link #skip(long)} are accounted for. * This is by default enabled, bytes that are skipped are added to the ByteCounter. */ public boolean getCountSkippedBytes() { return countSkippedBytes; } @Override public int read() throws IOException { int i = in.read(); if ( i > 0) { counter.add(1); } return i; } @Override public int read(byte b[]) throws IOException { int nbRead = in.read(b); if (nbRead > 0) { counter.add(nbRead); } return nbRead; } @Override public int read(byte b[], int off, int len) throws IOException { int nbRead = in.read(b, off, len); if (nbRead > 0) { counter.add(nbRead); } return nbRead; } @Override public long skip(long n) throws IOException { long nbSkipped = in.skip(n); // Count skipped bytes only if this has been enabled if (countSkippedBytes && nbSkipped > 0) { counter.add(nbSkipped); } return nbSkipped; } @Override public int available() throws IOException { return in.available(); } @Override public void close() throws IOException { in.close(); } @Override public void mark(int readLimit) { in.mark(readLimit); } @Override public boolean markSupported() { return in.markSupported(); } @Override public void reset() throws IOException { in.reset(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/CounterOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.OutputStream; /** * An OutputStream that keeps track of the number of bytes that have been written to it. * *

    The actual number of bytes can be retrieved from the {@link ByteCounter} instance returned by {@link #getCounter()}. * The {@link #CounterOutputStream(OutputStream, ByteCounter)} constructor can be used to specify an existing * ByteCounter instance instead of creating a new one. The ByteCounter will always remain accessible, even * after this stream has been closed. * * @see ByteCounter * @author Maxence Bernard */ public class CounterOutputStream extends OutputStream { /** Underlying OutputStream */ private final OutputStream out; /** Byte counter */ private final ByteCounter counter; /** * Creates a new CounterOutputStream using the specified OutputStream. A new {@link ByteCounter} will be created. * * @param out the underlying OutputStream the data will be written to */ public CounterOutputStream(OutputStream out) { this.out = out; this.counter = new ByteCounter(); } /** * Creates a new CounterOutputStream using the specified OutputStream and {@link ByteCounter}. * The provided ByteCounter will NOT be reset, whatever value it contains will be kept. * * @param out the underlying OutputStream the data will be written to */ public CounterOutputStream(OutputStream out, ByteCounter counter) { this.out = out; this.counter = counter; } /** * Returns the ByteCounter that holds the number of bytes that have been written to this OutputStream. * @return the ByteCounter that holds the number of bytes that have been written to this OutputStream */ public ByteCounter getCounter() { return this.counter; } @Override public void write(int b) throws IOException { out.write(b); counter.add(1); } @Override public void write(byte b[]) throws IOException { out.write(b); counter.add(b.length); } @Override public void write(byte b[], int off, int len) throws IOException { out.write(b, off, len); counter.add(len); } @Override public void flush() throws IOException { out.flush(); } @Override public void close() throws IOException { out.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/EncodingDetector.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import com.ibm.icu.text.CharsetDetector; import com.ibm.icu.text.CharsetMatch; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; /** * This class allows to guess at an encoding in which an array of bytes is encoded. Detecting an encoding is by no means * an accurate operation, as it relies on heuristics that are imprecise by nature. However, accuracy improves with the * quantity of bytes that is supplied: a small amount of data (say 10 bytes) has little chance of being guessed * correctly, whereas a larger amount of data (say 1000 bytes) is likely to provide a good result. On the other hand, * providing a very large amount of data will only marginally improve the accuracy, and is not worth the extra effort * considering that encoding detection is a costly operation which involves many comparisons per byte. * The {@link #MAX_RECOMMENDED_BYTE_SIZE} field controls that threshold: if a supplied byte array is larger than this * value, the additional bytes will not be processed by the detectEncoding methods. Therefore, this value * should be taken into account if bytes are to be fetched specifically for the purpose of detecting the encoding. * *

    * EncodingDetector uses ICU4J under the hood. Here's a list of encodings that can currently be detected: *

     * UTF-8
     * UTF-16BE
     * UTF-16LE
     * UTF-32BE
     * UTF-32LE
     * Shift_JIS
     * ISO-2022-JP
     * ISO-2022-CN
     * ISO-2022-KR
     * GB18030
     * EUC-JP
     * EUC-KR
     * Big5
     * ISO-8859-1
     * ISO-8859-2
     * ISO-8859-5
     * ISO-8859-6
     * ISO-8859-7
     * ISO-8859-8
     * windows-1251
     * windows-1256
     * KOI8-R
     * ISO-8859-9
     * 
    * * @author Maxence Bernard, Nicolas Rinaudo * @see ICU charset detection accuracy */ public class EncodingDetector { private static final Logger LOGGER = LoggerFactory.getLogger(EncodingDetector.class); /** Maximum number of bytes that the detectEncoding methods will process. *

    * See http://philip.html5.org/data/charsets.html and http://philip.html5.org/data/encoding-detection.svg * for why 4096 is the recommended size. * * Comment by Trol: 4096 is wrong value here. I have a lot of times situation when the encoding doesn't detected correctly * in case of source file with UTF-8 comments at the end */ public final static int MAX_RECOMMENDED_BYTE_SIZE = 1024*16; /** * This method is a shorthand for {@link #detectEncoding(byte[], int, int) detectEncoding(b, 0, b.length)}. * * @param bytes the bytes for which to detect the encoding * @return the best guess at the character encoding, null if there is none (not enough data or confidence) */ public static String detectEncoding(byte[] bytes) { return detectEncoding(bytes, 0, bytes.length); } /** * Try and detect the character encoding in which the given bytes are encoded, and returns the best guess or * null if there is none (not enough data or confidence). * Note that the returned character encoding may not be available on the Java runtime -- use * java.nio.Charset#isSupported(String) to determine if it is available. * *

    A maximum of {@link #MAX_RECOMMENDED_BYTE_SIZE} will be read from the array. If the array is larger than this * value, all further bytes will be ignored. * * @param bytes the bytes for which to detect the encoding * @param off the array offset at which the data to process starts * @param len length of the data in the array * @return the best guess at the encoding, null if there is none (not enough data or confidence) */ public static String detectEncoding(byte[] bytes, int off, int len) { // The current ICU CharsetDetector class will throw an ArrayIndexOutOfBoundsException exception if the // supplied array is less than 4 bytes long. In that case, return null. if (len < 4) { return null; } // Trim the array if it is too long, detecting the charset is an expensive operation and past a certain point, // having more bytes won't help any further if (len > MAX_RECOMMENDED_BYTE_SIZE) { len = MAX_RECOMMENDED_BYTE_SIZE; } // CharsetDetector will process the array fully, so if the data does not start at 0 or ends before the array's // length, create a new array that fits the data exactly if (off > 0 || len < bytes.length) { byte[] tmp = new byte[len]; System.arraycopy(bytes, off, tmp, 0, len); bytes = tmp; } CharsetDetector cd = new CharsetDetector(); cd.setText(bytes); CharsetMatch[] matches = cd.detectAll(); CharsetMatch cm = getBestCharsetMatch(matches); // Debug info LOGGER.trace("bestMatch getName()={}, getConfidence()={}", (cm == null ? "null" : cm.getName()), (cm == null ? "null" : Integer.toString(cm.getConfidence()))); return cm == null ? null : cm.getName(); } @Nullable private static CharsetMatch getBestCharsetMatch(CharsetMatch[] matches) { if (matches == null || matches.length == 0) { return null; } CharsetMatch cm = matches[0]; // detect win-1251 for case latin + cyrillic String detectedName = cm.getName().toLowerCase(); if (detectedName.startsWith("iso-8859-")) { for (CharsetMatch match : matches) { if (match.getName().toLowerCase().startsWith("windows-1251")) { return match; } else if (match.getName().toLowerCase().startsWith("utf-8")) { return match; } } } return cm; } /** * Try and detect the character encoding in which the bytes contained by the given InputStream are * encoded, and returns the best guess or null if there is none (not enough data or confidence). * Note that the returned character encoding may or may not be available on the Java runtime -- use * java.nio.Charset#isSupported(String) to determine if it is available. * *

    A maximum of {@link #MAX_RECOMMENDED_BYTE_SIZE} will be read from the InputStream. The * stream will not be closed and will not be repositioned after the bytes have been read. It is up to the calling * method to use the InputStream#mark() and InputStream#reset() methods (if supported) * or reopen the stream if needed. * * @param in the InputStream that supplies the bytes * @return the best guess at the character encoding, null if there is none (not enough data or confidence) * @throws IOException if an error occurred while reading the stream */ public static String detectEncoding(InputStream in) throws IOException { byte[] buf = BufferPool.getByteArray(MAX_RECOMMENDED_BYTE_SIZE); try { return detectEncoding(buf, 0, StreamUtils.readUpTo(in, buf)); } finally { BufferPool.releaseByteArray(buf); } } public static String detectEncoding(PushbackInputStream in) throws IOException { byte[] buf = BufferPool.getByteArray(MAX_RECOMMENDED_BYTE_SIZE); try { int readBytes = StreamUtils.readUpTo(in, buf); String result = detectEncoding(buf, 0, readBytes); in.unread(buf, 0, readBytes); return result; } finally { BufferPool.releaseByteArray(buf); } } /** * Returns an array of encodings that can be detected by the detectEncoding methods. * Note that some of the returned character encodings may not be available on the Java runtime. * * @return an array of encodings that can be detected by the detectEncoding methods. */ public static String[] getDetectableEncodings() { return CharsetDetector.getAllDetectableCharsets(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/FailSafePipedInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; /** * SafePipedInputStream is a PipedInputStream that can be safely interrupted when the thread that * pushes data to this stream has encountered an error. This can be done by calling * {@link #setExternalFailure(java.io.IOException)} with an IOException that will be thrown by * read/skip/available methods thereafter. This method ensures that the IOException will be propagated * to any thread that is currently executing a read/skip/available method of this class. * *

    This class overcomes a limitation of PipedInputStream whose read/skip/available methods do not * throw an IOException when the stream is closed from another thread in the midst of their execution. * * @author Maxence Bernard */ public class FailSafePipedInputStream extends PipedInputStream { /** An IOException to be thrown by read/skip/available methods */ private volatile IOException failure; public FailSafePipedInputStream() { super(); } public FailSafePipedInputStream(int pipeSize) { super(pipeSize); } public FailSafePipedInputStream(PipedOutputStream src) throws IOException { super(src); } public FailSafePipedInputStream(PipedOutputStream src, int pipeSize) throws IOException { super(src, pipeSize); } /** * Sets an IOException to be subsequently thrown by read, skip and * available methods. This method calls {@link #close()} to have any other thread blocked in a * read/skip/available return immediately and throw the specified exception. * * @param failure the IOException to be thrown by read, skip and available methods, null for none */ public void setExternalFailure(IOException failure) { this.failure = failure; if (failure!=null) { // Close the PipedInputStream to have any other thread blocked in a read/skip/available return immediately try { super.close(); } catch(IOException ignore) { } } } /** * Checks whether an external failure (IOException) has been registered and if has, throws it. * * @throws java.io.IOException if an external failure has been registered */ protected void checkExternalFailure() throws IOException { if (failure != null) { throw failure; } } @Override public synchronized int read() throws IOException { int ret = super.read(); checkExternalFailure(); return ret; } @Override public synchronized int read(byte b[], int off, int len) throws IOException { int ret = super.read(b, off, len); checkExternalFailure(); return ret; } @Override public synchronized int read(byte b[]) throws IOException { int ret = super.read(b); checkExternalFailure(); return ret; } @Override public long skip(long n) throws IOException { long ret = super.skip(n); checkExternalFailure(); return ret; } @Override public synchronized int available() throws IOException { int ret = super.available(); checkExternalFailure(); return ret; } @Override public void close() throws IOException { super.close(); checkExternalFailure(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/FileTransferException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * FileTransferException is an IOException which can be thrown to indicate a file transfer error. * {@link #getReason() getReason()} returns the reason why the transfer failed. * * @author Maxence Bernard */ public class FileTransferException extends IOException { /** Reason not known / not specified */ public final static int UNKNOWN_REASON = 0; /** Source and destination files are identical */ public final static int SOURCE_AND_DESTINATION_IDENTICAL = 1; /** Source file could not be opened for read */ public final static int OPENING_SOURCE = 2; /** Destination file could not be opened for write */ public final static int OPENING_DESTINATION = 3; /** An error occurred while reading the source file */ public final static int READING_SOURCE = 4; /** An error occurred while writing the destination file */ public final static int WRITING_DESTINATION = 5; /** An error occurred while deleting the source file (used when moving a file) */ public final static int DELETING_SOURCE = 6; /** Source file could not be closed */ public final static int CLOSING_SOURCE = 7; /** Destination file could not be closed */ public final static int CLOSING_DESTINATION = 8; /** Destination file exists */ public final static int DESTINATION_EXISTS = 9; /** File not found (does not exist) */ public final static int FILE_NOT_FOUND = 10; /** Source file is a parent of the destination file */ public final static int SOURCE_PARENT_OF_DESTINATION = 11; /** An error occurred while reading the destination file */ public final static int READING_DESTINATION = 12; /** The checksum of the source and destination files don't match */ public final static int CHECKSUM_MISMATCH = 13; /** The requested operation is not supported */ public final static int UNSUPPORTED_OPERATION = 14; protected int reason; private final long bytesWritten; public FileTransferException(int reason) { this(reason, 0, null); } public FileTransferException(int reason, Throwable cause) { this(reason, 0, cause); } public FileTransferException(int reason, long bytesWritten) { this(reason, bytesWritten, null); } public FileTransferException(int reason, long bytesWritten, Throwable cause) { super(cause); this.reason = reason; this.bytesWritten = bytesWritten; } public int getReason() { return reason; } public long getBytesWritten() { return bytesWritten; } public String toString() { return super.toString()+" reason="+reason; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/FilterRandomAccessInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * FilterRandomAccessInputStream implements {@link RandomAccessInputStream} by delegating all methods * to an existing RandomAccessInputStream instance. It allows to override selected methods and * filter the underlying RandomAccessInputStream. * * @see java.io.FilterInputStream * @author Maxence Bernard */ public class FilterRandomAccessInputStream extends RandomAccessInputStream { /** The RandomAccessInputStream instance to proxy */ protected RandomAccessInputStream rais; public FilterRandomAccessInputStream(RandomAccessInputStream rais) { this.rais = rais; } @Override public int read() throws IOException { return rais.read(); } @Override public int read(byte[] b, int off, int len) throws IOException { return rais.read(b, off, len); } @Override public void close() throws IOException { rais.close(); } public long getOffset() throws IOException { return rais.getOffset(); } public long getLength() throws IOException { return rais.getLength(); } public void seek(long offset) throws IOException { rais.seek(offset); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/FilteredOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.OutputStream; /** * This class provides a proper implementation of an OutputStream filter. * *

    Unlike java.io.FilterOutputStream, this method delegates all methods to an underlying OutputStream * and nothing more. In particular, {@link #write(byte[])} and {@link #write(byte[], int, int)} do not * call {@link #write(int)} repeatedly (very inefficient) but delegate to the corresponding OutputStream methods. This * makes this class much safer to use from a performance perspective than java.io.FilteredOutputStream. * * @author Maxence Bernard */ public class FilteredOutputStream extends OutputStream { /** The underlying OutputStream to filter */ protected OutputStream out; /** * Creates a new FilteredOutputStream that delegates all methods to the provided OutputStream. * * @param out the underlying OutputStream to filter */ public FilteredOutputStream(OutputStream out) { this.out = out; } @Override public void write(int b) throws IOException { out.write(b); } @Override public void write(byte[] b) throws IOException { out.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } @Override public void flush() throws IOException { out.flush(); } @Override public void close() throws IOException { out.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/FilteredRandomOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * FilteredRandomOutputStream is a filtered output stream for {@link RandomAccessOutputStream} subclasses, allowing * to easily extend the functionality provided by the stream by overriding only a few methods. * In a similar way to java.io.FilteredOutputStream, this class delegates all method calls to the * specified RandomAccessOutputStream. * * @author Maxence Bernard */ public class FilteredRandomOutputStream extends RandomAccessOutputStream { /** The underlying RandomAccessOutputStream */ protected RandomAccessOutputStream raos; /** * Creates a new FilteredRandomOutputStream that delegates all method calls to the specified * RandomAccessOutputStream. * * @param raos the RandomAccessOutputStream to delegate method calls to */ public FilteredRandomOutputStream(RandomAccessOutputStream raos) { this.raos = raos; } ///////////////////////////////////////////// // RandomAccessOutputStream implementation // ///////////////////////////////////////////// @Override public void write(int b) throws IOException { raos.write(b); } @Override public void write(byte b[]) throws IOException { raos.write(b); } @Override public void write(byte b[], int off, int len) throws IOException { raos.write(b, off, len); } @Override public void setLength(long newLength) throws IOException { raos.setLength(newLength); } public long getOffset() throws IOException { return raos.getOffset(); } public long getLength() throws IOException { return raos.getLength(); } public void seek(long offset) throws IOException { raos.seek(offset); } @Override public void flush() throws IOException { raos.flush(); } @Override public void close() throws IOException { try { flush(); } catch(IOException e) { // Try closing the stream anyway } raos.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/FixedByteArrayOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; /** * FixedByteArrayOutputStream writes data to a pre-allocated byte array passed to the constructor. *

    * This class is similar to {@link ByteArrayOutputStream} except that the byte array is pre-allocated and does not * grow when its capacity is reached. Attempts to write more bytes than the array's length will result in * {@link ArrayIndexOutOfBoundsException}. To prevent {@link ArrayIndexOutOfBoundsException} from being thrown, * a FixedByteArrayOutputStream can be wrapped around a {@link BoundedOutputStream} with a limit set to * the byte array's length. * * @author Maxence Bernard */ public class FixedByteArrayOutputStream extends OutputStream { private byte[] bytes; private int offset; public FixedByteArrayOutputStream(byte bytes[]) { this(bytes, 0); } public FixedByteArrayOutputStream(byte bytes[], int offset) { this.bytes = bytes; this.offset = offset; } //////////////////////// // Overridden methods // //////////////////////// @Override public synchronized void write(int b) throws IOException { bytes[offset++] = (byte)b; } @Override public synchronized void write(byte[] b) throws IOException { write(b, 0, b.length); } @Override public synchronized void write(byte[] b, int off, int len) throws IOException { System.arraycopy(b, off, bytes, offset, len); offset += len; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/MultiOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.OutputStream; import java.util.Enumeration; import java.util.Vector; /** * MultiOutputStream 'multiplies' a stream by forwarding the data that is written to it to several * registered output streams. Similarily, {@link #flush()} and {@link #close()} call the same method on all registered * output streams. * Until one or more OutputStream is registered, this stream acts as a sink: all OutputStream operations are no-ops. * * @author Maxence Bernard */ public class MultiOutputStream extends OutputStream { /** Registered OutputStreams */ protected Vector streams = new Vector<>(); /** * Creates a new MultiOutputStream that initially contains no OutputStream. * Until one or more OutputStream is * registered, this stream acts as a sink: all OutputStream operations are no-ops. */ public MultiOutputStream() { } /** * Adds an OutputStream to the list of destination output streams. This method doesn't check whether * the specified stream already exists in the list, so the same output stream may be added several times. * * @param out the OutputStream to add */ public synchronized void addOutputStream(OutputStream out) { streams.add(out); } /** * Removes the first occurrence of the given OutputStream from the list of destination output streams. * If the same stream was added several times, this method has to be called that many times to remove it entirely. * * @param out the OutputStream to add */ public synchronized void removeOutputStream(OutputStream out) { streams.remove(out); } /** * Returns true if the specified stream is present in the list of destination output streams. * * @param out the OutputStream to look for * @return true if the specified stream is present in the list of destination output streams */ public synchronized boolean containsOutputStream(OutputStream out) { return streams.contains(out); } /** * Returns an {@link java.util.Enumeration} of the destination output streams. This instance should be synchronized * externally to ensure that the list of streams is not modified while it is being enumerated. * * @return an {@link java.util.Enumeration} of the destination output streams */ public synchronized Enumeration enumOutputStream() { return streams.elements(); } ///////////////////////////////// // OutputStream implementation // ///////////////////////////////// /** * Calls write(b) with the specified byte on each of the destination output streams. *

    * Any IOException thrown by any of the destination output stream is immediately re-thrown, aborting the write * as a whole. */ @Override public synchronized void write(int b) throws IOException { Enumeration elements = streams.elements(); while(elements.hasMoreElements()) elements.nextElement().write(b); } /** * Calls write(b,off,len) with the specified parameters on each of the destination output streams. *

    * Any IOException thrown by any of the destination output stream is immediately re-thrown, aborting the operation * as a whole. */ @Override public synchronized void write(byte b[], int off, int len) throws IOException { Enumeration elements = streams.elements(); while(elements.hasMoreElements()) elements.nextElement().write(b, off, len); } /** * Calls write(b) with the specified parameter on each of the destination output streams. *

    * Any IOException thrown by any of the destination output stream is immediately re-thrown, aborting the operation * as a whole. */ @Override public synchronized void write(byte b[]) throws IOException { Enumeration elements = streams.elements(); while(elements.hasMoreElements()) elements.nextElement().write(b); } /** * Calls flush on each of the destination output streams. *

    * Any IOException thrown by any of the destination output stream is immediately re-thrown, aborting the operation * as a whole. */ @Override public synchronized void flush() throws IOException { Enumeration elements = streams.elements(); while(elements.hasMoreElements()) elements.nextElement().flush(); } /** * Calls close on each of the destination output streams. *

    * Any IOException thrown by any of the destination output stream is immediately re-thrown, aborting the operation * as a whole. */ @Override public void close() throws IOException { Enumeration elements = streams.elements(); while(elements.hasMoreElements()) elements.nextElement().close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/RandomAccess.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * RandomAccess provides a common interface to random access streams, whether they be input or output streams. * * @author Maxence Bernard */ public interface RandomAccess { /** * Closes the random access stream and releases any system resources associated with the stream. * A closed stream cannot perform input or output operations and cannot be reopened. * * @throws IOException if an I/O error occurs */ void close() throws IOException; /** * Returns the offset (in bytes) from the beginning of the file at which the next read or write occurs. * * @return the offset (in bytes) from the beginning of the file at which the next read or write occurs * @throws IOException if an I/O error occurs. */ long getOffset() throws IOException; /** * Returns the length of the file, in bytes. * * @return the length of the file, in bytes * @throws IOException if an I/O error occurs */ long getLength() throws IOException; /** * Sets the offset, measured from the beginning of the file, at which the next read or write occurs. * The offset may be set beyond the end of the file. Setting the offset beyond the end of the file does not change * the file length. The file length will change only by writing after the offset has been set beyond the end of the * file. * * @param offset the new offset position, measured in bytes from the beginning of the file * @throws IOException if an I/O error occurs */ void seek(long offset) throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/io/RandomAccessInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; /** * RandomAccessInputStream is an InputStream with random access. * * The following java.io.InputStream methods are overridden to provide an improved implementation: *

      *
    • {@link #mark(int)}
    • *
    • {@link #reset()}
    • *
    • {@link #markSupported()}
    • *
    • {@link #skip(long)}
    • *
    • {@link #available()}
    • *
    * * Important: BufferedInputStream or any wrapper InputStream class that uses a read buffer * CANNOT be used with a RandomAccessInputStream if the {@link #seek(long)} method is to be used. Doing so * would corrupt the read buffer and yield to data inconsistencies. * * @author Maxence Bernard, Nicolas Rinaudo */ public abstract class RandomAccessInputStream extends InputStream implements RandomAccess { /** Logger used by this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(RandomAccessInputStream.class); /** The last offset set by {@link #mark(int)} */ private long markOffset; /** * Creates a new RandomAccessInputStream. */ public RandomAccessInputStream() { } /** * Reads b.length bytes from this file into the byte array, starting at the current file pointer. * This method reads repeatedly from the file until the requested number of bytes are read. This method blocks until * the requested number of bytes are read, the end of the stream is detected, or an exception is thrown. * * @param b the buffer into which the data is read. * @throws java.io.EOFException if this file reaches the end before reading all the bytes. * @throws IOException if an I/O error occurs. */ public void readFully(byte b[]) throws IOException { StreamUtils.readFully(this, b, 0, b.length); } /** * Reads exactly len bytes from this file into the byte array, starting at the current file pointer. * This method reads repeatedly from the file until the requested number of bytes are read. This method blocks until * the requested number of bytes are read, the end of the stream is detected, or an exception is thrown. * * @param b the buffer into which the data is read. * @param off the start offset of the data. * @param len the number of bytes to read. * @throws java.io.EOFException if this file reaches the end before reading all the bytes. * @throws IOException if an I/O error occurs. */ public void readFully(byte b[], int off, int len) throws IOException { StreamUtils.readFully(this, b, off, len); } //////////////////////// // Overridden methods // //////////////////////// /** * Skips (up to) the specified number of bytes and returns the number of bytes effectively skipped. * The exact given number of bytes will be skipped as long as the current offset as returned by {@link #getOffset()} * plus the number of bytes to skip doesn't exceed the length of this stream as returned by {@link #getLength()}. * If it does, all the remaining bytes will be skipped so that the offset of this stream will be positioned to * {@link #getLength()}. * Returns -1 if the offset is already positioned to the end of the stream when this method is called. * * @param n number of bytes to skip * @return the number of bytes that have effectively been skipped, -1 if the offset is already positioned to the * end of the stream when this method is called (FIXME this violates the definition of the return value * of java.io.InputStream (>=0) and therefore it cannot be used as an InputStream as it should when extending it. * Either this class should not extend InputStream or it should not return a negative value) * @throws IOException if something went wrong */ @Override public long skip(long n) throws IOException { if (n <= 0) { return 0; } long offset = getOffset(); long length = getLength(); // Return -1 if the offset is already at the end of the stream if(offset>=length) return -1; // Makes sure not to go beyond the end of the stream long newOffset = offset + n; if (newOffset > length) newOffset = length; // Seek to the new offset seek(newOffset); // Return the actual number of bytes skipped return (int) (newOffset - offset); } /** * Return the number of bytes that are available for reading, that is: {@link #getLength()} - {@link #getOffset()} - 1. * Since InputStream.available() returns an int and this method overrides it, a maximum of * Integer.MAX_VALUE can be returned, even if this stream has more bytes available. * * @return the number of bytes that are available for reading. * @throws IOException if something went wrong */ @Override public int available() throws IOException { return (int)(getLength() - getOffset() - 1); } /** * Overrides InputStream.mark() to provide a working implementation of the method. The given readLimit * is simply ignored, the stream can be repositioned using {@link #reset()} with no limit on the number of bytes * read after mark() has been called. * * @param readLimit this parameter has no effect and is simply ignored */ @Override public synchronized void mark(int readLimit) { try { this.markOffset = getOffset(); } catch (IOException e) { LOGGER.info("Caught exception", e); } } /** * Overrides InputStream.mark() to provide a working implementation of the method. * * @throws IOException if something went wrong */ @Override public synchronized void reset() throws IOException { seek(this.markOffset); } /** * Always returns true: {@link #mark(int)} and {@link #reset()} methods are supported. */ @Override public boolean markSupported() { return true; } ////////////////////// // Abstract methods // ////////////////////// /** * Reads up to len bytes of data from this file into an array of bytes. This method blocks until at * least one byte of input is available. * * @param b the buffer into which the data is read * @param off the start offset of the data * @param len the maximum number of bytes read * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the * file has been reached. * @throws IOException if an I/O error occurs */ @Override public abstract int read(byte b[], int off, int len) throws IOException; /** * Closes this stream and releases any system resources associated with the stream. * A closed stream cannot perform input operations and cannot be reopened. * * @throws IOException if an I/O error occurs. */ @Override public abstract void close() throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/io/RandomAccessOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.OutputStream; /** * RandomAccessOutputStream is an OutputStream with random access. * * Important: BufferedOutputStream or any class wrapping a standard OutputStream * and using an internal buffer CANNOT be used with a RandomAccessInputStream if the {@link #seek(long)} * method is to be used. Doing so would corrupt the write buffer and yield to data inconsistencies. * {@link BufferedRandomOutputStream} provides safe buffering to RandomAccessOutputStream. * * @author Maxence Bernard */ public abstract class RandomAccessOutputStream extends OutputStream implements RandomAccess { /** * Creates a new RandomAccessOutputStream. */ public RandomAccessOutputStream() { } ////////////////////// // Abstract methods // ////////////////////// /** * Writes b.length bytes from the specified byte array to this file, starting at the current file offset. * * @param b the data to write * @throws IOException if an I/O error occurs */ @Override public abstract void write(byte b[]) throws IOException; /** * Writes len bytes from the specified byte array starting at offset off to this file. * * @param b the data to write * @param off the start offset in the data array * @param len the number of bytes to write * @throws IOException if an I/O error occurs */ @Override public abstract void write(byte b[], int off, int len) throws IOException; /** * Sets the length of the file. * *

    If the present length of the file as returned by the {@link #getLength()} method is greater than the * newLength argument then the file will be truncated. In this case, if the file offset as returned * by the {@link #getOffset()} method is greater than newLength then the * offset will be equal to newLength after this method returns. * *

    If the present length of the file as returned by the {@link #getLength()} method is smaller than the * newLength argument then the file will be extended. In this case, the contents of the extended * portion of the file are not defined. * * @param newLength the new file's length * @throws IOException If an I/O error occurred while trying to change the file's length */ public abstract void setLength(long newLength) throws IOException; /** * Closes this stream and releases any system resources associated with the stream. * A closed stream cannot perform output operations and cannot be reopened. * * @throws IOException if an I/O error occurs. */ @Override public abstract void close() throws IOException; } ================================================ FILE: src/main/java/com/mucommander/commons/io/RandomGeneratorInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.InputStream; import java.util.Random; /** * This stream allows random data generated with a {@link Random} instance to be read. * It can be instantiated in one of the three following ways: *

      *
    • with a pseudo-unique seed, leading to different random data being generated from one instance of this class * to the other.
    • *
    • with a specified seed, leading to the same random data being generated from one instance of this class to * the other.
    • *
    • with a specified {@link Random}.
    • *
    * * @author Maxence Bernard */ public class RandomGeneratorInputStream extends InputStream { /** Random data generator. */ protected Random random; public RandomGeneratorInputStream() { this(new Random()); } public RandomGeneratorInputStream(long seed) { this(new Random(seed)); } public RandomGeneratorInputStream(Random random) { this.random = random; } @Override public int read() throws IOException { return random.nextInt(); } @Override public int read(byte[] b) throws IOException { random.nextBytes(b); return b.length; } @Override public int read(byte[] b, int off, int len) throws IOException { int end = off+len; for(int i=off; i. */ package com.mucommander.commons.io; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; /** * SilenceableOutStream is a {@link FilterOutputStream} that forwards data to an underlying * {@link OutputStream} and that can be silenced on demand. * The {@link #setSilenced(boolean)} method allows to control whether the data written to the stream should go through * (be written to the underlying stream) or be discarded. * * @author Maxence Bernard */ public class SilenceableOutputStream extends FilterOutputStream { /** When true, write methods are no-op */ private boolean silenced; /** * Creates a new SilenceableOutputStream that forwards written data to the specified * OutputStream when not silenced. By default, this SilenceableOutputStream is not * silenced. * * @param out the OutputStream to forward the data written to when not silenced */ public SilenceableOutputStream(OutputStream out) { super(out); } /** * Creates a new SilenceableOutputStream that forwards written data to the specified * OutputStream when not silenced. * * @param out the OutputStream to forward the data written to when not silenced * @param silenced initial silenced state */ public SilenceableOutputStream(OutputStream out, boolean silenced) { super(out); this.silenced = silenced; } /** * Controls whether the data written to the stream goes through (be written to the underlying stream) or * discarded. If called with false, any subsequent call to write methods will be * ignored (they become no-op), until this method is called again with false. Note that un-silencing * this stream will not print messages that were previously written while the stream was silenced. * * @param silenced true to have write methods become no-ops, false to have * them forward the data to the underlying OutputStream */ public void setSilenced(boolean silenced) { this.silenced = silenced; } /** * Returns true if this SilenceableOutStream is currently ignoring calls to * write methods, false if it is forwarding written data to the * underlying OutputStream. * * @return true if this SilenceableOutStream is currently ignoring calls to * write, false if it is forwarding written data to the * underlying OutputStream. */ public boolean isSilenced() { return silenced; } @Override public void write(int b) throws IOException { if(silenced) return; out.write(b); } @Override public void write(byte[] b) throws IOException { if(silenced) return; out.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { if(silenced) return; out.write(b, off, len); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/SinkOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.OutputStream; /** * SinkOutputStream is an OutputStream which implements all write() methods as no-ops, loosing data as * it gets written, in a similar fashion to the /dev/null UNIX device. * * @author Maxence Bernard */ public class SinkOutputStream extends OutputStream { @Override public void write(int i) throws IOException { } /** * Overridden for performance reasons. */ @Override public void write(byte[] bytes) throws IOException { } /** * Overridden for performance reasons. */ @Override public void write(byte[] bytes, int off, int len) throws IOException { } } ================================================ FILE: src/main/java/com/mucommander/commons/io/StreamOutOfBoundException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; /** * This IOException can be used when attempting to read from a {@link BoundedInputStream} or * {@link BoundedReader} beyond the byte or character limit set. * * @see com.mucommander.commons.io.BoundedInputStream * @see com.mucommander.commons.io.BoundedOutputStream * @see com.mucommander.commons.io.BoundedReader * @author Maxence Bernard */ public class StreamOutOfBoundException extends IOException { public StreamOutOfBoundException(long limit) { super("Attempt to read out of bounds, limit="+limit); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/StreamUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.*; /** * This class provides convenience static methods that operate on streams. All read/write buffers are allocated using * {@link BufferPool} for memory efficiency reasons. * * @author Maxence Bernard */ public class StreamUtils { /** * This method is a shorthand for {@link #copyStream(java.io.InputStream, java.io.OutputStream, int)} called with a * {@link BufferPool#getDefaultBufferSize() default buffer size}. * * @param in the InputStream to read from * @param out the OutputStream to write to * @return the number of bytes that were copied * @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams */ public static long copyStream(InputStream in, OutputStream out) throws FileTransferException { return copyStream(in, out, BufferPool.getDefaultBufferSize()); } /** * This method is a shorthand for {@link #copyStream(java.io.InputStream, java.io.OutputStream, int, long)} called * with a {@link Long#MAX_VALUE}. * * @param in the InputStream to read from * @param out the OutputStream to write to * @param bufferSize size of the buffer to use, in bytes * @return the number of bytes that were copied * @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams */ public static long copyStream(InputStream in, OutputStream out, int bufferSize) throws FileTransferException { return copyStream(in, out, bufferSize, Long.MAX_VALUE); } /** * Shorthand for {@link #copyStream(InputStream, OutputStream, byte[], long)} called with a buffer of the specified * size retrieved from {@link BufferPool}. * * @param in the InputStream to read from * @param out the OutputStream to write to * @param bufferSize size of the buffer to use, in bytes * @param length number of bytes to copy from InputStream * @return the number of bytes that were copied * @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams */ public static long copyStream(InputStream in, OutputStream out, int bufferSize, long length) throws FileTransferException { // Use BufferPool to reuse any available buffer of the same size byte[] buffer = BufferPool.getByteArray(bufferSize); try { return copyStream(in, out, buffer, length); } finally { // Make the buffer available for further use BufferPool.releaseByteArray(buffer); } } /** * Copies up to {@code length} bytes from the given {@code InputStream} to the specified * {@code OutputStream}, less if the end-of-file was reached before that. * This method does *NOT* close any of the given streams. * *

    Read and write operations use the specified buffer, making the use of a {@code BufferedInputStream} * unnecessary. A {@code BufferedOutputStream} also isn't necessary, unless this method * is called repeatedly with the same {@code OutputStream} and with potentially small {@code InputStream} * (smaller than the buffer's size): in this case, providing a {@code BufferedOutputStream} will further * improve performance by grouping calls to the underlying {@code OutputStream} write method. * *

    Copy progress can optionally be monitored by supplying a {@link com.mucommander.commons.io.CounterInputStream} and/or * {@link com.mucommander.commons.io.CounterOutputStream}. * * @param in the InputStream to read from * @param out the OutputStream to write to * @param buffer buffer to use for copying * @param length number of bytes to copy from InputStream * @return the number of bytes that were copied * @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams */ public static long copyStream(InputStream in, OutputStream out, byte[] buffer, long length) throws FileTransferException { // Copies the InputStream's content to the OutputStream chunk by chunk long totalRead = 0; int failureCounter = 0; while (length > 0) { int nbRead; try { nbRead = in.read(buffer, 0, (int)Math.min(buffer.length, length)); // the result of min will be int } catch(IOException e) { throw new FileTransferException(FileTransferException.READING_SOURCE); } if (nbRead < 0) { break; } else if (nbRead == 0) { failureCounter++; if (failureCounter > 10) { throw new FileTransferException(FileTransferException.UNKNOWN_REASON); } sleepIfNoRead(); } else { failureCounter = 0; } try { out.write(buffer, 0, nbRead); } catch(IOException e) { throw new FileTransferException(FileTransferException.WRITING_DESTINATION, totalRead); } length -= nbRead; totalRead += nbRead; } return totalRead; } /** * This method is a shorthand for {@link #transcode(java.io.InputStream, String, java.io.OutputStream, String, int)} * called with a {@link BufferPool#getDefaultBufferSize() default buffer size}. * * @param in the InputStream to read from * @param inCharset the source charset * @param out the OutputStream to write to * @param outCharset the destination charset * @return the number of bytes that were transcoded * @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams * @throws UnsupportedEncodingException if any of the two charsets are not supported by the JVM */ public static long transcode(InputStream in, String inCharset, OutputStream out, String outCharset) throws FileTransferException, UnsupportedEncodingException { return transcode(in, inCharset, out, outCharset, BufferPool.getDefaultBufferSize()); } /** * Converts a stream from a charset to another, copying the contents of the given InputStream to the * OutputStream. A {@link java.io.UnsupportedEncodingException} is thrown if any of the two charsets * are not supported by the JVM. * *

    Apart from the transcoding part, this method operates exactly like {@link #copyStream(java.io.InputStream, java.io.OutputStream, int)}. * In particular, none of the given streams are closed. * * @param in the InputStream to read the data from * @param inCharset the source charset * @param out the OutputStream to write to * @param outCharset the destination charset * @param bufferSize size of the buffer to use, in bytes * @return the number of bytes that were transcoded * @throws FileTransferException if something went wrong while reading from or writing to one of the provided streams * @throws UnsupportedEncodingException if any of the two charsets are not supported by the JVM * @see #copyStream(java.io.InputStream, java.io.OutputStream, int) * @see java.nio.charset.Charset#isSupported(String) */ public static long transcode(InputStream in, String inCharset, OutputStream out, String outCharset, int bufferSize) throws FileTransferException, UnsupportedEncodingException { InputStreamReader isr = new InputStreamReader(in, inCharset); OutputStreamWriter osw = new OutputStreamWriter(out, outCharset); // Use BufferPool to reuse any available buffer of the same size char[] buffer = BufferPool.getCharArray(bufferSize); try { // Copies the InputStreamReader's content to the OutputStreamWriter chunk by chunk int nbRead; long totalRead = 0; int failureCounter = 0; while (true) { try { nbRead = isr.read(buffer, 0, buffer.length); } catch(IOException e) { throw new FileTransferException(FileTransferException.READING_SOURCE); } if (nbRead < 0) { break; } else if (nbRead == 0) { failureCounter++; if (failureCounter > 10) { throw new FileTransferException(FileTransferException.UNKNOWN_REASON); } sleepIfNoRead(); } else { failureCounter = 0; } try { osw.write(buffer, 0, nbRead); // Let's not forget to flush as the writer will *not* be closed (to avoid closing the OutputStream) osw.flush(); } catch(IOException e) { throw new FileTransferException(FileTransferException.WRITING_DESTINATION); } totalRead += nbRead; } return totalRead; } finally { // Make the buffer available for further use BufferPool.releaseCharArray(buffer); } } /** * This method is a shorthand for {@link #fillWithConstant(java.io.OutputStream, byte, long, int)} called with a * {@link BufferPool#getDefaultBufferSize default buffer size}. * * @param out the OutputStream to write to * @param value the byte constant to write len times * @param len number of bytes to write * @throws java.io.IOException if an error occurred while writing */ public static void fillWithConstant(OutputStream out, byte value, long len) throws IOException { fillWithConstant(out, value, len, BufferPool.getDefaultBufferSize()); } /** * Writes the specified byte constant len times to the given OutputStream. * This method does *NOT* close the stream when it is finished. * * @param out the OutputStream to write to * @param value the byte constant to write len times * @param len number of bytes to write * @param bufferSize size of the buffer to use, in bytes * @throws java.io.IOException if an error occurred while writing */ public static void fillWithConstant(OutputStream out, byte value, long len, int bufferSize) throws IOException { // Use BufferPool to avoid excessive memory allocation and garbage collection byte[] buffer = BufferPool.getByteArray(bufferSize); // Fill the buffer with the constant byte value, not necessary if the value is zero if (value != 0) { for (int i = 0; i < bufferSize; i++) { buffer[i] = value; } } try { long remaining = len; while (remaining > 0) { int nbWrite = (int)(remaining > bufferSize ? bufferSize : remaining); out.write(buffer, 0, nbWrite); remaining -= nbWrite; } } finally { BufferPool.releaseByteArray(buffer); } } /** * This method is a shorthand for {@link #copyChunk(RandomAccessInputStream, RandomAccessOutputStream, long, long, long, int)} * called with a {@link BufferPool#getDefaultBufferSize default buffer size}. * * @param rais the source stream * @param raos the destination stream * @param srcOffset offset to the beginning of the chunk in the source stream * @param destOffset offset to the beginning of the chunk in the destination stream * @param length number of bytes to copy * @throws java.io.IOException if an error occurred while copying data */ public static void copyChunk(RandomAccessInputStream rais, RandomAccessOutputStream raos, long srcOffset, long destOffset, long length) throws IOException { copyChunk(rais, raos, srcOffset, destOffset, length, BufferPool.getDefaultBufferSize()); } /** * Copies a chunk of data from the given {@link com.mucommander.commons.io.RandomAccessInputStream} to the specified * {@link com.mucommander.commons.io.RandomAccessOutputStream}. * * @param rais the source stream * @param raos the destination stream * @param srcOffset offset to the beginning of the chunk in the source stream * @param destOffset offset to the beginning of the chunk in the destination stream * @param length number of bytes to copy * @param bufferSize size of the buffer to use, in bytes * @throws java.io.IOException if an error occurred while copying data */ public static void copyChunk(RandomAccessInputStream rais, RandomAccessOutputStream raos, long srcOffset, long destOffset, long length, int bufferSize) throws IOException { rais.seek(srcOffset); raos.seek(destOffset); // Use BufferPool to avoid excessive memory allocation and garbage collection byte[] buffer = BufferPool.getByteArray(bufferSize); try { long remaining = length; while (remaining>0) { int nbBytes = (int)(remaining < bufferSize ? remaining : bufferSize); rais.readFully(buffer, 0, nbBytes); raos.write(buffer, 0, nbBytes); remaining -= nbBytes; } } finally { BufferPool.releaseByteArray(buffer); } } /** * This method is a shorthand for {@link #readFully(java.io.InputStream, byte[], int, int)}. * * @param in the InputStream to read from * @param b the buffer into which the stream data is copied * @return the same byte array that was passed, returned only for convenience * @throws java.io.EOFException if EOF is reached before all bytes have been read * @throws IOException if an I/O error occurs */ public static byte[] readFully(InputStream in, byte[] b) throws EOFException, IOException { return readFully(in, b, 0, b.length); } /** * Reads exactly len bytes from the InputStream and copies them into the byte array, * starting at position off. * *

    This method calls the read() method of the given stream until the requested number of bytes have * been skipped, or throws an {@link EOFException} if the end of file has been reached prematurely. * * @param in the InputStream to read from * @param b the buffer into which the stream data is copied * @param off specifies where the copy should start in the buffer * @param len the number of bytes to read * @return the same byte array that was passed, returned only for convenience * @throws java.io.EOFException if EOF is reached before all bytes have been read * @throws IOException if an I/O error occurs */ public static byte[] readFully(InputStream in, byte[] b, int off, int len) throws EOFException, IOException { if (len > 0) { int totalRead = 0; int failureCounter = 0; do { int nbRead = in.read(b, off + totalRead, len - totalRead); if (nbRead < 0) { throw new EOFException(); } else if (nbRead == 0) { failureCounter++; if (failureCounter > 10) { throw new FileTransferException(FileTransferException.UNKNOWN_REASON); } sleepIfNoRead(); } else { failureCounter = 0; } totalRead += nbRead; } while (totalRead < len); } return b; } /** * Skips exactly nbytes from the given InputStream. * *

    This method calls the skip() method of the given stream until the requested number of bytes have * been skipped, or throws an {@link EOFException} if the end of file has been reached prematurely. * * @param in the InputStream to skip bytes from * @param n the number of bytes to skip * @throws java.io.EOFException if the EOF is reached before all bytes have been skipped * @throws java.io.IOException if an I/O error occurs */ public static void skipFully(InputStream in, long n) throws IOException { if (n <= 0) { return; } int failureCounter = 0; do { long nbSkipped = in.skip(n); if (nbSkipped < 0) { throw new EOFException(); } else if (nbSkipped == 0) { failureCounter++; if (failureCounter > 10) { throw new IOException("skipFully timeout"); } sleepIfNoRead(); } else { failureCounter = 0; } n -= nbSkipped; } while (n > 0); } private static void sleepIfNoRead() { try { Thread.sleep(50); } catch (InterruptedException ignore) {} } /** * This method is a shorthand for {@link #readUpTo(java.io.InputStream, byte[], int, int) readUpTo(in, b, 0, b.length)}. * * @param in the InputStream to read from * @param b the buffer into which the stream data is copied * @return the number of bytes that have been read, can be less than len if EOF has been reached prematurely * @throws IOException if an I/O error occurs */ public static int readUpTo(InputStream in, byte[] b) throws IOException { return readUpTo(in, b, 0, b.length); } /** * Reads up to len bytes from the InputStream and copies them into the byte array, * starting at position off. * *

    This method differs from {@link #readFully(java.io.InputStream, byte[], int, int)} in that it does not throw * a java.io.EOFException if the end of stream is reached before all bytes have been read. In that * case (and in that case only), the number of bytes returned by this method will be lower than len. * * @param in the InputStream to read from * @param b the buffer into which the stream data is copied * @param off specifies where the copy should start in the buffer * @param len the number of bytes to read * @return the number of bytes that have been read, can be less than len if EOF has been reached prematurely * @throws IOException if an I/O error occurs */ public static int readUpTo(InputStream in, byte[] b, int off, int len) throws IOException { int totalRead = 0; int failureCounter = 0; if (len > 0) { do { int nbRead = in.read(b, off + totalRead, len - totalRead); if (nbRead < 0) { break; } else if (nbRead == 0) { failureCounter++; if (failureCounter > 10) { throw new FileTransferException(FileTransferException.UNKNOWN_REASON); } sleepIfNoRead(); } else { failureCounter = 0; } totalRead += nbRead; } while (totalRead < len); } return totalRead; } /** * This method is a shorthand for {@link #readUntilEOF(java.io.InputStream, int)} called with a * {@link BufferPool#getDefaultBufferSize default buffer size}. * * @param in the InputStream to read * @throws IOException if an I/O error occurs */ public static void readUntilEOF(InputStream in) throws IOException { readUntilEOF(in, BufferPool.getDefaultBufferSize()); } /** * This method reads the given InputStream until the End Of File is reached, discarding all the data that is read * in the process. It is noteworthy that this method does not close the stream. * * @param in the InputStream to read * @param bufferSize size of the read buffer * @throws IOException if an I/O error occurs */ public static void readUntilEOF(InputStream in, int bufferSize) throws IOException { // TUse BufferPool to avoid excessive memory allocation and garbage collection byte[] buffer = BufferPool.getByteArray(bufferSize); try { int failureCounter = 0; while (true) { int nbRead = in.read(buffer, 0, buffer.length); if (nbRead < 0) { break; } else if (nbRead == 0) { failureCounter++; if (failureCounter > 10) { throw new FileTransferException(FileTransferException.UNKNOWN_REASON); } sleepIfNoRead(); } else { failureCounter = 0; } } } finally { BufferPool.releaseByteArray(buffer); } } } ================================================ FILE: src/main/java/com/mucommander/commons/io/ThroughputLimitInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io; import java.io.IOException; import java.io.InputStream; /** * ThroughputLimitInputStream extends InputStream to provide control over the transfer speed and limit it to a specified * number of bytes per second. * Whenever the bytes per second quota has been reached, the read and skip methods will lock and won't return * until either: *

    • a new second commences, bringing the bytes read count back to zero for the new second
    • *
    • {@link #setThroughputLimit(long)} is called with a more permissive bytes per second value (different from 0), * yielding to more bytes available for the current second.
    • *

    * *

    Setting the throughput limit to 0 effectively blocks all read and skip calls indefinitely. * Any calls to the read or skip methods will lock, the only way to remove this lock being to call the * {@link #setThroughputLimit(long)} method with a value different from 0 from another thread. * *

    Setting the throughput limit to -1 or any other negative values will disable any limit and make * this ThroughputLimitInputStream behave just like a normal InputStream. * *

    Finally, the {@link #setUnderlyingInputStream(java.io.InputStream)} method allows to use the * same ThroughputLimitInputStream instance for multiple InputStream instances, keeping the bytes count for the * current second intact and thus the throughput limit stable. This does not hold true if a new ThroughputLimitInputStream * is created for each InputStream, the bytes count for the current second starting at 0. * * @author Maxence Bernard */ public class ThroughputLimitInputStream extends InputStream { /** Underlying InputStream */ private InputStream in; /** Throughput limit in bytes per second, -1 for no limit, 0 to completely block reads */ private long bpsLimit; /** Holds the current second, allowing to detect when a new second commences */ private long currentSecond; /** Number of bytes that have been read or skipped this second */ private long nbBytesReadThisSecond; /** * Creates a new ThroughputLimitInputStream with no initial throughput limit (-1 value). * * @param in underlying stream that is used to read data from */ public ThroughputLimitInputStream(InputStream in) { this.in = in; this.bpsLimit = -1; } /** * Creates a new ThroughputLimitInputStream with an initial throughput limit. * * @param in underlying stream that is used to read data from * @param bytesPerSecond initial throughput limit in bytes per second * @see #setThroughputLimit(long) */ public ThroughputLimitInputStream(InputStream in, long bytesPerSecond) { this.in = in; this.bpsLimit = bytesPerSecond; } /** * Specifies a new throughput limit expressed in bytes per second. * The new limit will take effect the next time one of the read or skip methods are called. * *

    Setting the throughput limit to 0 effectively blocks all read and skip calls indefinitely. * Any calls to the read or skip methods will lock, the only way to remove this lock being to call the * {@link #setThroughputLimit(long)} method with a value different from 0 from another thread. * *

    Setting the throughput limit to -1 or any other negative values will disable any limit and make * this ThroughputLimitInputStream behave just like a normal InputStream. * * @param bytesPerSecond new throughput limit expressed in bytes, -1 to disable it, 0 to block reads. */ public void setThroughputLimit(long bytesPerSecond) { this.bpsLimit = bytesPerSecond; // Wake up any thread waiting for data to be available to have them check the new limit counter synchronized(this) { notify(); } } /** * Changes the underlying InputStream which data is read from, keeping the bytes count for the current second intact. * *

    Note: the existing underlying InputStream will not be closed, the {@link #close()} method must be called prior * to calling this method. * * @param in the new InputStream to read data from */ public void setUnderlyingInputStream(InputStream in) { this.in = in; } /** * Returns the number of bytes that can be read (or skipped) without exceeding the current throughput limit. * This method blocks until at least 1 byte is available. In other words the method always returns * strictly positive values. * *

    If the current throughput limit is negative (no limit), this method returns immediately Integer.MAX_VALUE. *

    If the byte quota for the current second has been exceeded, this method locks and returns as soon as a new second * has started (i.e. bytes are available), or the {@link #setThroughputLimit(long)} with a more permissive value * has been called. *

    If the current throughput limit is 0, it will lock undefinitely, until {@link #setThroughputLimit(long)} has * been called from another thread with a value different from 0. * * @return the number of bytes available for reading without exceeding the current throughput limit */ private int getNbAllowedBytes() { // Update limit counter and retrieve number of milliseconds until next second long msUntilNextSecond = updateLimitCounter(); long allowedBytes; synchronized(this) { // Loop while throughput limit has been exceeded while((allowedBytes=bpsLimit- nbBytesReadThisSecond)<=0) { // Throughput limit was removed, return max int value if(bpsLimit<0) return Integer.MAX_VALUE; try { // If limit is 0, wait indefinitely for a call to notify() from setThroughputLimit() if(bpsLimit==0) wait(); // Wait until the current second is over for more bytes to be available, // or until a call to notify() is made from setThroughputLimit() else { wait(msUntilNextSecond); } } catch(InterruptedException e) { // No problem in this unlikely event, loop one more time and wait some more } // Update limit counter and retrieve number of milliseconds until next second msUntilNextSecond = updateLimitCounter(); } } return (int)allowedBytes; } /** * Checks if the current second has changed. If that's the case, updates the current second value and resets the * number of bytes read this second. Returns the number of milliseconds until a new second starts. */ private long updateLimitCounter() { long now = System.currentTimeMillis(); long nowSecond = now/1000; // Current second has changed if(this.currentSecond!=nowSecond) { this.currentSecond = nowSecond; this.nbBytesReadThisSecond = 0; } return 1000-(now%1000); } /** * Increases the number of bytes read this second to the given number. * * @param nbRead number of bytes that have been read or skipped from the underlying stream. */ private void addToLimitCounter(long nbRead) { updateLimitCounter(); this.nbBytesReadThisSecond += nbRead; } //////////////////////////////// // InputStream implementation // //////////////////////////////// @Override public int read() throws IOException { // Wait until at least 1 byte is available if a limit is set if(bpsLimit>=0) getNbAllowedBytes(); // Read the byte from the underlying stream int i = in.read(); // Increase read counter by 1 if(i>0) addToLimitCounter(1); return i; } @Override public int read(byte[] bytes) throws IOException { return this.read(bytes, 0, bytes.length); } @Override public int read(byte[] bytes, int off, int len) throws IOException { int nbRead; // Wait until at least 1 byte is available if a limit is set and try to read as many bytes are available // without exceeding the throughput limit or the number specified if(bpsLimit>=0) nbRead = in.read(bytes, off, Math.min(getNbAllowedBytes(),len)); else nbRead = in.read(bytes, off, len); // Increase read counter by the number of bytes that have actually been read by the underlying stream if(nbRead>0) addToLimitCounter(nbRead); return nbRead; } @Override public long skip(long l) throws IOException { long nbSkipped = in.skip(bpsLimit>=0?Math.min(getNbAllowedBytes(),l):l); // Increase read counter by the number of bytes that have actually been skipped by the underlying stream if(nbSkipped>0) addToLimitCounter(nbSkipped); return nbSkipped; } @Override public int available() throws IOException { return in.available(); } @Override public void close() throws IOException { in.close(); } @Override public synchronized void mark(int i) { in.mark(i); } @Override public synchronized void reset() throws IOException { in.reset(); } @Override public boolean markSupported() { return in.markSupported(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/base64/Base64Decoder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.base64; import java.io.*; /** * Base64Decoder provides methods to ease the decoding of strings and byte arrays in base64. * The {@link Base64InputStream} class is used under the hood to perform the actual base64 decoding. * * @see Base64InputStream * @author Maxence Bernard */ public abstract class Base64Decoder { /** * Shorthand for {@link #decodeAsBytes(String, Base64Table)} invoked with {@link Base64Table#STANDARD_TABLE}. * * @param s a Base64-encoded String * @return the decoded string as a byte array * @throws java.io.IOException if the given String isn't properly Base64-encoded */ public static byte[] decodeAsBytes(String s) throws IOException { return decodeAsBytes(s, Base64Table.STANDARD_TABLE); } /** * Decodes the given Base64-encoded string and returns the result as a byte array. * Throws an IOException if the String isn't properly Base64-encoded. * * @param s a Base64-encoded String * @param table the table to use to decode data * @return the decoded string as a byte array * @throws java.io.IOException if the given String isn't properly Base64-encoded */ public static byte[] decodeAsBytes(String s, Base64Table table) throws IOException { byte[] b = s.getBytes(); if (b.length % 4 != 0) { // Base64 encoded data must come in a multiple of 4 bytes, throw an IOException if it's not the case throw new IOException("Byte array length is not a multiple of 4"); } ByteArrayOutputStream bout = new ByteArrayOutputStream(); try (Base64InputStream bin = new Base64InputStream(new ByteArrayInputStream(b), table)) { int i; while ((i = bin.read()) != -1) { bout.write(i); } return bout.toByteArray(); } } /** * Decodes the given Base64-encoded string and returns the result as a String. The specified encoding is used for * transforming the decoded bytes into a String. Throws an IOException if the String isn't properly * Base64-encoded, or if the encoding is not supported by the Java runtime. * * @param s a Base64-encoded String * @param encoding the character encoding to use for transforming the decoded bytes into a String * @param table the table to use to decode data * @return the decoded String * @throws UnsupportedEncodingException if the specified encoding is not supported by the Java runtime * @throws java.io.IOException if the given String isn't properly Base64-encoded */ public static String decode(String s, String encoding, Base64Table table) throws IOException { StringBuilder sb = new StringBuilder(); try (InputStreamReader isr = new InputStreamReader(new ByteArrayInputStream(decodeAsBytes(s, table)), encoding)) { int i; while ((i = isr.read()) != -1) { sb.append((char) i); } return sb.toString(); } } /** * Shorthand for {@link #decode(String, String, Base64Table)} invoked with UTF-8 encoding and * {@link Base64Table#STANDARD_TABLE}. * * @param s a Base64-encoded String * @return the decoded String * @throws java.io.IOException if the given String isn't properly Base64-encoded */ public static String decode(String s) throws IOException { return decode(s, "UTF-8", Base64Table.STANDARD_TABLE); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/base64/Base64Encoder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.base64; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; /** * Base64Encoder provides methods to ease the encoding of strings and byte arrays in base64. * The {@link Base64OutputStream} class is used under the hood to perform the actual base64 encoding. * * @see Base64OutputStream * @author Maxence Bernard */ public abstract class Base64Encoder { /** * Shorthand for {@link #encode(byte[], Base64Table)} invoked with {@link Base64Table#STANDARD_TABLE}. * * @param b bytes to base64-encode * @return the base64-encoded String */ public static String encode(byte[] b) { return encode(b, 0, b.length, Base64Table.STANDARD_TABLE); } /** * Base64-encodes the given byte array using {@link Base64Table#STANDARD_TABLE} using the given Base64 table * and returns the result. * * @param b bytes to base64-encode * @param table the table to use to encode data * @return the base64-encoded String */ public static String encode(byte[] b, Base64Table table) { return encode(b, 0, b.length, table); } /** * Shorthand for {@link #encode(byte[], int, int, Base64Table)} invoked with {@link Base64Table#STANDARD_TABLE}. * * @param b bytes to base64-encode * @param off position to the first byte in the array to be encoded * @param len number of bytes in the array to encode * @return the base64-encoded String */ public static String encode(byte[] b, int off, int len) { return encode(b, off, len, Base64Table.STANDARD_TABLE); } /** * Base64-encodes the given byte array, from off to len, and returns the result. * * @param b bytes to base64-encode * @param off position to the first byte in the array to be encoded * @param len number of bytes in the array to encode * @param table the table to use to encode data * @return the base64-encoded String */ public static String encode(byte[] b, int off, int len, Base64Table table) { ByteArrayOutputStream bout = new ByteArrayOutputStream(); try (Base64OutputStream out64 = new Base64OutputStream(bout, false, table)) { out64.write(b, off, len); out64.writePadding(); return bout.toString(); } catch (IOException e) { // Should never happen return null; } } /** * Shorthand for {@link #encode(String, String, Base64Table)} invoked with UTF-8 encoding and * {@link Base64Table#STANDARD_TABLE}. * * @param s the String to base64-encode * @return the base64-encoded String */ public static String encode(String s) { try { return encode(s, "UTF-8", Base64Table.STANDARD_TABLE); } catch(UnsupportedEncodingException e) { // Should never happen, UTF-8 is necessarily supported by the Java runtime return null; } } /** * Base64-encodes the given String and returns result. The specified encoding is used for tranforming * the string into bytes. * * @param s the String to base64-encode * @param encoding the character encoding to use for transforming the string into bytes * @param table the table to use to encode data * @return the base64-encoded String * @throws UnsupportedEncodingException if the specified encoding is not supported by the Java runtime */ public static String encode(String s, String encoding, Base64Table table) throws UnsupportedEncodingException { return encode(s.getBytes(encoding), table); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/base64/Base64InputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.base64; import java.io.IOException; import java.io.InputStream; /** * Base64InputStream is an InputStream that decodes Base64-encoded data provided by * an underlying InputStream. The underlying data must be valid base64-encoded data with respect to * the character table in use. If not, an IOException will be thrown when illegal data is encountered. * * @see Base64Decoder * @author Maxence Bernard */ public class Base64InputStream extends InputStream { /** Underlying stream data is read from */ private final InputStream in; /** The Base64 decoding table */ private final int[] decodingTable; /** The character used for padding */ private final byte paddingChar; /** Decoded bytes available for reading */ private final int[] readBuffer = new int[3]; /** Index of the next byte available for reading in the buffer */ private int readOffset; /** Number of bytes left for reading in the buffer */ private int bytesLeft; /** Buffer used temporarily for decoding */ private final int[] decodeBuffer = new int[4]; /** * Equivalent to calling {@link #Base64InputStream(java.io.InputStream, Base64Table)} with * a {@link Base64Table#STANDARD_TABLE} table. * * @param in underlying InputStream the Base64-encoded data is read from */ public Base64InputStream(InputStream in) { this(in, Base64Table.STANDARD_TABLE); } /** * Creates a new Base64InputStream that allows to decode data that has been Base64-encoded using the * given table, from the provided InputStream. * * @param in underlying InputStream the Base64-encoded data is read from * @param table the table to use for Base64 decoding */ public Base64InputStream(InputStream in, Base64Table table) { this.in = in; this.decodingTable = table.getDecodingTable(); this.paddingChar = table.getPaddingChar(); } @Override public int read() throws IOException { // Read buffer empty: read and decode a new base64-encoded 4-byte group if (bytesLeft == 0) { int read; int nbRead = 0; while (nbRead<4) { read = in.read(); // EOF reached if(read==-1) { if(nbRead%4 != 0) { // Base64 encoded data must come in a multiple of 4 bytes, throw an IOException if the underlying stream ended prematurely throw new IOException("InputStream did not end on a multiple of 4 bytes"); } if(nbRead==0) return -1; else // nbRead==4 break; } decodeBuffer[nbRead] = decodingTable[read]; // Discard any character that's not a base64 character, without throwing an IOException. // In particular, '\r' and '\n' characters that are usually found in email attachments are simply ignored. if(decodeBuffer[nbRead]==-1 && read!=paddingChar) { continue; } nbRead++; } // Decode byte 0 readBuffer[bytesLeft++] = ((decodeBuffer[0]<<2)&0xFC | ((decodeBuffer[1]>>4)&0x03)); // Test if the character is not a padding character if (decodeBuffer[2]!=-1) { // Decode byte 1 readBuffer[bytesLeft++] = (decodeBuffer[1]<<4)&0xF0 | ((decodeBuffer[2]>>2)&0x0F); // Test if the character is a padding character if (decodeBuffer[3]!=-1) // Decode byte 2 readBuffer[bytesLeft++] = ((decodeBuffer[2]<<6)&0xC0) | (decodeBuffer[3]&0x3F); } readOffset = 0; } bytesLeft--; return readBuffer[readOffset++]; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/base64/Base64OutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.base64; import java.io.IOException; import java.io.OutputStream; /** * This OutputStream encodes supplied data to Base64 encoding and writes it to the underlying * OutputStream. * * @see Base64Encoder * @author Maxence Bernard, with the exception of the algorithm description which was found on the Web. */ public class Base64OutputStream extends OutputStream { /* Base64 uses a 65 character subset of US-ASCII, allowing 6 bits for each character so the character "m" with a Base64 value of 38, when represented in binary form, is 100110. With a text string, let's say "men" is encoded this is what happens : The text string is converted into its US-ASCII value. The character "m" has the decimal value of 109 The character "e" has the decimal value of 101 The character "n" has the decimal value of 110 When converted to binary the string looks like this : m 01101101 e 01100101 n 01101110 These three "8-bits" are concatenated to make a 24 bit stream 011011010110010101101110 This 24 bit stream is then split up into 4 6-bit sections 011011 010110 010101 101110 We now have 4 values. These binary values are converted to decimal form 27 22 21 46 And the corresponding Base64 character are : b W V u The encoding is always on a three characters basis (to have a set of 4 Base64 characters). To encode one or two then, we use the special character "=" to pad until 4 base64 characters is reached. ex. encode "me" 01101101 01100101 0110110101100101 011011 010110 0101 111111 (AND to fill the missing bits) 011011 010110 010100 b W U b W U = ("=" is the padding character) so "bWU=" is the base64 equivalent. encode "m" 01101101 011011 01 111111 (AND to fill the missing bits) 011011 010000 b Q = = (two paddings are added) Finally, MIME specifies that lines are 76 characters wide maximum. */ /** Underlying OutputStream encoded data is sent to */ private final OutputStream out; /** The Base64 encoding table */ private final byte[] encodingTable; /** The character used for padding */ private final byte paddingChar; /** Array used to accumulate the first 2 bytes of a 3-byte group */ private final byte[] byteAcc = new byte[2]; /** Number of bytes accumulated to form a 3-byte group */ private int nbBytesWaiting; /** Specifies whether line breaks should be inserted after 80 chars */ private final boolean insertLineBreaks; /** Current line length (to insert line return character after 80 chars)*/ private int lineLength; /** * Equivalent to calling {@link #Base64OutputStream(java.io.OutputStream, boolean, Base64Table)} with * a {@link Base64Table#STANDARD_TABLE} table. * * @param out the underlying OutputStream to write the base64-encoded data to * @param insertLineBreaks if true, line breaks will be inserted after every 80 characters written */ public Base64OutputStream(OutputStream out, boolean insertLineBreaks) { this(out, insertLineBreaks, Base64Table.STANDARD_TABLE); } /** * Creates a new Base64OutputStream using the underlying OutputStream and table to write the * base64-encoded data. * * @param out the underlying OutputStream to write the base64-encoded data to * @param insertLineBreaks if true, line breaks will be inserted after every 80 characters written * @param table the table to use to encode data */ public Base64OutputStream(OutputStream out, boolean insertLineBreaks, Base64Table table) { this.out = out; this.insertLineBreaks = insertLineBreaks; this.encodingTable = table.getEncodingTable(); this.paddingChar = table.getPaddingChar(); } /** * Writes padding '=' characters to the underlying OutputStream if there currently is an * unfinished 3-byte group. If it's not the case, then this method is a no-op. * * @throws IOException if the padding characters could not be written to the underlying OutputStream. */ public void writePadding() throws IOException { // No padding needed if(nbBytesWaiting==0) return; // 1 padding character if (nbBytesWaiting==2) { // 2 bytes left out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]); out.write(encodingTable[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]); out.write(encodingTable[(byte)((byteAcc[1] & 0x0F) << 2)]); out.write(paddingChar); } // 2 padding characters else if (nbBytesWaiting==1) { // 1 byte left out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]); out.write(encodingTable[(byte)((byteAcc[0] & 0x03) << 4)]); out.write(paddingChar); out.write(paddingChar); } // Just in case this method is called again nbBytesWaiting = 0; } ///////////////////////////////// // OutputStream implementation // ///////////////////////////////// @Override public void write(int i) throws IOException { // We have a 3-byte group if(nbBytesWaiting==2) { // Write 3 bytes as 4 base64 characters out.write(encodingTable[(byte)((byteAcc[0] & 0xFC) >> 2)]); out.write(encodingTable[(byte)(((byteAcc[0] & 0x03) << 4) | ((byteAcc[1] & 0xF0) >> 4))]); out.write(encodingTable[(byte)(((byteAcc[1] & 0x0F) << 2) | ((i & 0xC0) >> 6))]); out.write(encodingTable[(byte)(i & 0x3F)]); nbBytesWaiting = 0; // Insert a line break after every 80 characters written if (insertLineBreaks && (lineLength += 4) >= 76) { out.write('\r'); out.write('\n'); lineLength = 0; } } // Waiting for more bytes... else { byteAcc[nbBytesWaiting++] = (byte)i; } } /** * Writes padding if necessary and closes the underlying stream. */ @Override public void close() throws IOException { writePadding(); out.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/base64/Base64Table.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.base64; /** * This class represents an immutable Base64 encoding/decoding table. It provides the correspondance between encoded * and decoded characters (and vice-versa), and the character to use for padding. *

    * A number of common Base64 tables are provided as public static final fields of this class: *

      *
    • {@link #STANDARD_TABLE}
    • *
    • {@link #URL_SAFE_TABLE}
    • *
    • {@link #FILENAME_SAFE_TABLE}
    • *
    • {@link #REGEXP_SAFE_TABLE}
    • *
    * * @author Maxence Bernard */ class Base64Table { /** Encoding table, 64 bytes long */ protected byte[] encodingTable; /** Decoding table, 256 bytes long */ protected int[] decodingTable; /** Padding character used */ protected byte paddingChar; /** The standard Base64 table, using '+' and '/' for non-alphanumerical characters, and '=' for padding */ public final static Base64Table STANDARD_TABLE = createTable((byte)'+', (byte)'/', (byte)'='); /** An URL-safe Base64 table, using '-' and '_' for non-alphanumerical characters, and '.' for padding */ public final static Base64Table URL_SAFE_TABLE = createTable((byte)'-', (byte)'_', (byte)'.'); /** A filename-safe Base64 table, using '+' and '-' for non-alphanumerical characters, and '=' for padding */ public final static Base64Table FILENAME_SAFE_TABLE = createTable((byte)'+', (byte)'-', (byte)'='); /** A regexp-safe Base64 table, using '!' and '-' for non-alphanumerical characters, and '=' for padding */ public final static Base64Table REGEXP_SAFE_TABLE = createTable((byte)'!', (byte)'-', (byte)'='); /** * Creates a base64 table using A–Z, a–z, and 0–9 for the first 62 values, the two specified characters at * position 62 and 63 in the table, and the specified padding character. * * @param char62 ASCII character to use at position 62 of the table * @param char63 ASCII character to use at position 63 of the table * @param paddingChar ASCII character to use for padding * @throws IllegalArgumentException if one specified characters are the same or alphanumerical characters * @return a base64 table */ public static Base64Table createTable(byte char62, byte char63, byte paddingChar) throws IllegalArgumentException { byte[] table = new byte[] { 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 'w','x','y','z','0','1','2','3','4','5','6','7','8','9',char62,char63 }; return new Base64Table(table, paddingChar); } /** * Creates a new Base64Table using the specified character table and padding character. * *

    An IllegalArgumentException if the specified table is not 64 bytes long, contains duplicate * values, or if the specified padding character is present in the table. * *

    The given byte array is cloned before being stored, to avoid any side effect that could be caused by the * byte array being modified inadvertently after this constructor is called. * * @param table the base64 character table. The array must be 64 bytes long and must not contain any duplicate values. * @param paddingChar the ASCII character used for padding. This character must not already be used in the table. * @throws IllegalArgumentException if the specified table is not 64 bytes long, contains duplicate values, or * if the specified padding character is present in the table. */ public Base64Table(byte[] table, byte paddingChar) throws IllegalArgumentException { // Basic length check if(table==null || table.length!=64) throw new IllegalArgumentException("Base64 table is not 64 bytes long"); // create the decoding table and initialize all values to -1 this.decodingTable = new int[256]; char c; for(c=0; c<256; c++) decodingTable[c] = -1; // Fill the decoding table and ensure that characters are used only once for (int i = 0; i < 64; i++) { byte val = table[i]; if (decodingTable[val] != -1) { throw new IllegalArgumentException("Base64 table contains duplicate values"); } decodingTable[val] = i; } // Ensure that the padding character is not already used in the table if (decodingTable[paddingChar] != -1) { throw new IllegalArgumentException("Padding char is already used in Base64 table"); } this.paddingChar = paddingChar; // Clone the byte array so that it cannot be altered externally this.encodingTable = new byte[64]; System.arraycopy(table, 0, this.encodingTable, 0, 64); } /** * Returns the base64 encoding table. The return array should not be modified. * * @return the base64 encoding table. */ byte[] getEncodingTable() { return encodingTable; } /** * Returns the base64 decoding table, containing byte values with the exception of -1 which indicates * the character is not used. The return array should not be modified. * * @return the base64 decoding table. */ int[] getDecodingTable() { return decodingTable; } /** * Returns the ASCII character used for padding. * * @return the ASCII character used for padding. */ byte getPaddingChar() { return paddingChar; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/base64/package.html ================================================ Provides classes to deal with base-64 encoded streams. ================================================ FILE: src/main/java/com/mucommander/commons/io/bom/BOM.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.bom; import java.nio.charset.Charset; /** * BOM represents a Byte-Order Mark, a byte sequence that can be found at the beginning of a Unicode text stream * which indicates the encoding of the text that follows. * * @see BOMInputStream * @author Maxence Bernard */ public class BOM { /** the byte sequence that identifies this BOM */ private final byte[] sig; /** the character encoding denoted by this BOM */ private final String encoding; /** character encoding aliases that map onto this BOM */ private final String[] aliases; /** * Creates a new BOM instance identified by the given signature and denoting the specified * character encoding. * * @param signature the byte sequence that identifies this BOM * @param encoding the character encoding denoted by this BOM * @param aliases character encoding aliases */ BOM(byte[] signature, String encoding, String[] aliases) { this.sig = signature; this.encoding = encoding; this.aliases = aliases; } /** * Returns the byte sequence that identifies this BOM at the beginning of a byte stream. * * @return the byte sequence that identifies this BOM at the beginning of a byte stream */ public byte[] getSignature() { return sig; } /** * Returns the character encoding that this BOM denotes. * * @return the character encoding that this BOM denotes */ public String getEncoding() { return encoding; } /** * Returns a set of character encoding aliases that map onto this BOM. * * @return a set of character encoding aliases that map onto this BOM */ public String[] getAliases() { return aliases; } /** * Returns true if this BOM's signature starts with the given byte sequence. * * @param bytes the byte sequence to compare against this BOM's signature * @return true if this BOM's signature starts with the given byte sequence */ public boolean sigStartsWith(byte[] bytes) { int bytesLen = bytes.length; if (bytesLen > sig.length) { return false; } for (int i = 0; i < bytesLen; i++) { if (bytes[i] != sig[i]) { return false; } } return true; } /** * Returns true if this BOM's signature matches the given byte sequence. * * @param bytes the byte sequence to compare against this BOM's signature * @return true if this BOM's signature matches the given byte sequence */ public boolean sigEquals(byte[] bytes) { return bytes.length==sig.length && sigStartsWith(bytes); } /** * Returns a {@link BOM} instance for the specified encoding, null if the encoding doesn't * have a corresponding BOM (non-Unicode encoding). The search is case-insensitive. * *

    All UTF encoding aliases are supported, in a BOM-neutral way: a BOM is always returned, regardless of * whether the particular encoding requires a BOM to be used or not. For instance, * UTF-16LE and UnicodeLittleUnmarked will both return the {@link BOMConstants#UTF16_LE_BOM} * BOM, even though by specification UTF-16LE and UnicodeLittleUnmarked should not * include a BOM in the data stream. Furthermore, when called with UTF-16 and UTF-32, * the returned BOM will arbitrarily default to big endian and return {@link BOMConstants#UTF16_BE_BOM} and * {@link BOMConstants#UTF32_BE_BOM} respectively. * * @param encoding name of a character encoding * @return a {@link BOM} instance for the specified encoding, null if the encoding doesn't * have a corresponding BOM (non-Unicode encoding). */ public static BOM getInstance(String encoding) { if (!Charset.isSupported(encoding)) { return null; } Charset charset = Charset.forName(encoding); // Retrieve the charset's canonical name for aliases we may not know about encoding = charset.name(); for (int i = 0; itrue if and only if the given Object is a BOM instance with the same * signature as this instance. * * @param o the Object to test for equality * @return true if the specified Object is a BOM instance with the same signature as this instance */ @Override public boolean equals(Object o) { return (o instanceof BOM) && ((BOM)o).sigEquals(sig); } /** * Returns a String representation of this BOM. * * @return returns a String representation of this BOM. */ @Override public String toString() { StringBuilder out = new StringBuilder(super.toString()); out.append(", signature="); for(int i = 0; i < sig.length; i++) { out.append(0xFF&sig[i]); out.append((i == sig.length-1 ? "}" : ", ")); } out.append(", encoding="); out.append(encoding); return out.toString(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/bom/BOMConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.bom; /** * This interface contains constants used by several classes of the BOM package. * * @author Maxence Bernard */ public interface BOMConstants { /** UTF-8 BOM: EF BB BF */ BOM UTF8_BOM = new BOM( new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF}, "UTF-8", new String[]{} ); /** UTF-16 Big Endian BOM: FE FF */ BOM UTF16_BE_BOM = new BOM( new byte[]{(byte)0xFE, (byte)0xFF}, "UTF-16BE", new String[]{"UTF-16", "x-UTF-16BE-BOM" ,"UnicodeBig", "UnicodeBigUnmarked"} ); /** UTF-16 Little Endian BOM: FF FE */ BOM UTF16_LE_BOM = new BOM( new byte[]{(byte)0xFF, (byte)0xFE}, "UTF-16LE", new String[]{"x-UTF-16LE-BOM", "UnicodeLittle", "UnicodeLittleUnmarked"} ); /** UTF-32 Big Endian BOM: 00 00 FE FF. */ BOM UTF32_BE_BOM = new BOM( new byte[]{(byte)0x00, (byte)0x00, (byte)0xFE, (byte)0xFF}, "UTF-32BE", new String[]{"UTF-32", "x-UTF-32BE-BOM"} ); /** UTF-32 Little Endian BOM: FF FE 00 00 */ BOM UTF32_LE_BOM = new BOM( new byte[]{(byte)0xFF, (byte)0xFE, (byte)0x00, (byte)0x00}, "UTF-32LE", new String[]{"x-UTF-32LE-BOM"} ); /** List of supported BOMs */ BOM[] SUPPORTED_BOMS = new BOM[] { UTF8_BOM, UTF16_BE_BOM, UTF16_LE_BOM, UTF32_BE_BOM, UTF32_LE_BOM }; } ================================================ FILE: src/main/java/com/mucommander/commons/io/bom/BOMInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.bom; import java.io.IOException; import java.io.InputStream; /** * BOMInputStream is an InputStream which provides support for Byte-Order Marks (BOM). * A BOM is a byte sequence found at the beginning of a Unicode text stream which indicates the encoding of the text * that follows. * *

    * This class serves a dual purpose:
    * 1) it allows to detect a BOM in the underlying stream and determine the encoding used by the stream: * the {@link BOM} instance returned by {@link #getBOM()} provides that information.
    * 2) it allows to discard the BOM from a Unicode stream: the leading bytes corresponding to the BOM are swallowed by * the stream and never returned by the read methods. * *

    * The following BOMs are supported by this class: *

      *
    • {@link #UTF8_BOM UTF-8}
    • *
    • {@link #UTF16_BE_BOM UTF-16 Big Endian}
    • *
    • {@link #UTF16_LE_BOM UTF-16 Little Endian}
    • *
    • {@link #UTF32_BE_BOM UTF-32 Big Endian}.
    • *
    • {@link #UTF32_LE_BOM UTF-32 Little Endian}
    • *
    * Note that UTF-32 encodings (both Little and Big Endians) are usually not supported by Java runtimes * out of the box. *

    * * @see BOMReader * @author Maxence Bernard */ public class BOMInputStream extends InputStream implements BOMConstants { /** The underlying InputStream that feeds bytes to this stream */ private final InputStream in; /** Contains the BOM that was detected in the stream, null if none was found */ private BOM bom; /** Bytes that were swallowed by this stream when searching for a BOM, null if a BOM was found */ private byte[] leadingBytes; /** Current offset within the {@link #leadingBytes} array */ private int leadingBytesOff; private byte[] oneByteBuf; /** Contains the max signature length of supported BOMs */ private final static int MAX_BOM_LENGTH; static { // Calculates MAX_BOM_LENGTH int maxLen = SUPPORTED_BOMS[0].getSignature().length; int len; for(int i=1; imaxLen) maxLen = len; } MAX_BOM_LENGTH = maxLen; } /** * Creates a new BOMInputStream and looks for a BOM at the beginning of the stream. * * @param in the underlying stream * @throws IOException if an error occurred while reading the given InputStream */ public BOMInputStream(InputStream in) throws IOException { this.in = in; // Read up to MAX_BOM_LENGTH bytes byte[] bytes = new byte[MAX_BOM_LENGTH]; int nbRead; int totalRead = 0; while((nbRead=in.read(bytes, totalRead, MAX_BOM_LENGTH-totalRead))!=-1 && (totalRead+=nbRead)bestMatchLength && startsWith(bytes, tempBomSig)) { bestMatchIndex = i; bestMatchLength = tempBomSig.length; } } // Keep the bytes that do not correspond to a BOM to have the read methods return them if (bestMatchIndex!=-1) { bom = SUPPORTED_BOMS[bestMatchIndex]; if(bestMatchLengthtrue if the first byte sequence starts with the second byte sequence. * * @param b1 first byte array to test * @param b2 second byte array to test * @return true if the first byte sequence starts with the second byte sequence. */ private static boolean startsWith(byte[] b1, byte[] b2) { int b1Len = b1.length; int b2Len = b2.length; if (b1Len < b2Len) { return false; } for(int i = 0; i < b2Len; i++) { if (b2[i] != b1[i]) { return false; } } return true; } /** * Returns the {@link BOM} that was found at the beginning of the stream if there was one, * null otherwise. * * @return the BOM that was found at the beginning of the stream */ public BOM getBOM() { return bom; } @Override public int read() throws IOException { if (oneByteBuf == null) { oneByteBuf = new byte[1]; } int ret = read(oneByteBuf, 0, 1); return ret == -1 ? -1 : oneByteBuf[0]; } @Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); } @Override public int read(byte[] b, int off, int len) throws IOException { if (leadingBytes == null || leadingBytesOff >= leadingBytes.length) { return in.read(b, off, len); } int nbBytes = Math.min(leadingBytes.length-leadingBytesOff, len); System.arraycopy(leadingBytes, leadingBytesOff, b, off, nbBytes); leadingBytesOff += nbBytes; return nbBytes; } @Override public void close() throws IOException { in.close(); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/bom/BOMReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.bom; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; /** * BOMReader is a Reader which provides support for Byte-Order Marks (BOM). * A BOM is a byte sequence found at the beginning of a Unicode text stream which indicates the encoding of the text * that follows. * *

    * This class uses a {@link BOMInputStream} for BOM handling and serves a dual purpose:
    * 1) it allows to auto-detect the encoding when a BOM is present in the underlying stream and use it.
    * 2) it allows to discard the BOM from a Unicode stream: the leading bytes corresponding to the BOM are swallowed by * the stream and never returned by the read methods. * * @see BOMInputStream * @author Maxence Bernard */ public class BOMReader extends InputStreamReader { /** * Creates a new BOMReader. A {@link BOMInputStream} is created on top of the specified * InputStream to auto-detect a potential {@link BOM} and use the associated encoding. * If no BOM is found at the beginning of the stream, UTF-8 encoding is assumed. * * @param in the underlying InputStream * @throws IOException if an error occurred while detecting the BOM or initializing this reader. */ public BOMReader(InputStream in) throws IOException { this(new BOMInputStream(in), "UTF-8"); } /** * Creates a new BOMReader. A {@link BOMInputStream} is created on top of the specified * InputStream to auto-detect a potential {@link BOM} and use the associated encoding. * If no BOM is found at the beginning of the stream, the specified default encoding is assumed. * * @param in the underlying InputStream * @param defaultEncoding the encoding used if the stream doesn't contain a BOM * @throws IOException if an error occurred while detecting the BOM or initializing this reader. */ public BOMReader(InputStream in, String defaultEncoding) throws IOException { this(new BOMInputStream(in), defaultEncoding); } /** * Creates a new BOMReader using the given {@link BOMInputStream}. If the BOMInputStream * does not contain a {@link BOM}, the specified default encoding is assumed. * * @param bomIn the underlying BOMInputStream * @param defaultEncoding the encoding used if the stream doesn't contain a BOM * @throws UnsupportedEncodingException if the encoding associated with the BOM or the default encoding is not * supported by the Java runtime */ public BOMReader(BOMInputStream bomIn, String defaultEncoding) throws UnsupportedEncodingException { super(bomIn, bomIn.getBOM()==null?defaultEncoding:bomIn.getBOM().getEncoding()); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/bom/BOMWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.bom; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; /** * BOMWriter is a Writer that writes a Byte-Order Mark (BOM) at the beginning of a Unicode * stream and subsequently encodes characters in the requested encoding. * * @author Maxence Bernard */ public class BOMWriter extends OutputStreamWriter { /** The underlying InputStream. */ protected OutputStream out; /** The BOM to write at the beginning of the stream, null for none. */ protected BOM bom; /** True if the BOM has already been written if there was one */ protected boolean bomWriteChecked; /** * Creates a new BOMWriter that will write the specified BOM at the beginning of the stream and * subsequently encode characters in the encoding returned by {@link BOM#getEncoding()}. * * @param out the OutputStream to write the encoded data to * @param bom the byte-order mark to write at the beginning of the stream. * @throws UnsupportedEncodingException if the BOM's encoding is not a character encoding supported by the Java runtime. */ public BOMWriter(OutputStream out, BOM bom) throws UnsupportedEncodingException { this(out, bom.getEncoding(), bom); } /** * Creates a new BOMWriter that will encode characters in the specified encoding and, if the * encoding has a corresponding {@link BOM}, write at the beginning of the stream. If a Non-Unicode encoding * is passed, no BOM will be written to the stream and this Writer will act as a regular * {@link OutputStreamWriter}. * *

    It is important to note that a BOM will always be written for Unicode encodings, * even if the particular encoding specifies that no BOM should be written (UnicodeLittleUnmarked for * instance). See {@link BOM#getInstance(String)} for more information about this. * * @param out the OutputStream to write the encoded data to * @param encoding character encoding to use for encoding characters. * @throws UnsupportedEncodingException if the specified encoding is not a character encoding supported by the Java runtime. * @see BOM#getInstance(String) */ public BOMWriter(OutputStream out, String encoding) throws UnsupportedEncodingException { this(out, encoding, BOM.getInstance(encoding)); } /** * Creates a new BOMWriter that will write the specified BOM at the beginning of the stream and * subsequently encode characters in the specified encoding. * * @param out the OutputStream to write the encoded data to * @param bom the byte-order mark to write at the beginning of the stream. * @param encoding character encoding to use for encoding characters. * @throws UnsupportedEncodingException if the specified encoding is not a character encoding supported by the Java runtime. */ protected BOMWriter(OutputStream out, String encoding, BOM bom) throws UnsupportedEncodingException { super(out, encoding); this.out = out; this.bom = bom; } /** * Checks if a BOM is waiting for being written, and if there is, writes it to the underlying output stream. * * @throws IOException if an error occurred while writing the BOM */ protected void checkWriteBOM() throws IOException { if (!bomWriteChecked) { if (bom != null) { out.write(bom.getSignature()); } bomWriteChecked = true; } } @Override public void write(int c) throws IOException { checkWriteBOM(); super.write(c); } @Override public void write(char[] cbuf, int off, int len) throws IOException { checkWriteBOM(); super.write(cbuf, off, len); } @Override public void write(String str, int off, int len) throws IOException { checkWriteBOM(); super.write(str, off, len); } } ================================================ FILE: src/main/java/com/mucommander/commons/io/bom/package.html ================================================ Provides support for Byte-Order Marks (BOM) found in Unicode text streams. ================================================ FILE: src/main/java/com/mucommander/commons/io/compound/CompoundInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.compound; import java.io.IOException; import java.io.InputStream; /** * CompoundInputStream concatenates several input streams into one. It can operate in two modes: *

    *
    Merged
    *
    the compound stream acts as a single, global input stream, merging the contents of underlying streams. The * compound stream can be read just like a regular InputStream -- streams are advanced automatically as EOF * of individual streams are reached.
    * *
    Unmerged
    *
    the compound stream has be advanced manually. EOF are signaled individually for each underlying stream. * After EOF is reached, the current stream has to be advanced to the next one using {@link #advanceInputStream()}.
    *
    * *

    * This class is abstract, with a single method to implement: {@link #getNextInputStream()}. * See {@link IteratorCompoundInputStream} for an Iterator-backed implementation. * * @see IteratorCompoundInputStream * @see CompoundReader * @author Maxence Bernard */ public abstract class CompoundInputStream extends InputStream { /** True if this CompoundInputStream operates in 'merged' mode */ private boolean merged; /** The InputStream that's currently being processed */ private InputStream currentIn; /** Used by {@link #read()} */ private byte oneByteBuf[]; /** true if the global EOF has been reached */ private boolean globalEOFReached; /** * Creates a new CompoundInputStream operating in the specified mode. * * @param merged true if the streams should be merged, acting as a single stream, or considered * as separate streams that have to be {@link #advanceInputStream() advanced manually}. */ public CompoundInputStream(boolean merged) { this.merged = merged; } /** * Returns: *

      *
    • true if this stream acts as a single, global input stream, merging the contents of underlying * streams. In this mode, the compound stream can be read just like a regular InputStream -- streams are advanced * automatically as EOF of individual streams are reached.
    • *
    • false if this stream has be advanced manually. In this mode, EOF are signaled individually * for each underlying stream. After EOF has been reached, the current stream has to be advanced to the next one * using {@link #advanceInputStream()}.
    • *
    * * @return true if this stream acts as a global input stream, false if this stream has * to be advanced manually. */ public boolean isMerged() { return merged; } /** * Returns the InputStream this compound stream is currently reading, null if this stream * hasn't read anything yet, or if it has no underlying stream to read. * * @return InputStream this compound stream is currently reading */ public InputStream getCurrentInputStream() { return currentIn; } /** * Closes the current input stream, if any. This method has no effect if there is no current input stream. * * @throws IOException if an error occurred while closing the current input stream. */ public void closeCurrentInputStream() throws IOException { if(currentIn!=null) currentIn.close(); } /** * Tries to advances the current stream to the next one, causing subsequent calls to InputStream * methods to operate on the new stream. Returns true if there was a next stream, false * otherwise. *

    * Note: the current stream (if any) will be closed by this method. * * @return true if there was a next stream, false otherwise * @throws IOException if an error occurred while trying to advancing the current stream. This * CompoundInputStream can't be used after that and must be closed. */ public boolean advanceInputStream() throws IOException { // Return immediately (don't close the stream) if this method is global EOF has already been reached if(globalEOFReached) return false; // Close the current stream if(currentIn!=null) { try { closeCurrentInputStream(); } catch(IOException e) { // Fail silently } } // Try to advance the current InputStream to the next try { currentIn = getNextInputStream(); } catch(IOException e) { // Can't recover from this, this is the end of this stream globalEOFReached = true; throw e; } if(currentIn==null) { // Global EOF reached globalEOFReached = true; return false; } return true; } /** * Checks the current stream and returns true if the current stream is in a state where it can be * accessed, false if global EOF has been reached. * * @return true if the current stream is in a state where it can be accessed, false if * global EOF has been reached. * @throws IOException if an error occurred while trying to advancing the current stream. */ private boolean checkStream() throws IOException { if(globalEOFReached) return true; if(currentIn==null) return !advanceInputStream(); return false; } ////////////////////// // Abstract methods // ////////////////////// /** * Returns the next InputStream, null if there is none. *

    * Before calling this method, {@link #advanceInputStream()} closes the current stream (if any). In other words, * implementations do not have to worry about closing previously-returned streams. * * @return the next InputStream, null if there is none. * @throws IOException if an error occurred while retrieving the next input stream */ public abstract InputStream getNextInputStream() throws IOException; //////////////////////////////// // InputStream implementation // //////////////////////////////// /** * Delegates to {@link #read(byte[], int, int)} with a 1-byte buffer. */ @Override public int read() throws IOException { if(oneByteBuf==null) oneByteBuf = new byte[1]; int ret = read(oneByteBuf, 0, 1); return ret<=0?ret:oneByteBuf[0]; } /** * Delegates to {@link #read(byte[], int, int)} with a 0 offset and the whole buffer's length. */ @Override public int read(byte[] b) throws IOException { return read(b, 0, b.length); } /** * Reads up to len-off bytes and stores them in the specified byte buffer, starting at off. * Returns the number of bytes that were actually read, or -1 to signal: *

      *
    • if {@link #isMerged()} is true, the end of the compound stream as a whole
    • *
    • if {@link #isMerged ()} is false, the end of the current stream, which may or may not coincide * with the end of the stream as a whole.
    • *
    */ @Override public int read(byte[] b, int off, int len) throws IOException { if(checkStream()) return -1; int ret = currentIn.read(b, off, len); if(ret==-1) { // read the next stream if(merged) { if(!advanceInputStream()) return -1; // Global EOF reached // Recurse return read(b, off, len); } return -1; } return ret; } /** * Skips up to n bytes and returns the number of bytes that were actually skipped, or -1 * to signal: *
      *
    • if {@link #isMerged()} is enabled, the end of the compound stream as a whole
    • *
    • if {@link #isMerged ()} is disabled, the end of the current stream, which may or may not coincide * with the end of the stream as a whole.
    • *
    */ @Override public long skip(long n) throws IOException { if(checkStream()) return -1; long ret = currentIn.skip(n); if(ret==-1) { // read the next stream if(merged) { if(!advanceInputStream()) return -1; // Global EOF reac hed return currentIn.skip(n); } return -1; } return ret; } /** * Closes the current InputStream and this CompoundInputStream a whole. * The current stream can no longer be advanced after this method has been called. * * @throws IOException if the current stream could not be closed. */ @Override public void close() throws IOException { try { if(currentIn!=null) closeCurrentInputStream(); } finally { globalEOFReached = true; } } /** * Delegates to the current InputStream. */ @Override public int available() throws IOException { if(checkStream()) return 0; return currentIn.available(); } /** * Delegates to the current InputStream. */ @Override public void mark(int readlimit) { try { if(!checkStream()) currentIn.mark(readlimit); } catch(IOException e) { // Can't throw an IOException here unfortunately, fail silently } } /** * Delegates to the current InputStream. */ @Override public void reset() throws IOException { if(!checkStream()) currentIn.reset(); } /** * Delegates to the current InputStream. */ @Override public boolean markSupported() { try { return !checkStream() && currentIn.markSupported(); } catch(IOException e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/commons/io/compound/CompoundReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.compound; import java.io.IOException; import java.io.Reader; /** * CompoundReader concatenates several readers into one. It can operate in two modes: *
    *
    Merged
    *
    the compound reader acts as a single, global reader, merging the contents of underlying readers. The * compound reader can be read just like a regular Reader -- readers are advanced automatically as EOF * of individual readers are reached.
    * *
    Unmerged
    *
    the compound reader has be advanced manually. EOF are signaled individually for each underlying reader. * After EOF is reached, the current reader has to be advanced to the next one using {@link #advanceReader()}.
    *
    * *

    * This class is abstract, with a single method to implement: {@link #getNextReader()}. * See {@link IteratorCompoundReader} for an Iterator-backed implementation. * * @see IteratorCompoundReader * @see CompoundInputStream * @author Maxence Bernard */ public abstract class CompoundReader extends Reader { /** True if this CompoundReader operates in 'merged' mode */ private boolean merged; /** The Reader that's currently being processed */ private Reader currentReader; /** Used by {@link #read()} */ private char oneCharBuf[]; /** true if the global EOF has been reached */ private boolean globalEOFReached; /** * Creates a new CompoundReader operating in the specified mode. * * @param merged true if the readers should be merged, acting as a single reader, or considered * as separate readers that have to be {@link #advanceReader() advanced manually}. */ public CompoundReader(boolean merged) { this.merged = merged; } /** * Returns: *

      *
    • true if this reader acts as a single, global input reader, merging the contents of underlying * readers. In this mode, the compound reader can be read just like a regular Reader -- readers are advanced * automatically as EOF of individual readers are reached.
    • *
    • false if this reader has be advanced manually. In this mode, EOF are signaled individually * for each underlying reader. After EOF has been reached, the current reader has to be advanced to the next one * using {@link #advanceReader()}.
    • *
    * * @return true if this reader acts as a global reader, false if this reader has * to be advanced manually. */ public boolean isMerged() { return merged; } /** * Returns the Reader this compound reader is currently reading, null if this reader * hasn't read anything yet, or if it has no underlying reader to read. * * @return Reader this compound reader is currently reading */ public Reader getCurrentReader() { return currentReader; } /** * Closes the current reader, if any. This method has no effect if there is no current reader. * * @throws IOException if an error occurred while closing the current reader. */ public void closeCurrentReader() throws IOException { if(currentReader!=null) currentReader.close(); } /** * Advances the current reader to the next one, causing subsequent calls to Reader * methods to operate on the new reader. Returns true if there was a next reader, false * otherwise. *

    * Note: the current reader (if any) will be closed by this method. * * @return true if there was a next reader, false otherwise * @throws IOException if an error occurred while trying to advancing the current reader. This * CompoundReader can't be used after that and must be closed. */ public boolean advanceReader() throws IOException { // Return immediately (don't close the reader) if this method is global EOF has already been reached if(globalEOFReached) return false; // Close the current reader if(currentReader !=null) { try { closeCurrentReader(); } catch(IOException e) { // Fail silently } } // Try to advance the current Reader to the next try { currentReader = getNextReader(); } catch(IOException e) { // Can't recover from this, this is the end of this stream globalEOFReached = true; throw e; } if(currentReader==null) { // Global EOF reached globalEOFReached = true; return false; } return true; } /** * Checks the current reader and returns true if the current reader is in a state where it can be * accessed, false if global EOF has been reached. * * @return true if the current reader is in a state where it can be accessed, false if * global EOF has been reached. * @throws IOException if an error occurred while trying to advancing the current reader. */ private boolean checkReader() throws IOException { if(globalEOFReached) return true; if(currentReader ==null) return !advanceReader(); return false; } ////////////////////// // Abstract methods // ////////////////////// /** * Returns the next Reader, null if there is none. *

    * Before calling this method, {@link #advanceReader()} closes the current reader (if any). In other words, * implementations do not have to worry about closing previously-returned readers. * * @return the next Reader, null if there is none. * @throws IOException if an error occurred while retrieving the next reader */ public abstract Reader getNextReader() throws IOException; /////////////////////////// // Reader implementation // /////////////////////////// /** * Delegates to {@link #read(char[], int, int)} with a 1-char buffer. */ @Override public int read() throws IOException { if(oneCharBuf ==null) oneCharBuf = new char[1]; int ret = read(oneCharBuf, 0, 1); return ret<=0?ret:oneCharBuf[0]; } /** * Delegates to {@link #read(char[], int, int)} with a 0 offset and the whole buffer's length. */ @Override public int read(char[] c) throws IOException { return read(c, 0, c.length); } /** * Reads up to len-off characters and stores them in the specified buffer, starting at off. * Returns the number of characters that were actually read, or -1 to signal: *

      *
    • if {@link #isMerged()} is enabled, the end of the compound reader as a whole
    • *
    • if {@link #isMerged ()} is disabled, the end of the current reader, which may or may not coincide * with the end of the reader as a whole.
    • *
    */ @Override public int read(char[] c, int off, int len) throws IOException { if(checkReader()) return -1; int ret = currentReader.read(c, off, len); if(ret==-1) { // read the next reader if(merged) { if(!advanceReader()) return -1; // Global EOF reached // Recurse return read(c, off, len); } return -1; } return ret; } /** * Skips up to n characters and returns the number of characters that were actually skipped, or * -1 to signal: *
      *
    • if {@link #isMerged()} is enabled, the end of the compound reader as a whole
    • *
    • if {@link #isMerged ()} is disabled, the end of the current reader, which may or may not coincide * with the end of the reader as a whole.
    • *
    */ @Override public long skip(long n) throws IOException { if(checkReader()) return -1; long ret = currentReader.skip(n); if(ret==-1) { // read the next reader if(merged) { if(!advanceReader()) return -1; // Global EOF reached return currentReader.skip(n); } return -1; } return ret; } /** * Closes the current Reader and this CompoundReader a whole. * The current reader can no longer be advanced after this method has been called. * * @throws IOException if the current reader could not be closed. */ @Override public void close() throws IOException { try { if(currentReader!=null) closeCurrentReader(); } finally { globalEOFReached = true; } } /** * Delegates to the current Reader. */ @Override public boolean ready() throws IOException { return !checkReader() && currentReader.ready(); } /** * Delegates to the current Reader. */ @Override public void mark(int readlimit) throws IOException { if(!checkReader()) currentReader.mark(readlimit); } /** * Delegates to the current Reader. */ @Override public void reset() throws IOException { if(!checkReader()) currentReader.reset(); } /** * Delegates to the current Reader. */ @Override public boolean markSupported() { try { return !checkReader() && currentReader.markSupported(); } catch(IOException e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/commons/io/compound/IteratorCompoundInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.compound; import java.io.InputStream; import java.util.Iterator; /** * A CompoundInputStream implementation using an {@link Iterator} to implement {@link #getNextInputStream()}. * * @author Maxence Bernard */ public class IteratorCompoundInputStream extends CompoundInputStream { /** Iterator containing the InputStreams to be concatenated */ private Iterator inputStreamIterator; /** * Creates a new compound input stream using the {@link InputStream} instances contained by the given * {@link Iterator} and the specified mode. * * @param inputStreamIterator an Iterator that contains the {@link InputStream} instances to be used * by this CompoundInputStream. * @param merged true if the streams should be merged, acting as a single stream, or considered * as separate streams that have to be {@link #advanceInputStream() advanced manually}. */ public IteratorCompoundInputStream(Iterator inputStreamIterator, boolean merged) { super(merged); this.inputStreamIterator = inputStreamIterator; } //////////////////////////////////////// // CompoundInputStream implementation // //////////////////////////////////////// @Override public InputStream getNextInputStream() { return inputStreamIterator.hasNext()?inputStreamIterator.next():null; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/compound/IteratorCompoundReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.compound; import java.io.Reader; import java.util.Iterator; /** * A CompoundReader implementation using an {@link Iterator} to implement {@link #getNextReader()}. * * @author Maxence Bernard */ public class IteratorCompoundReader extends CompoundReader { /** Iterator containing the readers to be concatenated */ private Iterator readerIterator; /** * Creates a new compound reader using the {@link Reader} instances contained by the given * {@link Iterator} and the specified mode. * * @param readerIterator an Iterator that contains the {@link Reader} instances to be used * by this CompoundReader. * @param merged true if the reader should be merged, acting as a single reader, or considered * as separate readers that have to be {@link #advanceReader() advanced manually}. */ public IteratorCompoundReader(Iterator readerIterator, boolean merged) { super(merged); this.readerIterator = readerIterator; } /////////////////////////////////// // CompoundReader implementation // /////////////////////////////////// @Override public Reader getNextReader() { return readerIterator.hasNext()?readerIterator.next():null; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/package.html ================================================ Provides various I/O related classes. ================================================ FILE: src/main/java/com/mucommander/commons/io/security/Adler32MessageDigest.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.security; import java.util.zip.Adler32; /** * Provides a ChecksumMessageDigest implementation of the Adler32 algorithm, using the * java.util.zip.Adler32 class. * * @author Maxence Bernard */ public class Adler32MessageDigest extends ChecksumMessageDigest { public Adler32MessageDigest() { super(new Adler32(), getAlgorithmName()); } /** * Returns the name of the algorithm implemented by this MessageDigest. * * @return the name of the algorithm implemented by this MessageDigest */ protected static String getAlgorithmName() { return "Adler32"; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/security/CRC32MessageDigest.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.security; import java.util.zip.CRC32; /** * Provides a ChecksumMessageDigest implementation of the CRC32 algorithm, using the * java.util.zip.CRC32 class. * * @author Maxence Bernard */ public class CRC32MessageDigest extends ChecksumMessageDigest { public CRC32MessageDigest() { super(new CRC32(), getAlgorithmName()); } /** * Returns the name of the algorithm implemented by this MessageDigest. * * @return the name of the algorithm implemented by this MessageDigest */ protected static String getAlgorithmName() { return "CRC32"; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/security/ChecksumMessageDigest.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.security; import java.security.MessageDigest; import java.util.zip.Checksum; /** * This class turns a java.util.zip.Checksum into a java.security.MessageDigest, allowing * Checksum implementations to be used with the Java Cryptography Extension. * * @author Maxence Bernard */ public class ChecksumMessageDigest extends MessageDigest { /** The Checksum instance that performs all of the checksumming work */ private Checksum checksum; /** * Creates a new ChecksumMessageDigest that delegates all the checksumming work to the given * Checksum instance. * * @param checksum the Checksum responsible for calculating the checksum * @param algorithm the name of the checksum algorithm implemented by the Checksum */ public ChecksumMessageDigest(Checksum checksum, String algorithm) { super(algorithm); this.checksum = checksum; } ////////////////////////////////// // MessageDigest implementation // ////////////////////////////////// /** * This method delegates to the underlying java.util.zip.Checksum instance. */ @Override protected void engineReset() { checksum.reset(); } /** * This method delegates to the underlying java.util.zip.Checksum instance. */ @Override protected void engineUpdate(byte input) { checksum.update(input); } /** * This method delegates to the underlying java.util.zip.Checksum instance. */ @Override protected void engineUpdate(byte[] input, int offset, int len) { checksum.update(input, offset, len); } /** * This method delegates to the underlying java.util.zip.Checksum instance. */ @Override protected byte[] engineDigest() { long crcLong = checksum.getValue(); byte[] crcBytes = new byte[4]; crcBytes[0] = (byte)((crcLong>>24) & 0xFF); crcBytes[1] = (byte)((crcLong>>16) & 0xFF); crcBytes[2] = (byte)((crcLong>>8) & 0xFF); crcBytes[3] = (byte)(crcLong & 0xFF); return crcBytes; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/security/MuProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.io.security; import java.security.Provider; import java.security.Security; /** * This custom java.security.Provider exposes trolCommander's own MessageDigest implementations, * and the ones used aggregated from third party libraries. * *

    The {@link #registerProvider()} method should be called once to register this Provider with the * Java Cryptography Extension and advertise the additional MessageDigest implementations. * * @author Maxence Bernard */ public class MuProvider extends Provider { /** True if an instance of this Provider has already been registered with java.security.Security */ private static boolean initialized; private MuProvider() { super("trolCommander", "1.0", "trolCommander's additional MessageDigest implementations."); } /** * Registers an instance of this Provider with the java.security.Security class, to expose the * additional MessageDigest implementations and have them returned by * java.security.Security.getAlgorithms("MessageDigest"). * This method should be called once */ public static void registerProvider() { // A Provider must be registered only once if (initialized) return; MuProvider provider = new MuProvider(); // Add our own MessageDigest implementations provider.put("MessageDigest."+Adler32MessageDigest.getAlgorithmName(), Adler32MessageDigest.class.getName()); provider.put("MessageDigest."+CRC32MessageDigest.getAlgorithmName(), CRC32MessageDigest.class.getName()); // Register the provider with java.security.Security Security.addProvider(provider); // A Provider must be registered only once initialized = true; } } ================================================ FILE: src/main/java/com/mucommander/commons/io/security/package.html ================================================ This package contains add-ons to the Java Security API that can be registered at runtime using {@link com.mucommander.commons.io.security.MuProvider}. ================================================ FILE: src/main/java/com/mucommander/commons/runtime/ComparableRuntimeProperty.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.runtime; /** * Utility methods for comparing the current runtime's value of this property to this instance * * @author Arik Hadas, Maxence Bernard */ public interface ComparableRuntimeProperty { /** * Returns true if the current runtime's value of this property is equal or lower to this instance, * according to {@link Enum#compareTo(Object)}. * * @return {@code true} if the current runtime's value of this property is equal or lower to this instance */ boolean isCurrentOrLower(); /** * Returns true if the current runtime's value of this property is lower than this instance, * according to {@link Enum#compareTo(Object)}. * * @return {@code true} if the current runtime's value of this property is lower than this instance */ boolean isCurrentLower(); /** * Returns {@code true} if the current runtime's value of this property is equal or higher to this instance, * according to {@link Enum#compareTo(Object)}. * * @return {@code true} if the current runtime's value of this property is equal or higher to this instance */ boolean isCurrentOrHigher(); /** * Returns {@code true} if the current runtime's value of this property is higher than this instance, * according to {@link Enum#compareTo(Object)}. * * @return {@code true} if the current runtime's value of this property is higher than this instance */ boolean isCurrentHigher(); } ================================================ FILE: src/main/java/com/mucommander/commons/runtime/JavaVersion.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.runtime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class represents a major version of Java, like Java 1.5 for instance. The current runtime instance * is determined using the value of the java.version system property. * Being a {@link com.mucommander.commons.runtime.ComparableRuntimeProperty}, versions of Java are ordered and can be compared * against each other. * * @author Maxence Bernard, Arik Hadas */ public enum JavaVersion implements ComparableRuntimeProperty { JAVA_11("11"), /** Java 12.x */ JAVA_12("12"), /** Java 13.x */ JAVA_13("13"), /** Java 14.x */ JAVA_14("14"), /** Java 15.x */ JAVA_15("15"), /** Java 16.x */ JAVA_16("16"), /** Java 17.x */ JAVA_17("17"), /** Java 18.x */ JAVA_18("18"), /** Java 19.x */ JAVA_19("19"), /** Java 20.x */ JAVA_20("20"), /** Java 21.x */ JAVA_21("21"), /** Java 22.x */ JAVA_22("22"), /** Java 23.x */ JAVA_23("23"); /** Logger used by this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(JavaVersion.class); /** Holds the JavaVersion of the current runtime environment */ private static final JavaVersion CURRENT_VALUE; /** Holds the String representation of the current JVM architecture */ private static final String CURRENT_ARCHITECTURE; /** The String representation of this RuntimeProperty, set at creation time */ private final String stringRepresentation; static { CURRENT_VALUE = parseSystemProperty(getRawSystemProperty()); CURRENT_ARCHITECTURE = System.getProperty("os.arch"); LOGGER.info("Current Java version: {}", CURRENT_VALUE); LOGGER.info("Current JVM architecture: {}", CURRENT_ARCHITECTURE); } JavaVersion(String stringRepresentation) { this.stringRepresentation = stringRepresentation; } /** * Returns true if the JVM architecture is amd64 * * @return true if the JVM architecture is amd64, and false otherwise. */ public static boolean isAmd64Architecture() { return "amd64".equals(CURRENT_ARCHITECTURE); } /** * Returns the Java version of the current runtime environment. * * @return the Java version of the current runtime environment */ public static JavaVersion getCurrent() { return CURRENT_VALUE; } /** * Returns the value of the system property which serves to detect the Java version at runtime. * * @return the value of the system property which serves to detect the Java version at runtime. */ public static String getRawSystemProperty() { return System.getProperty("java.version"); } /** * Returns a JavaVersion instance corresponding to the specified system property's value. * * @param s the value of the "java.version" system property * @return a JavaVersion instance corresponding to the specified system property's value */ static JavaVersion parseSystemProperty(String s) { // Java version property should never be null or empty, but better be safe than sorry ... if (s == null || (s = s.trim()).isEmpty()) // Assume java 11 (first supported Java version) return JavaVersion.JAVA_11; if (s.startsWith("22")) { return JavaVersion.JAVA_22; } else if (s.startsWith("21")) { return JavaVersion.JAVA_21; } else if (s.startsWith("20")) { return JavaVersion.JAVA_20; } else if (s.startsWith("19")) { return JavaVersion.JAVA_19; } else if (s.startsWith("18")) { return JavaVersion.JAVA_18; } else if (s.startsWith("17")) { return JavaVersion.JAVA_17; } else if (s.startsWith("16")) { return JavaVersion.JAVA_16; } else if (s.startsWith("15")) { return JavaVersion.JAVA_15; } else if (s.startsWith("14")) { return JavaVersion.JAVA_14; } else if (s.startsWith("13")) { return JavaVersion.JAVA_13; } else if (s.startsWith("12")) { return JavaVersion.JAVA_12; } else if (s.startsWith("11")) { return JavaVersion.JAVA_11; } // Newer version we don't know of yet, assume latest supported Java version return JavaVersion.JAVA_23; } /** * Returns true if this instance is the same instance as the one returned by {@link #getCurrent()}. * * @return true if this instance is the same as the current runtime value */ public boolean isCurrent() { return this == CURRENT_VALUE; } @Override public boolean isCurrentOrLower() { return CURRENT_VALUE.compareTo(this) <= 0; } @Override public boolean isCurrentLower() { return CURRENT_VALUE.compareTo(this) < 0; } @Override public boolean isCurrentOrHigher() { return CURRENT_VALUE.compareTo(this) >= 0; } @Override public boolean isCurrentHigher() { return CURRENT_VALUE.compareTo(this) > 0; } @Override public String toString() { return stringRepresentation; } } ================================================ FILE: src/main/java/com/mucommander/commons/runtime/OsFamily.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.runtime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class represents a non-versioned family of operating system, like Windows or Linux. * The current runtime instance is determined using the value of the os.name system property. * * @see OsVersion * @author Maxence Bernard, Arik Hadas */ public enum OsFamily { /** Windows */ WINDOWS("Windows",false), /** Mac OS X */ MAC_OS_X("Mac OS X"), /** Linux */ LINUX("Linux"), /** Solaris */ SOLARIS("Solaris"), /** OS/2 */ OS_2("OS/2"), /** FreeBSD */ FREEBSD("FreeBSD"), /** AIX */ AIX("AIX"), /** HP-UX */ HP_UX("HP-UX"), /** OpenVMS */ OPENVMS("OpenVMS"), /** Other OS */ UNKNOWN_OS_FAMILY("Unknown"); /** Logger used by this class. */ private static Logger logger; /** The String representation of this RuntimeProperty, set at creation time */ private final String stringRepresentation; private final boolean isCaseSensitiveFilesystem; /** Holds the OsFamily of the current runtime environment */ private static OsFamily currentValue; OsFamily(String stringRepresentation) { this(stringRepresentation,true); } OsFamily(String stringRepresentation, boolean isCaseSensitiveFilesystem) { this.stringRepresentation = stringRepresentation; this.isCaseSensitiveFilesystem = isCaseSensitiveFilesystem; } /** * Returns the OS family of the current runtime environment. * * @return the OS family of the current runtime environment */ public static OsFamily getCurrent() { if (currentValue == null) { currentValue = parseSystemProperty(getRawSystemProperty()); } return currentValue; } /** * Returns true if this OS family is UNIX-based. The following OS families are considered UNIX-based: *

      *
    • {@link #LINUX}
    • *
    • {@link #MAC_OS_X}
    • *
    • {@link #SOLARIS}
    • *
    • {@link #FREEBSD}
    • *
    • {@link #AIX}
    • *
    • {@link #HP_UX}
    • *
    • {@link #UNKNOWN_OS_FAMILY}: the reason for this being that most alternative OSes are Unix-based.
    • *
    * * @return true if the current OS is UNIX-based */ public boolean isUnixBased() { return this == MAC_OS_X || this == LINUX || this == SOLARIS || this == FREEBSD || this == AIX || this == HP_UX || this == UNKNOWN_OS_FAMILY; // Not UNIX-based: WINDOWS, OS/2 and OpenVMS } /** * Returns the value of the system property which serves to detect the OS family at runtime. * * @return the value of the system property which serves to detect the OS family at runtime. */ public static String getRawSystemProperty() { return System.getProperty("os.name"); } public static String getRawOsArch() { return System.getProperty("os.arch"); } public static boolean isAarch64() { return "aarch64".equals(getRawOsArch()); } public static boolean isAmd64() { return "amd64".equals(getRawOsArch()); } /** * Returns an OsFamily instance corresponding to the specified system property's value. * * @param osNameProp the value of the "os.name" system property * @return an OsFamily instance corresponding to the specified system property's value */ static OsFamily parseSystemProperty(String osNameProp) { // This website holds a collection of system property values under many OSes: // http://lopica.sourceforge.net/os.html // Windows family if (osNameProp.startsWith("Windows")) { return WINDOWS; } // Mac OS X family if (osNameProp.startsWith("Mac OS X")) { return MAC_OS_X; } // OS/2 family if (osNameProp.startsWith("OS/2")) { return OS_2; } // Linux family if (osNameProp.startsWith("Linux")) { return LINUX; } // Solaris family if (osNameProp.startsWith("Solaris") || osNameProp.startsWith("SunOS")) { return SOLARIS; } if (osNameProp.startsWith("FreeBSD")) { return FREEBSD; } if (osNameProp.startsWith("AIX")) { return AIX; } if (osNameProp.startsWith("HP-UX")) { return HP_UX; } if (osNameProp.startsWith("OpenVMS")) { return OPENVMS; } // Any other OS return UNKNOWN_OS_FAMILY; } /** * Returns true if this instance is the same instance as the one returned by {@link #getCurrent()}. * * @return true if this instance is the same as the current runtime's value */ public boolean isCurrent() { return this == currentValue; } @Override public String toString() { return stringRepresentation; } public boolean isCaseSensitiveFilesystem() { return isCaseSensitiveFilesystem; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(OsFamily.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/commons/runtime/OsVersion.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.runtime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class represents a major version of an operating system, like Mac OS X 10.5 or * Windows XP. The current runtime value is determined using the value of the os.version * system property and the current {@link OsFamily} instance. * Being a {@link com.mucommander.commons.runtime.ComparableRuntimeProperty}, OS versions are ordered and can be compared * against each other. * * @see OsFamily * @author Maxence Bernard, Arik Hadas */ public enum OsVersion implements ComparableRuntimeProperty { /** Unknown OS version */ UNKNOWN_VERSION("Unknown"), /** Windows 95 */ WINDOWS_95("Windows 95"), /** Windows 98 */ WINDOWS_98("Windows 98"), /** Windows Me */ WINDOWS_ME("Windows Me"), // Windows NT subfamily /** Windows NT */ WINDOWS_NT("Windows NT"), /** Windows 2000 */ WINDOWS_2000("Windows 2000"), /** Windows XP */ WINDOWS_XP("Windows XP"), /** Windows 2003 */ WINDOWS_2003("Windows 2003"), /** Windows Vista */ WINDOWS_VISTA("Windows Vista"), /** Windows 7 */ WINDOWS_7("Windows 7"), /** Windows 8 */ WINDOWS_8("Windows 8"), /** Windows 8 */ WINDOWS_8_1("Windows 8.1"), /** Windows 10 */ WINDOWS_10("Windows 10"), /** Windows 11 */ WINDOWS_11("Windows 11"), /** Mac OS X 10.0 (Cheetah) */ MAC_OS_X_10_0("10.0"), /** Mac OS X 10.1 (Puma) */ MAC_OS_X_10_1("10.1"), /** Mac OS X 10.2 (Jaguar) */ MAC_OS_X_10_2("10.2"), /** Mac OS X 10.3 (Panther) */ MAC_OS_X_10_3("10.3"), /** Mac OS X 10.4 (Tiger) */ MAC_OS_X_10_4("10.4"), /** Mac OS X 10.5 (Leopard) */ MAC_OS_X_10_5("10.5"), /** Mac OS X 10.6 (Snow Leopard) */ MAC_OS_X_10_6("10.6"), /** Mac OS X 10.7 (Lion) */ MAC_OS_X_10_7("10.7"), /** Mac OS X 10.8 (Mountain Lion) */ MAC_OS_X_10_8("10.8"), /** Mac OS X 10.9 (Mavericks) */ MAC_OS_X_10_9("10.9"), /** Mac OS X 10.10 (Yosemite) */ MAC_OS_X_10_10("10.10"), /** Mac OS X 10.11 (El Capitan) */ MAC_OS_X_10_11("10.11"), /** Mac OS X 10.12 (Sierra) */ MAC_OS_X_10_12("10.12"), /** Mac OS X 10.13 (High Sierra) */ MAC_OS_X_10_13("10.13"), /** Mac OS X 10.14 (Mojave) */ MAC_OS_X_10_14("10.14"), /** Mac OS X 10.15 (Catalina) */ MAC_OS_X_10_15("10.15"), /** Big Sur */ MAC_OS_11("11"), /** Monterey */ MAC_OS_12("12"), /** Ventura */ MAC_OS_13("13"), /** Sonoma */ MAC_OS_14("14"); /** Logger used by this class. */ private static final Logger LOGGER = LoggerFactory.getLogger(OsVersion.class); /** The String representation of this RuntimeProperty, set at creation time */ private final String stringRepresentation; /** Holds the OsVersion of the current runtime environment */ private static final OsVersion currentValue; static { currentValue = parseSystemProperty(getRawSystemProperty(), OsFamily.getRawSystemProperty(), OsFamily.getCurrent()); LOGGER.info("Current OS version: {}", currentValue); } OsVersion(String stringRepresentation) { this.stringRepresentation = stringRepresentation; } /** * Returns the OS version of the current runtime environment. * * @return the OS version of the current runtime environment */ public static OsVersion getCurrent() { return currentValue; } /** * Returns the value of the system property which serves to detect the OS version at runtime. * * @return the value of the system property which serves to detect the OS version at runtime. */ public static String getRawSystemProperty() { return System.getProperty("os.version"); } /** * Returns an OsVersion instance corresponding to the specified system property's value. * * @param osVersionProp the value of the "os.version" system property * @param osNameProp the value of the "os.name" system property * @param osFamily the current OS family * @return an OsVersion instance corresponding to the specified system property's value */ static OsVersion parseSystemProperty(String osVersionProp, String osNameProp, OsFamily osFamily) { // This website holds a collection of system property values under many OSes: // http://lopica.sourceforge.net/os.html if (osFamily == OsFamily.WINDOWS) { return switch (osNameProp) { case "Windows 95" -> WINDOWS_95; case "Windows 98" -> WINDOWS_98; case "Windows Me" -> WINDOWS_ME; case "Windows NT" -> WINDOWS_NT; case "Windows 2000" -> WINDOWS_2000; case "Windows XP" -> WINDOWS_XP; case "Windows 2003" -> WINDOWS_2003; case "Windows Vista" -> WINDOWS_VISTA; case "Windows 7" -> WINDOWS_7; case "Windows 8" -> WINDOWS_8; case "Windows 8.1" -> WINDOWS_8_1; case "Windows 10" -> WINDOWS_10; case "Windows 11" -> WINDOWS_11; default -> WINDOWS_11; // Newer version we don't know of yet, assume latest supported OS version }; } // Mac OS X versions if (osFamily == OsFamily.MAC_OS_X) { if (osVersionProp.startsWith("14")) { return MAC_OS_14; } else if (osVersionProp.startsWith("13")) { return MAC_OS_13; } else if (osVersionProp.startsWith("12")) { return MAC_OS_12; } if (osVersionProp.startsWith("11")) { return MAC_OS_11; } else if (osVersionProp.startsWith("10.15")) { return MAC_OS_X_10_15; } else if (osVersionProp.startsWith("10.14")) { return MAC_OS_X_10_14; } else if (osVersionProp.startsWith("10.13")) { return MAC_OS_X_10_13; } else if (osVersionProp.startsWith("10.12")) { return MAC_OS_X_10_12; } else if (osVersionProp.startsWith("10.11")) { return MAC_OS_X_10_11; } else if (osVersionProp.startsWith("10.10")) { return MAC_OS_X_10_10; } else if (osVersionProp.startsWith("10.9")) { return MAC_OS_X_10_9; } else if (osVersionProp.startsWith("10.8")) { return MAC_OS_X_10_8; } else if (osVersionProp.startsWith("10.7")) { return MAC_OS_X_10_7; } else if (osVersionProp.startsWith("10.6")) { return MAC_OS_X_10_6; } else if (osVersionProp.startsWith("10.5")) { return MAC_OS_X_10_5; } else if (osVersionProp.startsWith("10.4")) { return MAC_OS_X_10_4; } else if (osVersionProp.startsWith("10.3")) { return MAC_OS_X_10_3; } else if (osVersionProp.startsWith("10.2")) { return MAC_OS_X_10_2; } else if (osVersionProp.startsWith("10.1")) { return MAC_OS_X_10_1; } else if (osVersionProp.startsWith("10.0")) { return MAC_OS_X_10_0; } return MAC_OS_14; // Newer version we don't know of yet, assume latest supported OS version } return OsVersion.UNKNOWN_VERSION; } /** * Returns true if this instance is the same instance as the one returned by {@link #getCurrent()}. * * @return true if this instance is the same as the current runtime's value */ public boolean isCurrent() { return this == currentValue; } @Override public boolean isCurrentOrLower() { return currentValue.compareTo(this) <= 0; } @Override public boolean isCurrentLower() { return currentValue.compareTo(this) < 0; } @Override public boolean isCurrentOrHigher() { return currentValue.compareTo(this) >= 0; } @Override public boolean isCurrentHigher() { return currentValue.compareTo(this) > 0; } @Override public String toString() { return stringRepresentation; } } ================================================ FILE: src/main/java/com/mucommander/commons/runtime/package.html ================================================ This package provides automatic detection and easy access to certain properties of the runtime environment such as the Java version and the OS family and version. ================================================ FILE: src/main/java/com/mucommander/commons/util/BufferOverflowException.java ================================================ /* * Buffer Overflow Exception * Copyright (C) 2002-2010 Stephen Ostermiller * http://ostermiller.org/contact.pl?regarding=Java+Utilities * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * See LICENSE.txt for details. */ package com.mucommander.commons.util; import java.io.IOException; /** * An indication that there was a buffer overflow. * * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities * @since ostermillerutils 1.00.00 */ public class BufferOverflowException extends IOException { /** * Serial version ID */ private static final long serialVersionUID = -322401823167626048L; /** * create a new Exception * * @since ostermillerutils 1.00.00 */ public BufferOverflowException(){ super(); } /** * create a new Exception with the given message. * * @param msg Error message. * * @since ostermillerutils 1.00.00 */ public BufferOverflowException(String msg){ super(msg); } } ================================================ FILE: src/main/java/com/mucommander/commons/util/CircularByteBuffer.java ================================================ /* * Circular Byte Buffer * Copyright (C) 2002-2010 Stephen Ostermiller * http://ostermiller.org/contact.pl?regarding=Java+Utilities * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * See LICENSE.txt for details. */ package com.mucommander.commons.util; import java.io.*; /** * Implements the Circular Buffer producer/consumer model for bytes. * More information about this class is available from ostermiller.org. *

    * Using this class is a simpler alternative to using a PipedInputStream * and a PipedOutputStream. PipedInputStreams and PipedOutputStreams don't support the * mark operation, don't allow you to control buffer sizes that they use, * and have a more complicated API that requires instantiating two * classes and connecting them. *

    * This class is thread safe. * * @author Stephen Ostermiller http://ostermiller.org/contact.pl?regarding=Java+Utilities * @since ostermillerutils 1.00.00 */ public class CircularByteBuffer { /** * The default size for a circular byte buffer. * * @since ostermillerutils 1.00.00 */ private final static int DEFAULT_SIZE = 1024; /** * A buffer that will grow as things are added. * * @since ostermillerutils 1.00.00 */ public final static int INFINITE_SIZE = -1; /** * The circular buffer. *

    * The actual capacity of the buffer is one less than the actual length * of the buffer so that an empty and a full buffer can be * distinguished. An empty buffer will have the markPostion and the * writePosition equal to each other. A full buffer will have * the writePosition one less than the markPostion. *

    * There are three important indexes into the buffer: * The readPosition, the writePosition, and the markPosition. * If the InputStream has never been marked, the readPosition and * the markPosition should always be the same. The bytes * available to be read go from the readPosition to the writePosition, * wrapping around the end of the buffer. The space available for writing * goes from the write position to one less than the markPosition, * wrapping around the end of the buffer. The bytes that have * been saved to support a reset() of the InputStream go from markPosition * to readPosition, wrapping around the end of the buffer. * * @since ostermillerutils 1.00.00 */ protected byte[] buffer; /** * Index of the first byte available to be read. * * @since ostermillerutils 1.00.00 */ protected volatile int readPosition = 0; /** * Index of the first byte available to be written. * * @since ostermillerutils 1.00.00 */ protected volatile int writePosition = 0; /** * Index of the first saved byte. (To support stream marking.) * * @since ostermillerutils 1.00.00 */ protected volatile int markPosition = 0; /** * Number of bytes that have to be saved * to support mark() and reset() on the InputStream. * * @since ostermillerutils 1.00.00 */ protected volatile int markSize = 0; /** * If this buffer is infinite (should resize itself when full) * * @since ostermillerutils 1.00.00 */ protected volatile boolean infinite = false; /** * True if a write to a full buffer should block until the buffer * has room, false if the write method should throw an IOException * * @since ostermillerutils 1.00.00 */ protected boolean blockingWrite = true; /** * The InputStream that can empty this buffer. * * @since ostermillerutils 1.00.00 */ protected InputStream in = new CircularByteBufferInputStream(); /** * true if the close() method has been called on the InputStream * * @since ostermillerutils 1.00.00 */ protected boolean inputStreamClosed = false; /** * The OutputStream that can fill this buffer. * * @since ostermillerutils 1.00.00 */ protected OutputStream out = new CircularByteBufferOutputStream(); /** * true if the close() method has been called on the OutputStream * * @since ostermillerutils 1.00.00 */ protected boolean outputStreamClosed = false; /** * Make this buffer ready for reuse. The contents of the buffer * will be cleared and the streams associated with this buffer * will be reopened if they had been closed. * * @since ostermillerutils 1.00.00 */ public void clear(){ synchronized (this){ readPosition = 0; writePosition = 0; markPosition = 0; outputStreamClosed = false; inputStreamClosed = false; } } /** * Retrieve a OutputStream that can be used to fill * this buffer. *

    * Write methods may throw a BufferOverflowException if * the buffer is not large enough. A large enough buffer * size must be chosen so that this does not happen or * the caller must be prepared to catch the exception and * try again once part of the buffer has been consumed. * * * @return the producer for this buffer. * * @since ostermillerutils 1.00.00 */ public OutputStream getOutputStream(){ return out; } /** * Retrieve a InputStream that can be used to empty * this buffer. *

    * This InputStream supports marks at the expense * of the buffer size. * * @return the consumer for this buffer. * * @since ostermillerutils 1.00.00 */ public InputStream getInputStream(){ return in; } /** * Get number of bytes that are available to be read. *

    * Note that the number of bytes available plus * the number of bytes free may not add up to the * capacity of this buffer, as the buffer may reserve some * space for other purposes. * * @return the size in bytes of this buffer * * @since ostermillerutils 1.00.00 */ public int getAvailable(){ synchronized (this){ return available(); } } /** * Get the number of bytes this buffer has free for * writing. *

    * Note that the number of bytes available plus * the number of bytes free may not add up to the * capacity of this buffer, as the buffer may reserve some * space for other purposes. * * @return the available space in bytes of this buffer * * @since ostermillerutils 1.00.00 */ public int getSpaceLeft(){ synchronized (this){ return spaceLeft(); } } /** * Get the capacity of this buffer. *

    * Note that the number of bytes available plus * the number of bytes free may not add up to the * capacity of this buffer, as the buffer may reserve some * space for other purposes. * * @return the size in bytes of this buffer * * @since ostermillerutils 1.00.00 */ public int getSize(){ synchronized (this) { return buffer.length; } } /** * double the size of the buffer * * @since ostermillerutils 1.00.00 */ private void resize() { byte[] newBuffer = new byte[buffer.length * 2]; int marked = marked(); int available = available(); if (markPosition <= writePosition){ // any space between the mark and // the first write needs to be saved. // In this case it is all in one piece. int length = writePosition - markPosition; System.arraycopy(buffer, markPosition, newBuffer, 0, length); } else { int length1 = buffer.length - markPosition; System.arraycopy(buffer, markPosition, newBuffer, 0, length1); int length2 = writePosition; System.arraycopy(buffer, 0, newBuffer, length1, length2); } buffer = newBuffer; markPosition = 0; readPosition = marked; writePosition = marked + available; } /** * Space available in the buffer which can be written. * * @since ostermillerutils 1.00.00 */ private int spaceLeft() { if (writePosition < markPosition){ // any space between the first write and // the mark except one byte is available. // In this case it is all in one piece. return (markPosition - writePosition - 1); } // space at the beginning and end. return ((buffer.length - 1) - (writePosition - markPosition)); } /** * Bytes available for reading. * * @since ostermillerutils 1.00.00 */ private int available() { if (readPosition <= writePosition){ // any space between the first read and // the first write is available. In this case i // is all in one piece. return (writePosition - readPosition); } // space at the beginning and end. return (buffer.length - (readPosition - writePosition)); } /** * Bytes saved for supporting marks. * * @since ostermillerutils 1.00.00 */ private int marked() { if (markPosition <= readPosition){ // any space between the markPosition and // the first write is marked. In this case i // is all in one piece. return (readPosition - markPosition); } // space at the beginning and end. return (buffer.length - (markPosition - readPosition)); } /** * If we have passed the markSize reset the * mark so that the space can be used. * * @since ostermillerutils 1.00.00 */ private void ensureMark() { if (marked() > markSize){ markPosition = readPosition; markSize = 0; } } /** * create a new buffer with a default capacity. * Writing to a full buffer will block until space * is available rather than throw an exception. * * @since ostermillerutils 1.00.00 */ public CircularByteBuffer(){ this (DEFAULT_SIZE, true); } /** * create a new buffer with given capacity. * Writing to a full buffer will block until space * is available rather than throw an exception. *

    * Note that the buffer may reserve some bytes for * special purposes and capacity number of bytes may * not be able to be written to the buffer. *

    * Note that if the buffer is of INFINITE_SIZE it will * neither block or throw exceptions, but rather grow * without bound. * * @param size desired capacity of the buffer in bytes or CircularByteBuffer.INFINITE_SIZE. * * @since ostermillerutils 1.00.00 */ public CircularByteBuffer(int size){ this (size, true); } /** * create a new buffer with a default capacity and * given blocking behavior. * * @param blockingWrite true writing to a full buffer should block * until space is available, false if an exception should * be thrown instead. * * @since ostermillerutils 1.00.00 */ public CircularByteBuffer(boolean blockingWrite){ this (DEFAULT_SIZE, blockingWrite); } /** * create a new buffer with the given capacity and * blocking behavior. *

    * Note that the buffer may reserve some bytes for * special purposes and capacity number of bytes may * not be able to be written to the buffer. *

    * Note that if the buffer is of INFINITE_SIZE it will * neither block or throw exceptions, but rather grow * without bound. * * @param size desired capacity of the buffer in bytes or CircularByteBuffer.INFINITE_SIZE. * @param blockingWrite true writing to a full buffer should block * until space is available, false if an exception should * be thrown instead. * * @since ostermillerutils 1.00.00 */ public CircularByteBuffer(int size, boolean blockingWrite) { if (size == INFINITE_SIZE) { buffer = new byte[DEFAULT_SIZE]; infinite = true; } else { buffer = new byte[size]; infinite = false; } this.blockingWrite = blockingWrite; } /** * Class for reading from a circular byte buffer. * * @since ostermillerutils 1.00.00 */ protected class CircularByteBufferInputStream extends InputStream { /** * Returns the number of bytes that can be read (or skipped over) from this * input stream without blocking by the next caller of a method for this input * stream. The next caller might be the same thread or or another thread. * * @return the number of bytes that can be read from this input stream without blocking. * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public int available() throws IOException { synchronized (CircularByteBuffer.this) { if (inputStreamClosed) { throw new IOException("InputStream has been closed, it is not ready."); } return (CircularByteBuffer.this.available()); } } /** * Close the stream. Once a stream has been closed, further read(), available(), * mark(), or reset() invocations will throw an IOException. Closing a * previously-closed stream, however, has no effect. * * @throws IOException never. * * @since ostermillerutils 1.00.00 */ @Override public void close() throws IOException { synchronized (CircularByteBuffer.this) { inputStreamClosed = true; } } /** * Mark the present position in the stream. Subsequent calls to reset() will * attempt to reposition the stream to this point. *

    * The readAheadLimit must be less than the size of circular buffer, otherwise * this method has no effect. * * @param readAheadLimit Limit on the number of bytes that may be read while * still preserving the mark. After reading this many bytes, attempting to * reset the stream will fail. * * @since ostermillerutils 1.00.00 */ @Override public void mark(int readAheadLimit) { synchronized (CircularByteBuffer.this) { //if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot mark a closed InputStream."); if (buffer.length - 1 > readAheadLimit) { markSize = readAheadLimit; markPosition = readPosition; } } } /** * Tell whether this stream supports the mark() operation. * * @return true, mark is supported. * * @since ostermillerutils 1.00.00 */ @Override public boolean markSupported() { return true; } /** * Read a single byte. * This method will block until a byte is available, an I/O error occurs, * or the end of the stream is reached. * * @return The byte read, as an integer in the range 0 to 255 (0x00-0xff), * or -1 if the end of the stream has been reached * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public int read() throws IOException { while (true) { synchronized (CircularByteBuffer.this){ if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot read from a closed InputStream."); int available = CircularByteBuffer.this.available(); if (available > 0) { int result = buffer[readPosition] & 0xff; readPosition++; if (readPosition == buffer.length){ readPosition = 0; } ensureMark(); return result; } else if (outputStreamClosed) { return -1; } } try { Thread.sleep(100); } catch(Exception x){ throw new IOException("Blocking read operation interrupted."); } } } /** * Read bytes into an array. * This method will block until some input is available, * an I/O error occurs, or the end of the stream is reached. * * @param cbuf Destination buffer. * @return The number of bytes read, or -1 if the end of * the stream has been reached * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public int read(byte[] cbuf) throws IOException { return read(cbuf, 0, cbuf.length); } /** * Read bytes into a portion of an array. * This method will block until some input is available, * an I/O error occurs, or the end of the stream is reached. * * @param cbuf Destination buffer. * @param off Offset at which to start storing bytes. * @param len Maximum number of bytes to read. * @return The number of bytes read, or -1 if the end of * the stream has been reached * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public int read(byte[] cbuf, int off, int len) throws IOException { while (true) { synchronized (CircularByteBuffer.this) { if (inputStreamClosed) { throw new IOException("InputStream has been closed; cannot read from a closed InputStream."); } int available = CircularByteBuffer.this.available(); if (available > 0) { int length = Math.min(len, available); int firstLen = Math.min(length, buffer.length - readPosition); int secondLen = length - firstLen; System.arraycopy(buffer, readPosition, cbuf, off, firstLen); if (secondLen > 0){ System.arraycopy(buffer, 0, cbuf, off+firstLen, secondLen); readPosition = secondLen; } else { readPosition += length; } if (readPosition == buffer.length) { readPosition = 0; } ensureMark(); return length; } else if (outputStreamClosed){ return -1; } } try { Thread.sleep(100); } catch(Exception x){ throw new IOException("Blocking read operation interrupted."); } } } /** * Reset the stream. * If the stream has been marked, then attempt to reposition i * at the mark. If the stream has not been marked, or more bytes * than the readAheadLimit have been read, this method has no effect. * * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public void reset() throws IOException { synchronized (CircularByteBuffer.this){ if (inputStreamClosed) { throw new IOException("InputStream has been closed; cannot reset a closed InputStream."); } readPosition = markPosition; } } /** * Skip bytes. * This method will block until some bytes are available, * an I/O error occurs, or the end of the stream is reached. * * @param n The number of bytes to skip * @return The number of bytes actually skipped * @throws IllegalArgumentException if n is negative. * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public long skip(long n) throws IOException, IllegalArgumentException { while (true) { synchronized (CircularByteBuffer.this){ if (inputStreamClosed) throw new IOException("InputStream has been closed; cannot skip bytes on a closed InputStream."); int available = CircularByteBuffer.this.available(); if (available > 0){ int length = Math.min((int)n, available); int firstLen = Math.min(length, buffer.length - readPosition); int secondLen = length - firstLen; if (secondLen > 0){ readPosition = secondLen; } else { readPosition += length; } if (readPosition == buffer.length) { readPosition = 0; } ensureMark(); return length; } else if (outputStreamClosed){ return 0; } } try { Thread.sleep(100); } catch(Exception x){ throw new IOException("Blocking read operation interrupted."); } } } } /** * Class for writing to a circular byte buffer. * If the buffer is full, the writes will either block * until there is some space available or throw an IOException * based on the CircularByteBuffer's preference. * * @since ostermillerutils 1.00.00 */ protected class CircularByteBufferOutputStream extends OutputStream { /** * Close the stream, flushing it first. * This will cause the InputStream associated with this circular buffer * to read its last bytes once it empties the buffer. * Once a stream has been closed, further write() or flush() invocations * will cause an IOException to be thrown. Closing a previously-closed stream, * however, has no effect. * * @throws IOException never. * * @since ostermillerutils 1.00.00 */ @Override public void close() throws IOException { synchronized (CircularByteBuffer.this){ if (!outputStreamClosed && !inputStreamClosed){ flush(); } outputStreamClosed = true; } } /** * Flush the stream. * * @throws IOException if the stream is closed. * * @since ostermillerutils 1.00.00 */ @Override public void flush() throws IOException { synchronized (CircularByteBuffer.this){ if (outputStreamClosed) throw new IOException("OutputStream has been closed; cannot flush a closed OutputStream."); if (inputStreamClosed) throw new IOException("Buffer closed by inputStream; cannot flush."); } // this method needs to do nothing } /** * Write an array of bytes. * If the buffer allows blocking writes, this method will block until * all the data has been written rather than throw an IOException. * * @param cbuf Array of bytes to be written * @throws BufferOverflowException if buffer does not allow blocking writes * and the buffer is full. If the exception is thrown, no data * will have been written since the buffer was set to be non-blocking. * @throws IOException if the stream is closed, or the write is interrupted. * * @since ostermillerutils 1.00.00 */ @Override public void write(byte[] cbuf) throws IOException { write(cbuf, 0, cbuf.length); } /** * Write a portion of an array of bytes. * If the buffer allows blocking writes, this method will block until * all the data has been written rather than throw an IOException. * * @param cbuf Array of bytes * @param off Offset from which to start writing bytes * @param len - Number of bytes to write * @throws BufferOverflowException if buffer does not allow blocking writes * and the buffer is full. If the exception is thrown, no data * will have been written since the buffer was set to be non-blocking. * @throws IOException if the stream is closed, or the write is interrupted. * * @since ostermillerutils 1.00.00 */ @Override public void write(byte[] cbuf, int off, int len) throws IOException { while (len > 0) { synchronized (CircularByteBuffer.this) { if (outputStreamClosed) { throw new IOException("OutputStream has been closed; cannot write to a closed OutputStream."); } if (inputStreamClosed) { throw new IOException("Buffer closed by InputStream; cannot write to a closed buffer."); } int spaceLeft = spaceLeft(); while (infinite && spaceLeft < len){ resize(); spaceLeft = spaceLeft(); } if (!blockingWrite && spaceLeft < len) { throw new BufferOverflowException("CircularByteBuffer is full; cannot write " + len + " bytes"); } int realLen = Math.min(len, spaceLeft); int firstLen = Math.min(realLen, buffer.length - writePosition); int secondLen = Math.min(realLen - firstLen, buffer.length - markPosition - 1); int written = firstLen + secondLen; if (firstLen > 0){ System.arraycopy(cbuf, off, buffer, writePosition, firstLen); } if (secondLen > 0){ System.arraycopy(cbuf, off+firstLen, buffer, 0, secondLen); writePosition = secondLen; } else { writePosition += written; } if (writePosition == buffer.length) { writePosition = 0; } off += written; len -= written; } if (len > 0){ try { Thread.sleep(100); } catch(Exception x){ throw new IOException("Waiting for available space in buffer interrupted."); } } } } /** * Write a single byte. * The byte to be written is contained in the 8 low-order bits of the * given integer value; the 24 high-order bits are ignored. * If the buffer allows blocking writes, this method will block until * all the data has been written rather than throw an IOException. * * @param c number of bytes to be written * @throws BufferOverflowException if buffer does not allow blocking writes * and the buffer is full. * @throws IOException if the stream is closed, or the write is interrupted. * * @since ostermillerutils 1.00.00 */ @Override public void write(int c) throws IOException { boolean written = false; while (!written) { synchronized (CircularByteBuffer.this){ if (outputStreamClosed) { throw new IOException("OutputStream has been closed; cannot write to a closed OutputStream."); } if (inputStreamClosed) { throw new IOException("Buffer closed by InputStream; cannot write to a closed buffer."); } int spaceLeft = spaceLeft(); while (infinite && spaceLeft < 1){ resize(); spaceLeft = spaceLeft(); } if (!blockingWrite && spaceLeft < 1) { throw new BufferOverflowException("CircularByteBuffer is full; cannot write 1 byte"); } if (spaceLeft > 0){ buffer[writePosition] = (byte)(c & 0xff); writePosition++; if (writePosition == buffer.length) { writePosition = 0; } written = true; } } if (!written){ try { Thread.sleep(100); } catch(Exception x){ throw new IOException("Waiting for available space in buffer interrupted."); } } } } } } ================================================ FILE: src/main/java/com/mucommander/commons/util/Pair.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.util; /** * Simple structure of two elements. * * Note that this class does not support concurrent usage, and does not exposes setters and * getters methods, on purpose in order to expose simple API similar to C++ pair structure. * * @author Arik Hadas */ public class Pair { /* first element */ public FIRST first; /* second element */ public SECOND second; /** * Empty Constructor * The elements should be assigned after the class instantiation */ public Pair() { } /** * Constructor that take the two elements as parameters * * @param first - first element * @param second - second element */ public Pair(FIRST first, SECOND second) { this.first = first; this.second = second; } } ================================================ FILE: src/main/java/com/mucommander/commons/util/StringUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2010 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.commons.util; import java.text.Collator; import java.util.Locale; /** * This class contains convenience methods for working with strings. * @author Maxence Bernard, Nicolas Rinaudo */ public final class StringUtils { public static final String EMPTY = ""; /** * Prevents instantiation of this class. */ private StringUtils() { } /** * Returns true if a ends with b regardless of the case. *

    * This method has a known bug under some alphabets with peculiar capitalization rules such as the Georgian one, * where Character.toUpperCase(a) == Character.toUpperCase(b) doesn't necessarily imply that * Character.toLowerCase(a) == Character.toLowerCase(b). * The performance hit of testing for these exceptions is so huge that it was deemed an acceptable issue. *

    * Note that this method will return true if b is an empty string. * * @param a string to test. * @param b suffix to test for. * @return true if a ends with b regardless of the case, false otherwise. */ public static boolean endsWithIgnoreCase(String a, String b) { return matchesIgnoreCase(a, b, a.length()); } /** * Returns true if the substring of a starting at posA matches b regardless of the case. *

    * This method has a known bug under some alphabets with peculiar capitalization rules such as the Georgian one, * where Character.toUpperCase(a) == Character.toUpperCase(b) doesn't necessarily imply that * Character.toLowerCase(a) == Character.toLowerCase(b). The performance hit of testing for these * exceptions is so huge that it was deemed an acceptable issue. *

    * Note that this method will return true if b is an empty string. * * @param a string to test. * @param b suffix to test for. * @param posA position in a at which to look for b * @return true if a ends with b regardless of the case, false otherwise. * @throws ArrayIndexOutOfBoundsException if a.length is smaller than posA. */ public static boolean matchesIgnoreCase(String a, String b, int posA) { int posB = b.length(); // Position in b. // Checks whether there's any point in testing the strings. if (posA < posB) return false; // Loops until we've tested the whole of b. while (posB > 0) { char cA = a.charAt(--posA); // Current character in a. // Works on lower-case characters only. if (!Character.isLowerCase(cA)) { cA = Character.toLowerCase(cA); } char cB = b.charAt(--posB); // Current character in b. if (!Character.isLowerCase(cB)) { cB = Character.toLowerCase(cB); } if (cA != cB) { return false; } } return true; } /** *

    * Checks if str contains a search string searchStr regardless of case, handling {@code null}. *

    * A {@code null} string will return {@code false}. * *

         * StringUtils.contains(null, *) = false
         * StringUtils.contains(*, null) = false
         * StringUtils.contains("", "") = true
         * StringUtils.contains("abc", "") = true
         * StringUtils.contains("abc", "a") = true
         * StringUtils.contains("abc", "z") = false
         * StringUtils.contains("abc", "A") = true
         * StringUtils.contains("abc", "Z") = false
         * 
    * * @param str string to test, may be null. * @param searchStr the string to find, may be null. * @return true if str contains b regardless of the case, false otherwise. */ public static boolean containsIgnoreCase(String str, String searchStr, Locale locale) { if (str == null || searchStr == null) { return false; } if (searchStr.isEmpty()) { return true; } return str.toLowerCase(locale).contains(searchStr.toLowerCase(locale)); } /** * Returns true if a ends with b regardless of the case. *

    * This method has a known bug under some alphabets with peculiar capitalization rules such as the Georgian one, * where Character.toUpperCase(a) == Character.toUpperCase(b) doesn't necessarily imply that * Character.toLowerCase(a) == Character.toLowerCase(b). The performance hit of testing for these * exceptions is so huge that it was deemed an acceptable issue. *

    * Note that this method will return true if b is an empty string. * * @param a string to test. * @param b suffix to test for. * @return true if a ends with b regardless of the case, false otherwise. */ public static boolean endsWithIgnoreCase(String a, char[] b) { return matchesIgnoreCase(a, b, a.length()); } /** * Returns true if the substring of a starting at posA matches b regardless of the case. *

    * This method has a known bug under some alphabets with peculiar capitalisation rules such as the Georgian one, * where Character.toUpperCase(a) == Character.toUpperCase(b) doesn't necessarily imply that * Character.toLowerCase(a) == Character.toLowerCase(b). The performance hit of testing for this * exceptions is so huge that it was deemed an acceptable issue. *

    * Note that this method will return true if b is an empty string. * * @param a string to test. * @param b suffix to test for. * @param posA position in a at which to look for b * @return true if a ends with b regardless of the case, false otherwise. * @throws ArrayIndexOutOfBoundsException if a.length is smaller than posA. */ public static boolean matchesIgnoreCase(String a, char[] b, int posA) { int posB = b.length; // Position in b. // Checks whether there's any point in testing the strings. if (posA < posB) return false; while (posB > 0) { char cA = a.charAt(--posA); // Current character in a. if (!Character.isLowerCase(cA)) { cA = Character.toLowerCase(cA); } char cB = b[--posB]; // Current character in b. if (!Character.isLowerCase(cB)) { cB = Character.toLowerCase(cB); } if (cA != cB) { return false; } } return true; } /** * Equivalent of String.endsWith(String) using a char[]. * @param a String to test. * @param b suffix to test. * @return true if a ends with b. */ public static boolean endsWith(String a, char[] b) { return matches(a, b, a.length()); } /** * Returns true if the substring of a ending at posA matches b. * @param a String to test. * @param b substring to look for. * @param posA position in a at which to look for b * @return true if a contains b at position posA - b.length(), false otherwise.. */ public static boolean matches(String a, char[] b, int posA) { int posB = b.length; if (posA < posB) { return false; } while (posB > 0) { if (a.charAt(--posA) != b[--posB]) { return false; } } return true; } /** * Returns true if a starts with b regardless of the case. *

    * Note that this method will return true if b is an empty string. * * @param a string to test. * @param b prefix to test for. * @return true if a starts with b regardless of the case, false otherwise.. */ public static boolean startsWithIgnoreCase(String a, String b) { return a.regionMatches(true, 0, b, 0, b.length()); } /** * This method is a locale-aware version of java.lang.String#equals(Object). It returns * true if the two given String are equal in the specified Locale. * *

    This method is useful for testing text expressed in a language where two strings with an identical * written representation can have a different String representation according to * java.lang.String#equals(Object). Japanese is such a language for instance. * This method uses the java.text.Collator class under the hood. * * @param s1 a String to compare * @param s2 a String to compare * @param locale the Locale to consider for testing the String * @return true if the two given String are equal in the specified Locale */ public static boolean equals(String s1, String s2, Locale locale) { return Collator.getInstance(locale).equals(s1, s2); } /** * Compares the two specified strings and returns true if both strings are equal. This method handles * null values with no risk of a NullPointerException. The comparison is case-sensitive * only if requested. * *

    In other words, this method returns true if strings are either both null * or equal according to String#equals(String) for case-sensitive comparison, or * String#equalsIgnoreCase(String) for case-insensitive comparison. * * @param s1 string to compare, potentially null * @param s2 string to compare, potentially null * @param caseSensitive true for case-sensitive comparison, false for case-insensitive comparison * @return true if strings are equal or both null */ public static boolean equals(String s1, String s2, boolean caseSensitive) { if (s1 == null && s2 == null) return true; if (caseSensitive) return s1 != null && s1.equals(s2); return s1 != null && s1.equalsIgnoreCase(s2); } /** * Parses the string argument as a signed decimal integer. If the string cannot be * parsed a default value is returned. * @param s a String containing the int representation to be parsed * @param def a default value if string cannot be parsed * @return the integer value represented by the argument or default value if it cannot be parsed */ public static int parseIntDef(String s, int def) { try { return Integer.parseInt(s); } catch (NumberFormatException e) { return def; } } /** * Capitalizes the given string, making its first character upper case, and the rest of them lower case. * This method returns an empty string if null or an empty string is passed. * * @param s the string to capitalize * @return the capitalized string */ public static String capitalize(String s) { if (isNullOrEmpty(s)) { return EMPTY; } StringBuilder out = new StringBuilder(s.length()); out.append(Character.toUpperCase(s.charAt(0))); if (s.length() > 1) { out.append(s.substring(1).toLowerCase()); } return out.toString(); } /** * Shorthand for {@link #flatten(String[], String)} invoked with a " " separator. * * @param s the string array to flatten * @return the flattened string array */ public static String flatten(String[] s) { return flatten(s, " "); } /** * Concatenates all the given string array's elements into a single string, separating each element * by the specified separator string. null and empty string values are simply skipped and not * reflected in the returned string. If the string array is null, the returned string will also be * null. * * @param s the string array to flatten * @param separator the String that separates each * @return the flattened string array */ public static String flatten(String[] s, String separator) { if (s == null) { return null; } StringBuilder sb = new StringBuilder(); boolean first = true; for (String el : s) { if (isNullOrEmpty(el)) { continue; } if (first) { first = false; } else { sb.append(separator); } sb.append(el); } return sb.toString(); } /** * Returns true if the given string is null or empty (i.e. it's length is 0) * * @param string - the given String to check * @return true if the given string is null or empty, false otherwise */ public static boolean isNullOrEmpty(String string) { return string == null || string.isEmpty(); } /** * Returns true if the given string is null or empty, or it's trimmed value is empty (i.e. it's length is 0) * * @param string - the given String to check * @return true if the given string is null or empty or whitespace only, false otherwise * * @see String#trim() */ public static boolean isNullOrBlank(String string) { return isNullOrEmpty(string) || string.trim().isEmpty(); } public static boolean isNumber(String string) { try { Integer.parseInt(string); return true; } catch (NumberFormatException e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/commons/util/package-info.java ================================================ /** * Contains various utility classes. * @author Nicolas Rinaudo, Maxence Bernard */ package com.mucommander.commons.util; ================================================ FILE: src/main/java/com/mucommander/conf/TcConfigurationFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import com.mucommander.PlatformManager; import com.mucommander.commons.conf.ConfigurationSource; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.io.backup.BackupOutputStream; import java.io.*; import java.nio.charset.StandardCharsets; /** * This abstract package-protected class represents configuration file of muCommander as configuration source * for the mucommander.commons.conf package. * It can point to a file in a given path or to the default file located in the preferences folder if no path was given. * * @author Nicolas Rinaudo, Arik Hadas */ abstract class TcConfigurationFile implements ConfigurationSource { /** Path to the configuration file. */ private AbstractFile configurationFile; /** Default configuration file name. */ private final String DEFAULT_CONFIGURATION_FILE_NAME; /** * Creates a new MuConfigurationSource on the specified file. * @param path path to the configuration file. * @throws FileNotFoundException if path is not accessible. */ TcConfigurationFile(String path, String defaultFilename) throws FileNotFoundException { DEFAULT_CONFIGURATION_FILE_NAME = defaultFilename; if (path != null) { setConfigurationFile(path); } } // - Configuration file handling ------------------------------------------------ // ------------------------------------------------------------------------------ /** * Returns the path to the configuration file. * @return the path to the configuration file. * @throws IOException if an error occurred while locating the default configuration file. */ private synchronized AbstractFile getConfigurationFile() throws IOException { if (configurationFile == null) { AbstractFile folder = PlatformManager.getPreferencesFolder(); return folder.getChild(DEFAULT_CONFIGURATION_FILE_NAME); } return configurationFile; } /** * Sets the path to the configuration file. * @param path path to the file that should be used for configuration storage. * @throws FileNotFoundException if the specified file is not a valid file. */ private synchronized void setConfigurationFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setConfigurationFile(new File(path)); } else { setConfigurationFile(file); } } /** * Sets the path to the configuration file. * @param file path to the file that should be used for configuration storage. * @throws FileNotFoundException if the specified file is not a valid file. */ private synchronized void setConfigurationFile(File file) throws FileNotFoundException { setConfigurationFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the configuration file. * @param file path to the file that should be used for configuration storage. * @throws FileNotFoundException if the specified file is not a valid file. */ private synchronized void setConfigurationFile(AbstractFile file) throws FileNotFoundException { // Makes sure file can be used as a configuration. if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } configurationFile = file; } /** * Returns an input stream on the configuration file. * @return an input stream on the configuration file. */ @Override public synchronized Reader getReader() throws IOException { InputStream is = new BackupInputStream(getConfigurationFile()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8); return new BufferedReader(reader); } /** * Returns an output stream on the configuration file. * @return an output stream on the configuration file. */ @Override public synchronized Writer getWriter() throws IOException { OutputStream os = new BackupOutputStream(getConfigurationFile()); Writer writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); return new BufferedWriter(writer); } @Override public boolean isExists() throws IOException { return getConfigurationFile().exists(); } } ================================================ FILE: src/main/java/com/mucommander/conf/TcConfigurations.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import java.io.FileNotFoundException; import java.io.IOException; import com.mucommander.commons.conf.Configuration; import com.mucommander.commons.conf.ConfigurationException; import com.mucommander.commons.conf.ConfigurationListener; /** * This class contains the configurations of trolCommander and exposes their API methods. * It provides global access to the configurations without using singletons. * * @author Arik Hadas */ public class TcConfigurations { /** Static configurations of trolCommander */ private static final TcPreferences preferences = new TcPreferences(); /** Dynamic configurations of trolCommander */ private static final TcSnapshot snapshot = new TcSnapshot(); public static TcPreferencesAPI getPreferences() { return preferences; } public static void loadPreferences() throws IOException, ConfigurationException { preferences.read(); } public static void savePreferences() throws IOException, ConfigurationException { preferences.write(); } public static void setPreferencesFile(String path) throws FileNotFoundException { preferences.setConfigurationFile(path); } public static boolean isPreferencesFileExists() throws IOException { return preferences.isFileExists(); } public static void addPreferencesListener(ConfigurationListener listener) { preferences.addConfigurationListener(listener); } public static void removePreferencesListener(ConfigurationListener listener) { preferences.removeConfigurationListener(listener); } public static Configuration getSnapshot() { return snapshot.getConfiguration(); } public static void loadSnapshot() throws IOException, ConfigurationException { snapshot.read(); } public static void saveSnapshot() throws IOException, ConfigurationException { snapshot.write(); } } ================================================ FILE: src/main/java/com/mucommander/conf/TcPreference.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; /** * * @author Arik Hadas */ public enum TcPreference { CHECK_FOR_UPDATE(TcPreferences.CHECK_FOR_UPDATE), DATE_FORMAT(TcPreferences.DATE_FORMAT), DATE_SEPARATOR(TcPreferences.DATE_SEPARATOR), TIME_FORMAT(TcPreferences.TIME_FORMAT), LANGUAGE(TcPreferences.LANGUAGE), DISPLAY_COMPACT_FILE_SIZE(TcPreferences.DISPLAY_COMPACT_FILE_SIZE), CONFIRM_ON_QUIT(TcPreferences.CONFIRM_ON_QUIT), SHOW_SPLASH_SCREEN(TcPreferences.SHOW_SPLASH_SCREEN), LOOK_AND_FEEL(TcPreferences.LOOK_AND_FEEL), CUSTOM_LOOK_AND_FEELS(TcPreferences.CUSTOM_LOOK_AND_FEELS), ENABLE_SYSTEM_NOTIFICATIONS(TcPreferences.ENABLE_SYSTEM_NOTIFICATIONS), PREFERRED_ENCODINGS(TcPreferences.PREFERRED_ENCODINGS), LOG_LEVEL(TcPreferences.LOG_LEVEL), LOG_BUFFER_SIZE(TcPreferences.LOG_BUFFER_SIZE), CUSTOM_SHELL(TcPreferences.CUSTOM_SHELL), USE_CUSTOM_SHELL(TcPreferences.USE_CUSTOM_SHELL), SHELL_HISTORY_SIZE(TcPreferences.SHELL_HISTORY_SIZE), SHELL_ENCODING(TcPreferences.SHELL_ENCODING), AUTODETECT_SHELL_ENCODING(TcPreferences.AUTODETECT_SHELL_ENCODING), SMTP_SERVER(TcPreferences.SMTP_SERVER), SMTP_PORT(TcPreferences.SMTP_PORT), MAIL_SENDER_NAME(TcPreferences.MAIL_SENDER_NAME), MAIL_SENDER_ADDRESS(TcPreferences.MAIL_SENDER_ADDRESS), COMMAND_BAR_VISIBLE(TcPreferences.COMMAND_BAR_VISIBLE), COMMAND_BAR_ICON_SCALE(TcPreferences.COMMAND_BAR_ICON_SCALE), STATUS_BAR_VISIBLE(TcPreferences.STATUS_BAR_VISIBLE), TOOLBAR_VISIBLE(TcPreferences.TOOLBAR_VISIBLE), TOOLBAR_ICON_SCALE(TcPreferences.TOOLBAR_ICON_SCALE), VOLUME_EXCLUDE_REGEXP(TcPreferences.VOLUME_EXCLUDE_REGEXP), SHOW_HIDDEN_FILES(TcPreferences.SHOW_HIDDEN_FILES), SHOW_DS_STORE_FILES(TcPreferences.SHOW_DS_STORE_FILES), SHOW_SYSTEM_FOLDERS(TcPreferences.SHOW_SYSTEM_FOLDERS), TABLE_ICON_SCALE(TcPreferences.TABLE_ICON_SCALE), AUTO_SIZE_COLUMNS(TcPreferences.AUTO_SIZE_COLUMNS), USE_SYSTEM_FILE_ICONS(TcPreferences.USE_SYSTEM_FILE_ICONS), SHOW_FOLDERS_FIRST(TcPreferences.SHOW_FOLDERS_FIRST), FOLDERS_ALWAYS_ALPHABETICAL(TcPreferences.FOLDERS_ALWAYS_ALPHABETICAL), SHOW_QUICK_SEARCH_MATCHES_FIRST(TcPreferences.SHOW_QUICK_SEARCH_MATCHES_FIRST), QUICK_SEARCH_TIMEOUT(TcPreferences.QUICK_SEARCH_TIMEOUT), CD_FOLLOWS_SYMLINKS(TcPreferences.CD_FOLLOWS_SYMLINKS), USE_BRUSHED_METAL(TcPreferences.USE_BRUSHED_METAL), USE_SCREEN_MENU_BAR(TcPreferences.USE_SCREEN_MENU_BAR), STARTUP_FOLDERS(TcPreferences.STARTUP_FOLDERS), LEFT_CUSTOM_FOLDER(TcPreferences.LEFT_CUSTOM_FOLDER), RIGHT_CUSTOM_FOLDER(TcPreferences.RIGHT_CUSTOM_FOLDER), REFRESH_CHECK_PERIOD(TcPreferences.REFRESH_CHECK_PERIOD), WAIT_AFTER_REFRESH(TcPreferences.WAIT_AFTER_REFRESH), PROGRESS_DIALOG_EXPANDED(TcPreferences.PROGRESS_DIALOG_EXPANDED), PROGRESS_DIALOG_CLOSE_WHEN_FINISHED(TcPreferences.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED), THEME_TYPE(TcPreferences.THEME_TYPE), THEME_NAME(TcPreferences.THEME_NAME), SYNTAX_THEME_NAME(TcPreferences.SYNTAX_THEME_NAME), ENABLE_BONJOUR_DISCOVERY(TcPreferences.ENABLE_BONJOUR_DISCOVERY), LIST_HIDDEN_FILES(TcPreferences.LIST_HIDDEN_FILES), SMB_LM_COMPATIBILITY(TcPreferences.SMB_LM_COMPATIBILITY), SMB_USE_EXTENDED_SECURITY(TcPreferences.SMB_USE_EXTENDED_SECURITY), SHOW_TAB_HEADER(TcPreferences.SHOW_SINGLE_TAB_HEADER), CALCULATE_FOLDER_SIZE_ON_MARK(TcPreferences.CALCULATE_FOLDER_SIZE_ON_MARK), FILE_GROUP_1_MASK(TcPreferences.FILE_GROUP_1_MASK), FILE_GROUP_2_MASK(TcPreferences.FILE_GROUP_2_MASK), FILE_GROUP_3_MASK(TcPreferences.FILE_GROUP_3_MASK), FILE_GROUP_4_MASK(TcPreferences.FILE_GROUP_4_MASK), FILE_GROUP_5_MASK(TcPreferences.FILE_GROUP_5_MASK), FILE_GROUP_6_MASK(TcPreferences.FILE_GROUP_6_MASK), FILE_GROUP_7_MASK(TcPreferences.FILE_GROUP_7_MASK), FILE_GROUP_8_MASK(TcPreferences.FILE_GROUP_8_MASK), FILE_GROUP_9_MASK(TcPreferences.FILE_GROUP_9_MASK), FILE_GROUP_10_MASK(TcPreferences.FILE_GROUP_10_MASK), CUSTOM_EXTERNAL_TERMINAL(TcPreferences.CUSTOM_EXTERNAL_TERMINAL), EXTERNAL_TERMINAL_TYPE(TcPreferences.EXTERNAL_TERMINAL_TYPE), TERMINAL_SHELL(TcPreferences.TERMINAL_SHELL), TERMINAL_USE_CUSTOM_SHELL(TcPreferences.TERMINAL_USE_CUSTOM_SHELL), FIND_FILE_ENCODING(TcPreferences.FIND_FILE_ENCODING), FIND_FILE_SUBDIRECTORIES(TcPreferences.FIND_FILE_SUBDIRECTORIES), FIND_FILE_ARCHIVES(TcPreferences.FIND_FILE_ARCHIVES), FIND_FILE_IGNORE_HIDDEN(TcPreferences.FIND_FILE_IGNORE_HIDDEN), FIND_FILE_CASE_SENSITIVE(TcPreferences.FIND_FILE_CASE_SENSITIVE), FIND_FILE_SEARCH_HEX(TcPreferences.FIND_FILE_SEARCH_HEX); private final String label; TcPreference(String label) { this.label = label; } @Override public String toString() { return label; } } ================================================ FILE: src/main/java/com/mucommander/conf/TcPreferences.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; import com.mucommander.RuntimeConstants; import com.mucommander.commons.conf.Configuration; import com.mucommander.commons.conf.ConfigurationException; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.conf.ValueList; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.icon.FileIcons; /** * trolCommander specific wrapper for the com.mucommander.conf API which is used to save 'static' configurations. * 'static' configurations refer to properties that can be changed from the preference's dialog only. * those properties do not change often. * * @author Nicolas Rinaudo, Maxence Bernard, Arik Hadas */ public class TcPreferences implements TcPreferencesAPI { // - Misc. variables ----------------------------------------------------- // ----------------------------------------------------------------------- /** Whether to automatically check for updates on startup. */ public static final String CHECK_FOR_UPDATE = "check_for_updates_on_startup"; /** Default automated update behavior. */ public static final boolean DEFAULT_CHECK_FOR_UPDATE = true; /** Description of the format dates should be displayed with. */ public static final String DATE_FORMAT = "date_format"; /** Default date format. */ public static final String DEFAULT_DATE_FORMAT = "MM/dd/yy"; /** Character used to separate years, months and days in a date. */ public static final String DATE_SEPARATOR = "date_separator"; /** Default date separator. */ public static final String DEFAULT_DATE_SEPARATOR = "/"; /** Description of the format timestamps should be displayed with.*/ public static final String TIME_FORMAT = "time_format"; /** Default time format. */ public static final String DEFAULT_TIME_FORMAT = "hh:mm a"; /** Language trolCommander should use when looking for text. */ public static final String LANGUAGE = "language"; /** Whether to display compact file sizes. */ public static final String DISPLAY_COMPACT_FILE_SIZE = "display_compact_file_size"; /** Default file size display behavior. */ public static final boolean DEFAULT_DISPLAY_COMPACT_FILE_SIZE = true; /** Whether to ask the user for confirmation before quitting trolCommander. */ public static final String CONFIRM_ON_QUIT = "quit_confirmation"; /** Default quitting behavior. */ public static final boolean DEFAULT_CONFIRM_ON_QUIT = true; /** Whether to display splash screen when starting trolCommander. */ public static final String SHOW_SPLASH_SCREEN = "show_splash_screen"; /** Default splash screen behavior. */ public static final boolean DEFAULT_SHOW_SPLASH_SCREEN = true; /** Look and feel used by trolCommander. */ public static final String LOOK_AND_FEEL = "lookAndFeel"; /** All registered custom Look and feels. */ public static final String CUSTOM_LOOK_AND_FEELS = "custom_look_and_feels"; /** Separator used to tokenize the custom look and feels variable. */ public static final String CUSTOM_LOOK_AND_FEELS_SEPARATOR = ";"; /** Controls whether system notifications are enabled. */ public static final String ENABLE_SYSTEM_NOTIFICATIONS = "enable_system_notifications"; /** System notifications are enabled by default on platforms where a notifier is available and works well enough. * In particular, the system tray notifier is available under Linux+Java 1.6, but it doesn't work well, so it is not * enabled by default. */ public static final boolean DEFAULT_ENABLE_SYSTEM_NOTIFICATIONS = OsFamily.MAC_OS_X.isCurrent() || OsFamily.WINDOWS.isCurrent(); /** List of encodings that are displayed in encoding selection components. */ public static final String PREFERRED_ENCODINGS = "preferred_encodings"; // - Log variables ------------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the log CONFIGURATION. */ public static final String LOG_SECTION = "log"; /** Log level. */ public static final String LOG_LEVEL = LOG_SECTION + '.' + "level"; /** Default log level. */ public static final String DEFAULT_LOG_LEVEL = "WARNING"; /** Log buffer size, in number of messages. */ public static final String LOG_BUFFER_SIZE = LOG_SECTION + '.' + "buffer_size"; /** Default log buffer size. Should be set to a low value to minimize memory usage, yet high enough to have most of * the recent log messages. */ public static final int DEFAULT_LOG_BUFFER_SIZE = 200; // - Shell variables ----------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the shell CONFIGURATION. */ public static final String SHELL_SECTION = "shell"; /** Shell invocation command (in case trolCommander is not using the default one). */ public static final String CUSTOM_SHELL = SHELL_SECTION + '.' + "custom_command"; /** Whether to use a custom shell invocation command. */ public static final String USE_CUSTOM_SHELL = SHELL_SECTION + '.' + "use_custom"; /** Default custom shell behavior. */ public static final boolean DEFAULT_USE_CUSTOM_SHELL = false; /** Maximum number of items that should be present in the shell history. */ public static final String SHELL_HISTORY_SIZE = SHELL_SECTION + '.' + "history_size"; /** Default maximum shell history size. */ public static final int DEFAULT_SHELL_HISTORY_SIZE = 100; /** Encoding used to read the shell output. */ public static final String SHELL_ENCODING = SHELL_SECTION + '.' + "encoding"; /** Whether shell encoding should be auto-detected. */ public static final String AUTODETECT_SHELL_ENCODING = SHELL_SECTION + '.' + "autodetect_encoding"; /** Default shell encoding auto-detection behavior. */ public static final boolean DEFAULT_AUTODETECT_SHELL_ENCODING = true; // - Terminal variables ----------------------------------------------------- public static final int TERMINAL_DEFAULT = 0; public static final int TERMINAL_CUSTOM = 1; public static final int TERMINAL_ITERM = 2; // MacOS only /** Section describing the terminal (external and built in) CONFIGURATION. */ public static final String TERMINAL_SECTION = "terminal"; /** Terminal invocation command. */ public static final String CUSTOM_EXTERNAL_TERMINAL = TERMINAL_SECTION + '.' + "custom_external_command"; /** Whether to use a custom shell invocation Terminal command. */ public static final String EXTERNAL_TERMINAL_TYPE = TERMINAL_SECTION + '.' + "external_type"; /** Default custom terminal behavior. */ public static final int DEFAULT_TERMINAL_TYPE = TERMINAL_DEFAULT; /** Terminal shell command. */ public static final String TERMINAL_SHELL = TERMINAL_SECTION + '.' + "shell"; public static final String TERMINAL_USE_CUSTOM_SHELL = TERMINAL_SECTION + '.' + "use_custom_shell"; public static final boolean DEFAULT_TERMINAL_USE_CUSTOM_SHELL = false; // - Mail variables ------------------------------------------------------ // ----------------------------------------------------------------------- /** Section describing mail CONFIGURATION. */ public static final String MAIL_SECTION = "mail"; /** Address of the SMTP server that should be used when sending mails. */ public static final String SMTP_SERVER = MAIL_SECTION + '.' + "smtp_server"; /** Outgoing TCP port to the SMTP server. */ public static final String SMTP_PORT = MAIL_SECTION + '.' + "smtp_port"; /** Default outgoing TCP port to the SMTP server. */ public static final int DEFAULT_SMTP_PORT = 25; /** Name under which mails sent by trolCommander should appear. */ public static final String MAIL_SENDER_NAME = MAIL_SECTION + '.' + "sender_name"; /** Address which mails sent by trolCommander should be replied to. */ public static final String MAIL_SENDER_ADDRESS = MAIL_SECTION + '.' + "sender_address"; // - Command bar variables ----------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the command bar CONFIGURATION. */ public static final String COMMAND_BAR_SECTION = "command_bar"; /** Whether the command bar is visible. */ public static final String COMMAND_BAR_VISIBLE = COMMAND_BAR_SECTION + '.' + "visible"; /** Default command bar visibility. */ public static final boolean DEFAULT_COMMAND_BAR_VISIBLE = true; /** Scale factor of commandbar icons. */ public static final String COMMAND_BAR_ICON_SCALE = COMMAND_BAR_SECTION + '.' + "icon_scale"; /** Default scale factor of commandbar icons. */ public static final float DEFAULT_COMMAND_BAR_ICON_SCALE = 1.0f; // - Status bar variables ------------------------------------------------ // ----------------------------------------------------------------------- /** Section describing the status bar CONFIGURATION. */ public static final String STATUS_BAR_SECTION = "status_bar"; /** Whether the status bar is visible. */ public static final String STATUS_BAR_VISIBLE = STATUS_BAR_SECTION + '.' + "visible"; /** Default status bar visibility. */ public static final boolean DEFAULT_STATUS_BAR_VISIBLE = true; // - Toolbar variables --------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the toolbar CONFIGURATION. */ public static final String TOOLBAR_SECTION = "toolbar"; /** Whether the toolbar is visible. */ public static final String TOOLBAR_VISIBLE = TOOLBAR_SECTION + '.' + "visible"; /** Default toolbar visibility. */ public static final boolean DEFAULT_TOOLBAR_VISIBLE = true; /** Scale factor of toolbar icons. */ public static final String TOOLBAR_ICON_SCALE = TOOLBAR_SECTION + '.' + "icon_scale"; /** Default scale factor of toolbar icons. */ public static final float DEFAULT_TOOLBAR_ICON_SCALE = 1.0f; // - Volume list --------------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the volume list CONFIGURATION. */ public static final String VOLUME_LIST_SECTION = "volume_list"; /** Regexp that allows volumes to be excluded from the list. */ public static final String VOLUME_EXCLUDE_REGEXP = VOLUME_LIST_SECTION + '.' + "exclude_regexp"; // - FileTable variables --------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the folders view CONFIGURATION. */ public static final String FILE_TABLE_SECTION = "file_table"; /** Identifier of the left file table. */ public static final String LEFT = "left"; /** Identifier of the right file table. */ public static final String RIGHT = "right"; /** Whether to display hidden files. */ public static final String SHOW_HIDDEN_FILES = FILE_TABLE_SECTION + '.' + "show_hidden_files"; /** Default hidden files visibility. */ public static final boolean DEFAULT_SHOW_HIDDEN_FILES = true; /** Whether to display OS X .DS_Store files. */ public static final String SHOW_DS_STORE_FILES = FILE_TABLE_SECTION + '.' + "show_ds_store_files"; /** Default .DS_Store files visibility. */ public static final boolean DEFAULT_SHOW_DS_STORE_FILES = true; /** Whether to display system folders. */ public static final String SHOW_SYSTEM_FOLDERS = FILE_TABLE_SECTION + '.' + "show_system_folders"; /** Default system folders visibility. */ public static final boolean DEFAULT_SHOW_SYSTEM_FOLDERS = true; /** Scale factor of file table icons. */ public static final String TABLE_ICON_SCALE = FILE_TABLE_SECTION + '.' + "icon_scale"; /** Default scale factor of file table icons. */ public static final float DEFAULT_TABLE_ICON_SCALE = 1.0f; /** Whether columns should resize themselves automatically. */ public static final String AUTO_SIZE_COLUMNS = FILE_TABLE_SECTION + '.' + "auto_size_columns"; /** Default columns auto-resizing behavior. */ public static final boolean DEFAULT_AUTO_SIZE_COLUMNS = true; /** Controls if and when system file icons should be used instead of custom file icons. */ public static final String USE_SYSTEM_FILE_ICONS = FILE_TABLE_SECTION + '.' + "use_system_file_icons"; /** Default system file icons policy. */ public static final String DEFAULT_USE_SYSTEM_FILE_ICONS = FileIcons.USE_SYSTEM_ICONS_APPLICATIONS; /** Controls whether folders are displayed first in the FileTable or mixed with regular files. */ public static final String SHOW_FOLDERS_FIRST = FILE_TABLE_SECTION + '.' + "show_folders_first"; /** Controls whether folders are always sorted alphabetical, doesn't matter which sort is set for the files. */ public static final String FOLDERS_ALWAYS_ALPHABETICAL = FILE_TABLE_SECTION + '.' + "folders_always_alphabetical"; /** Default value for 'Show folders first' option. */ public static final boolean DEFAULT_SHOW_FOLDERS_FIRST = true; /** Default value for 'Folders always alphabetical' option. */ public static final boolean DEFAULT_FOLDERS_ALWAYS_ALPHABETICAL = false; /** Controls whether symlinks should be followed when changing directory. */ public static final String CD_FOLLOWS_SYMLINKS = FILE_TABLE_SECTION + '.' + "cd_follows_symlinks"; /** Default value for 'Follow symlinks when changing directory' option. */ public static final boolean DEFAULT_CD_FOLLOWS_SYMLINKS = false; /** Whether to always show the header of a single tab or not */ public static final String SHOW_SINGLE_TAB_HEADER = FILE_TABLE_SECTION + '.' + "show_single_tab_header"; /** Default value for 'Always show single tab header' */ public static final boolean DEFAULT_SHOW_TAB_HEADER = false; /** Whether to calculate folder size when marking that folder */ public static final String CALCULATE_FOLDER_SIZE_ON_MARK = FILE_TABLE_SECTION + '.' + "calculate_folder_size_on_mark"; /** Default value for 'Calculate folder size on mark' */ public static final boolean DEFAULT_CALCULATE_FOLDER_SIZE_ON_MARK = false; /** Name of the root element's attribute that contains the version of trolCommander used to write the CONFIGURATION file. */ static final String VERSION_ATTRIBUTE = "version"; // - Mac OS X variables -------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing trolCommander's Mac OS X integration. */ public static final String MAC_OSX_SECTION = "macosx"; /** Whether to use the brushed metal look. */ public static final String USE_BRUSHED_METAL = MAC_OSX_SECTION + '.' + "brushed_metal_look"; /** Default brushed metal look behavior. */ // At the time of writing, the 'brushed metal' look causes the JVM to crash randomly under Leopard (10.5) // so we disable brushed metal on that OS version but leave it for earlier versions where it works fine. public static final boolean DEFAULT_USE_BRUSHED_METAL = false; /** Whether to use a Mac OS X style menu bar. */ public static final String USE_SCREEN_MENU_BAR = MAC_OSX_SECTION + '.' + "screen_menu_bar"; /** Default menu bar type. */ public static final boolean DEFAULT_USE_SCREEN_MENU_BAR = true; // - Startup folder variables -------------------------------------------- // ----------------------------------------------------------------------- /** Section describing trolCommander's startup folders. */ public static final String STARTUP_FOLDER_SECTION = "startup_folder"; /** Startup folder type (for the two panels) */ public static final String STARTUP_FOLDERS = STARTUP_FOLDER_SECTION + '.' + "on_startup"; /** The custom folder should be used on startup. */ public static final String STARTUP_FOLDERS_CUSTOM = "customFolders"; /** The last visited folder should be used on startup. */ public static final String STARTUP_FOLDERS_LAST = "lastFolders"; /** Default startup folder type. */ public static final String DEFAULT_STARTUP_FOLDERS = STARTUP_FOLDERS_LAST; /** Section describing custom folders that were set for the two panel. */ public static final String CUSTOM_FOLDERS_SECTION = STARTUP_FOLDER_SECTION + '.' + "custom_folders"; /** Path to a custom startup folders Section describing the right panel's startup folder. */ public static final String RIGHT_CUSTOM_FOLDER = CUSTOM_FOLDERS_SECTION + '.' + RIGHT; /** Section describing the left panel's startup folder. */ public static final String LEFT_CUSTOM_FOLDER = CUSTOM_FOLDERS_SECTION + '.' + LEFT; // - Folder monitoring variables ----------------------------------------- // ----------------------------------------------------------------------- /** Section describing the automatic folder refresh behavior. */ static final String REFRESH_SECTION = "auto_refresh"; /** Frequency at which the current folder is checked for updates, -1 to disable auto refresh. */ public static final String REFRESH_CHECK_PERIOD = REFRESH_SECTION + '.' + "check_period"; /** Default folder refresh frequency. */ public static final long DEFAULT_REFRESH_CHECK_PERIOD = 3000; /** Minimum amount of time a folder should be checked for updates after it's been refreshed. */ public static final String WAIT_AFTER_REFRESH = REFRESH_SECTION + '.' + "wait_after_refresh"; /** Default minimum amount of time between two refreshes. */ public static final long DEFAULT_WAIT_AFTER_REFRESH = 10000; // - Quick search variables ----------------------------------------- // ----------------------------------------------------------------------- static final String QUICK_SEARCH_SECTION = "quick_search"; /** Controls whether matched files are displayed first in the FileTable or mixed with other files on quick search operation. */ static final String SHOW_QUICK_SEARCH_MATCHES_FIRST = QUICK_SEARCH_SECTION + '.' + "show_quick_search_matches_first"; /** Default value for 'Show matches first' option. */ public static final boolean DEFAULT_SHOW_QUICK_SEARCH_MATCHES_FIRST = true; static final String QUICK_SEARCH_TIMEOUT = QUICK_SEARCH_SECTION + '.' + "timeout"; /** Quick search timeout in ms. No timeout if <= 0 */ public static final int DEFAULT_QUICK_SEARCH_TIMEOUT = 5000; // - Progress dialog variables ------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the behavior of the progress dialog. */ public static final String PROGRESS_DIALOG_SECTION = "progress_dialog"; /** Whether the progress dialog is expanded or not. */ public static final String PROGRESS_DIALOG_EXPANDED = PROGRESS_DIALOG_SECTION + '.' + "expanded"; /** Default progress dialog expanded state. */ public static final boolean DEFAULT_PROGRESS_DIALOG_EXPANDED = true; /** Controls whether the progress dialog should be closed when the job is finished. */ public static final String PROGRESS_DIALOG_CLOSE_WHEN_FINISHED = PROGRESS_DIALOG_SECTION + '.' + "close_when_finished"; /** Default progress dialog behavior when the job is finished. */ public static final boolean DEFAULT_PROGRESS_DIALOG_CLOSE_WHEN_FINISHED = true; // - Variables used for themes ------------------------------------------- // ----------------------------------------------------------------------- /** Section controlling which theme should be applied to trolCommander. */ public static final String THEME_SECTION = "theme"; /** Current theme type (custom, predefined or user defined). */ public static final String THEME_TYPE = THEME_SECTION + '.' + "type"; /** Describes predefined themes. */ public static final String THEME_PREDEFINED = "predefined"; /** Describes custom themes. */ public static final String THEME_CUSTOM = "custom"; /** Describes the user theme. */ public static final String THEME_USER = "user"; /** Default theme type. */ public static final String DEFAULT_THEME_TYPE = THEME_PREDEFINED; /** Name of the current theme. */ public static final String THEME_NAME = THEME_SECTION + '.' + "path"; /** Name of the current theme. */ public static final String SYNTAX_THEME_NAME = "editor.syntax.path"; /** Default current theme name. */ public static final String DEFAULT_THEME_NAME = RuntimeConstants.DEFAULT_THEME; /** Default current editor syntax theme name. */ public static final String DEFAULT_SYNTAX_THEME_NAME = "Default"; // - Variables used by Bonjour/Zeroconf support -------------------------- // ----------------------------------------------------------------------- /** Section controlling parameters related to Bonjour/Zeroconf support. */ public static final String BONJOUR_SECTION = "bonjour"; /** Used do determine whether discovery of Bonjour services should be activated or not. */ public static final String ENABLE_BONJOUR_DISCOVERY = BONJOUR_SECTION + '.' + "discovery_enabled"; /** Default Bonjour discovery activation used on startup. */ public static final boolean DEFAULT_ENABLE_BONJOUR_DISCOVERY = !OsFamily.MAC_OS_X.isCurrent(); // - Variables used for FTP ---------------------------------------------- // ----------------------------------------------------------------------- /** Section containing all FTP variables. */ public static final String FTP_SECTION = "ftp"; /** Controls whether hidden files should be listed by the client (LIST -al instead of LIST -l). */ public static final String LIST_HIDDEN_FILES = FTP_SECTION + '.' + "list_hidden_files"; /** Default value for {@link #LIST_HIDDEN_FILES}. */ public static final boolean DEFAULT_LIST_HIDDEN_FILES = false; // - Variables used for SMB ---------------------------------------------- // ----------------------------------------------------------------------- /** Section containing all SMB variables. */ public static final String SMB_SECTION = "smb"; /** Controls the authentication protocol to use when connecting to SMB servers. */ public static final String SMB_LM_COMPATIBILITY = SMB_SECTION + '.' + "lm_compatibility"; /** Default value for {@link #SMB_LM_COMPATIBILITY}. */ public static final int DEFAULT_SMB_LM_COMPATIBILITY = 0; /** Controls the authentication protocol to use when connecting to SMB servers. */ public static final String SMB_USE_EXTENDED_SECURITY = SMB_SECTION + '.' + "use_extended_security"; /** Default value for {@link #SMB_USE_EXTENDED_SECURITY}. */ public static final boolean DEFAULT_SMB_USE_EXTENDED_SECURITY = false; // - File group masks ---------------------------------------------------- // ----------------------------------------------------------------------- static final String FILE_GROUP_SECTION = "file_groups"; static final String FILE_GROUP_1_MASK = FILE_GROUP_SECTION + ".files1"; static final String FILE_GROUP_2_MASK = FILE_GROUP_SECTION + ".files2"; static final String FILE_GROUP_3_MASK = FILE_GROUP_SECTION + ".files3"; static final String FILE_GROUP_4_MASK = FILE_GROUP_SECTION + ".files4"; static final String FILE_GROUP_5_MASK = FILE_GROUP_SECTION + ".files5"; static final String FILE_GROUP_6_MASK = FILE_GROUP_SECTION + ".files6"; static final String FILE_GROUP_7_MASK = FILE_GROUP_SECTION + ".files7"; static final String FILE_GROUP_8_MASK = FILE_GROUP_SECTION + ".files8"; static final String FILE_GROUP_9_MASK = FILE_GROUP_SECTION + ".files9"; static final String FILE_GROUP_10_MASK = FILE_GROUP_SECTION + ".files10"; // - Find file dialog ---------------------------------------------------- // ----------------------------------------------------------------------- static final String FIND_FILE_SECTION = "find_file"; static final String FIND_FILE_ENCODING = FIND_FILE_SECTION + ".encoding"; static final String FIND_FILE_SUBDIRECTORIES = FIND_FILE_SECTION + ".subdirectories"; static final String FIND_FILE_ARCHIVES = FIND_FILE_SECTION + ".archives"; static final String FIND_FILE_IGNORE_HIDDEN = FIND_FILE_SECTION + ".ignore_hidden"; static final String FIND_FILE_CASE_SENSITIVE = FIND_FILE_SECTION + ".case_sensitive"; static final String FIND_FILE_SEARCH_HEX = FIND_FILE_SECTION + ".search_hex"; private static final String ROOT_ELEMENT = "preferences"; // - Instance fields ----------------------------------------------------- // ----------------------------------------------------------------------- private Configuration configuration; private String configurationVersion; /** * Prevents instantiation of this class from outside this package. */ TcPreferences() { TcPreferencesFile muPreferencesFile = TcPreferencesFile.getPreferencesFile(); configuration = new Configuration(muPreferencesFile, new VersionedXmlConfigurationReaderFactory(), new VersionedXmlConfigurationWriterFactory(ROOT_ELEMENT)); } // - Configuration reading / writing ------------------------------------- // ----------------------------------------------------------------------- /** * Loads the trolCommander CONFIGURATION. * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a CONFIGURATION related error occurs. */ void read() throws IOException, ConfigurationException { VersionedXmlConfigurationReader reader = new VersionedXmlConfigurationReader(); configuration.read(reader); // Ensure backward compatibility configurationVersion = reader.getVersion(); if (configurationVersion == null || !configurationVersion.equals(RuntimeConstants.VERSION)) { // Rename preferences that have changed (from v0.8.5) configuration.renameVariable("show_hidden_files", SHOW_HIDDEN_FILES); configuration.renameVariable("auto_size_columns", AUTO_SIZE_COLUMNS); configuration.renameVariable("show_toolbar", TOOLBAR_VISIBLE); configuration.renameVariable("show_status_bar", STATUS_BAR_VISIBLE); configuration.renameVariable("show_command_bar", COMMAND_BAR_VISIBLE); } // Initializes MAC OS X specific values if (OsFamily.MAC_OS_X.isCurrent()) { if (configuration.getVariable(SHELL_ENCODING) == null) { configuration.setVariable(SHELL_ENCODING, "UTF-8"); configuration.setVariable(AUTODETECT_SHELL_ENCODING, false); } } } /** * Saves the trolCommander CONFIGURATION. * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a CONFIGURATION related error occurs. */ void write() throws IOException, ConfigurationException { if (configurationVersion != null && !configurationVersion.equals(RuntimeConstants.VERSION)) { // Clear the configuration before saving to drop preferences which are unused anymore Configuration conf = new Configuration(TcPreferencesFile.getPreferencesFile(), new VersionedXmlConfigurationReaderFactory(), new VersionedXmlConfigurationWriterFactory(ROOT_ELEMENT)); for (TcPreference preference : TcPreference.values()) conf.setVariable(preference.toString(), configuration.getVariable(preference.toString())); // Remove preferences which are not relevant if we're not using MAC if (!OsFamily.MAC_OS_X.isCurrent()) { conf.removeVariable(USE_BRUSHED_METAL); conf.removeVariable(USE_SCREEN_MENU_BAR); } configuration = conf; } configuration.write(); } // - Configuration listening ----------------------------------------------- // ------------------------------------------------------------------------- /** * Adds the specified object to the list of registered CONFIGURATION listeners. * @param listener object to register as a CONFIGURATION listener. * @see #removeConfigurationListener(ConfigurationListener) */ void addConfigurationListener(ConfigurationListener listener) {configuration.addConfigurationListener(listener);} /** * Removes the specified object from the list of registered CONFIGURATION listeners. * @param listener object to remove from the list of registered CONFIGURATION listeners. * @see #addConfigurationListener(ConfigurationListener) */ void removeConfigurationListener(ConfigurationListener listener) {configuration.removeConfigurationListener(listener);} // - Configuration source -------------------------------------------------- // ------------------------------------------------------------------------- /** * Sets the path to the CONFIGURATION file. * @param file path to the file that should be used for CONFIGURATION storage. * @throws FileNotFoundException if the specified file is not a valid file. */ void setConfigurationFile(String file) throws FileNotFoundException { configuration.setSource(TcPreferencesFile.getPreferencesFile(file)); } /** * Check whether the preferences file exists * @return true if the preferences file exits, false otherwise. * @throws IOException if an error occurred. */ boolean isFileExists() throws IOException { return configuration.getSource().isExists(); } ///////////////////////////////////// // MuPreferencesAPI implementation // ///////////////////////////////////// @Override public boolean setVariable(TcPreference preference, String value) { return configuration.setVariable(preference.toString(), value); } @Override public boolean setVariable(TcPreference preference, int value) { return configuration.setVariable(preference.toString(), value); } @Override public boolean setVariable(TcPreference preference, List value, String separator) { return configuration.setVariable(preference.toString(), value, separator); } @Override public boolean setVariable(TcPreference preference, float value) { return configuration.setVariable(preference.toString(), value); } @Override public boolean setVariable(TcPreference preference, boolean value) { return configuration.setVariable(preference.toString(), value); } @Override public boolean setVariable(TcPreference preference, long value) { return configuration.setVariable(preference.toString(), value); } @Override public boolean setVariable(TcPreference preference, double value) { return configuration.setVariable(preference.toString(), value); } @Override public String getVariable(TcPreference preference) { return configuration.getVariable(preference.toString()); } @Override public String getVariable(TcPreference preference, String value) { return configuration.getVariable(preference.toString(), value); } @Override public int getVariable(TcPreference preference, int value) { return configuration.getVariable(preference.toString(), value); } @Override public List getVariable(TcPreference preference, List value, String separator) { return configuration.getVariable(preference.toString(), value, separator); } @Override public float getVariable(TcPreference preference, float value) { return configuration.getVariable(preference.toString(), value); } @Override public boolean getVariable(TcPreference preference, boolean value) { return configuration.getVariable(preference.toString(), value); } @Override public long getVariable(TcPreference preference, long value) { return configuration.getVariable(preference.toString(), value); } @Override public double getVariable(TcPreference preference, double value) { return configuration.getVariable(preference.toString(), value); } @Override public ValueList getListVariable(TcPreference preference, String separator) { return configuration.getListVariable(preference.toString(), separator); } @Override public boolean getBooleanVariable(String name) { return configuration.getBooleanVariable(name); } @Override public String getVariable(String name) { return configuration.getVariable(name); } @Override public boolean isVariableSet(TcPreference preference) { return configuration.isVariableSet(preference.toString()); } @Override public String removeVariable(String name) { return configuration.removeVariable(name); } } ================================================ FILE: src/main/java/com/mucommander/conf/TcPreferencesAPI.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import java.util.List; import com.mucommander.commons.conf.ValueList; /** * * @author Arik Hadas */ public interface TcPreferencesAPI { boolean setVariable(TcPreference preference, String value); boolean setVariable(TcPreference preference, int value); boolean setVariable(TcPreference preference, List value, String separator); boolean setVariable(TcPreference preference, float value); boolean setVariable(TcPreference preference, boolean value); boolean setVariable(TcPreference preference, long value); boolean setVariable(TcPreference preference, double value); String getVariable(TcPreference preference); String getVariable(TcPreference preference, String value); int getVariable(TcPreference preference, int value); List getVariable(TcPreference preference, List value, String separator); float getVariable(TcPreference preference, float value); boolean getVariable(TcPreference preference, boolean value); long getVariable(TcPreference preference, long value); double getVariable(TcPreference preference, double value); ValueList getListVariable(TcPreference preference, String separator); // TODO: remove those methods boolean getBooleanVariable(String name); String getVariable(String name); boolean isVariableSet(TcPreference preference); String removeVariable(String name); } ================================================ FILE: src/main/java/com/mucommander/conf/TcPreferencesFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import java.io.FileNotFoundException; /** * * @author Arik Hadas */ class TcPreferencesFile extends TcConfigurationFile { private static final String DEFAULT_PREFERENCES_FILE_NAME = "preferences.xml"; static TcPreferencesFile getPreferencesFile(String path) throws FileNotFoundException { return new TcPreferencesFile(path); } static TcPreferencesFile getPreferencesFile() { try { return new TcPreferencesFile(null); } catch (FileNotFoundException e) { // Not possible exception return null; } } private TcPreferencesFile(String path) throws FileNotFoundException { super(path, DEFAULT_PREFERENCES_FILE_NAME); } } ================================================ FILE: src/main/java/com/mucommander/conf/TcSnapshot.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import java.awt.Dimension; import java.awt.HeadlessException; import java.awt.Rectangle; import java.awt.Toolkit; import java.io.IOException; import java.util.Iterator; import java.util.List; import javax.swing.JSplitPane; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.conf.Configuration; import com.mucommander.commons.conf.ConfigurationException; import com.mucommander.commons.file.FileURL; import com.mucommander.core.GlobalLocationHistory; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.tabs.FileTableTab; import com.mucommander.ui.main.tabs.FileTableTabs; import com.mucommander.ui.viewer.text.TextViewer; /** * muCommander specific wrapper for the com.mucommander.conf API which is used to save 'dynamic' configurations. * 'dynamic' configurations refer to properties that represent the state of the last running instance which is not set from the * preferences dialog. those properties are changed often. * * @author Arik Hadas */ public class TcSnapshot { private static Logger logger; // - Last screen variables ----------------------------------------------- // ----------------------------------------------------------------------- /** Section describing last known screen properties. */ private static final String SCREEN_SECTION = "screen"; /** Last known screen width. */ public static final String SCREEN_WIDTH = SCREEN_SECTION + "." + "width"; /** Last known screen height. */ public static final String SCREEN_HEIGHT = SCREEN_SECTION + "." + "height"; // - Text file presenter (viewer/editor) variables ----------------------- // ----------------------------------------------------------------------- /** Section describing information about features used by the last file presenter instance. */ private static final String FILE_PRESENTER_SECTION = "file_presenter"; /** Section describing information specific to text file presenter. */ private static final String TEXT_FILE_PRESENTER_SECTION = FILE_PRESENTER_SECTION + "." + "text"; /** Whether to wrap long lines. */ public static final String TEXT_FILE_PRESENTER_LINE_WRAP = TEXT_FILE_PRESENTER_SECTION + "." + "line_wrap"; /** Default wrap value. */ public static final boolean DEFAULT_LINE_WRAP = false; /** Whether to show line numbers. */ public static final String TEXT_FILE_PRESENTER_LINE_NUMBERS = TEXT_FILE_PRESENTER_SECTION + "." + "line_numbers"; /** Default line numbers value. */ public static final boolean DEFAULT_LINE_NUMBERS = true; /** Last known file presenter full screen mode. */ public static final String TEXT_FILE_PRESENTER_FULL_SCREEN = TEXT_FILE_PRESENTER_SECTION + "." + "full_screen"; // - Location history ---- ----------------------------------------------- // ----------------------------------------------------------------------- /** Section containing the visited location in each folder panel. */ private static final String RECENT_LOCATIONS_SECTION = "recent_locations"; /** A location that was visited within the folder panel. */ private static final String LOCATION = "location"; /** Describes the number of visited locations saved for the folder panel */ private static final String LOCATIONS_COUNT = "count"; // - Last windows variables ---------------------------------------------- // ----------------------------------------------------------------------- /** Section describing known information about last muCommander windows. */ private static final String WINDOWS_SECTION = "windows"; /** Describes the number of windows that were open. */ private static final String WINDOWS_COUNT = WINDOWS_SECTION + '.' + "count"; /** Describes the index of the selected window. */ private static final String WINDOWS_SELECTION = WINDOWS_SECTION + '.' + "selection"; /** Subsection describing information which is specific to a particular window. */ private static final String WINDOW = "window"; // - Window variables ---------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing known general information of muCommander window. */ private static final String WINDOW_PROPERTIES_SECTION = "window_properties"; /** Describes the window's horizontal position. */ private static final String X = "x"; /** Describes the window's vertical position. */ private static final String Y = "y"; /** Describes the window's width. */ private static final String WIDTH = "width"; /** Describes the window's height. */ private static final String HEIGHT = "height"; /** Describes the orientation used to split folder panels. */ private static final String SPLIT_ORIENTATION = "split_orientation"; /** Vertical split pane orientation. */ public static final String VERTICAL_SPLIT_ORIENTATION = "vertical"; /** Horizontal split pane orientation. */ public static final String HORIZONTAL_SPLIT_ORIENTATION = "horizontal"; /** Default split pane orientation. */ public static final String DEFAULT_SPLIT_ORIENTATION = VERTICAL_SPLIT_ORIENTATION; // - Panels variables ---------------------------------------------------- // ----------------------------------------------------------------------- /** Section describing the dynamic information contained in the folder panels */ private static final String PANELS_SECTION = "panels"; /** Identifier of the left panel. */ private static final String LEFT = "left"; /** Identifier of the right panel. */ private static final String RIGHT = "right"; // - Tree variables ------------------------------------------------------ // ----------------------------------------------------------------------- /** Subsection describing the tree view CONFIGURATION. */ private static final String TREE_SECTION = "tree"; /** Describes whether the tree is visible */ private static final String TREE_VISIBLE = "visible"; /** Describes the tree's width */ private static final String TREE_WIDTH = "width"; // - File Table variables ------------------------------------------------ // ----------------------------------------------------------------------- /** Subsection describing the folders view CONFIGURATION. */ private static final String FILE_TABLE_SECTION = "file_table"; /** Describes an ascending sort order. */ private static final String SORT_ORDER_ASCENDING = "asc"; /** Describes a descending sort order. */ public static final String SORT_ORDER_DESCENDING = "desc"; /** Default 'sort order' column for the file table. */ public static final String DEFAULT_SORT_ORDER = SORT_ORDER_ASCENDING; /** Name of the 'show column' variable. */ private static final String SHOW_COLUMN = "show"; /** Name of the 'column position' variable. */ private static final String COLUMN_POSITION = "position"; /** Name of the 'column width' variable. */ private static final String COLUMN_WIDTH = "width"; /** Default 'sort by' column for the file table. */ public static final String DEFAULT_SORT_BY = "name"; /** Identifier of the sort section in a file table's CONFIGURATION. */ private static final String SORT = "sort"; /** Identifier of the sort criteria in a file table's CONFIGURATION. */ private static final String SORT_BY = "by"; /** Identifier of the sort order in a file table's CONFIGURATION. */ private static final String SORT_ORDER = "order"; // - Tabs variables ------------------------------------------------------ // ----------------------------------------------------------------------- /** Subsection describing the tabs CONFIGURATION. */ private static final String TABS_SECTION = "tabs"; /** Describes the number of tabs presented in the panel */ private static final String TABS_COUNT = "count"; /** Subsection describing information which is specific to a particular tab */ private static final String TAB = "tab"; /** Describes the location presented in a tab */ private static final String TAB_LOCATION = "location"; /** Describes whether a tab is locked */ private static final String TAB_LOCKED = "locked"; /** The index of the selected tab within the folder panel */ private static final String SELECTED_TAB = "selection"; /** Describes the title that was set for the tab */ private static final String TAB_TITLE = "title"; /** Cache the screen's size. this value isn't computed during the shutdown process since it cause a deadlock then */ private static Dimension screenSize; public static String getSelectedWindow() { return WINDOWS_SELECTION; } public static String getWindowsCount() { return WINDOWS_COUNT; } /** * Returns the section of the {@link com.mucommander.ui.main.MainFrame} at a given index * * @param index index of MainFrame * @return the section of the MainFrame at the given index */ private static String getWindowSection(int index) { return WINDOWS_SECTION + "." + WINDOW + "-" + index; } /** * Returns the section of the properties for the {@link com.mucommander.ui.main.MainFrame} at a given index * * @param window index of MainFrame * @return the section of the properties for the MainFrame at the given index */ private static String getWindowPropertiesSection(int window) { return getWindowSection(window) + "." + WINDOW_PROPERTIES_SECTION; } /** * Returns the horizontal position on the screen of the {@link com.mucommander.ui.main.MainFrame} at a given index * * @param window index of MainFrame * @return the horizontal position of the MainFrame at the given index */ public static String getX(int window) { return getWindowPropertiesSection(window) + "." + X; } /** * Return the vertical position on the screen of the {@link com.mucommander.ui.main.MainFrame} at a given index * * @param window index of MainFrame * @return the vertical position of the MainFrame at the given index */ public static String getY(int window) { return getWindowPropertiesSection(window) + "." + Y; } /** * Returns the width of the {@link com.mucommander.ui.main.MainFrame} at a given index * * @param window index of MainFrame * @return the width of the MainFrame at the given index */ public static String getWidth(int window) { return getWindowPropertiesSection(window) + "." + WIDTH; } /** * Return the height of the {@link com.mucommander.ui.main.MainFrame} in a given index * * @param window index of MainFrame * @return the height of the MainFrame at the given index */ public static String getHeight(int window) { return getWindowPropertiesSection(window) + "." + HEIGHT; } /** * Returns the orientation used to split folder panels in the {@link com.mucommander.ui.main.MainFrame} at a given index * * @param window index of MainFrame * @return the orientation used to split folder panel in the MainFrame at the given index */ public static String getSplitOrientation(int window) { return getWindowPropertiesSection(window) + "." + SPLIT_ORIENTATION; } /** * Returns the CONFIGURATION section corresponding to the specified {@link com.mucommander.ui.main.FolderPanel}, * left or right one in the {@link com.mucommander.ui.main.MainFrame} at the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @return the CONFIGURATION section corresponding to the specified FolderPanel */ private static String getFolderPanelSection(int window, boolean left) { return getWindowSection(window) + "." + PANELS_SECTION + "." + (left?LEFT:RIGHT); } /** * Returns the CONFIGURATION section corresponding to the specified {@link com.mucommander.ui.main.tree.FoldersTreePanel}, * left or right one in the {@link com.mucommander.ui.main.MainFrame} at the given index. * * @param window index of MainFrame * @param left true for the left FoldersTreePanel, false for the right one * @return the CONFIGURATION section corresponding to the specified FoldersTreePanel */ private static String getTreeSection(int window, boolean left) { return getFolderPanelSection(window, left) + "." + TREE_SECTION; } /** * Returns the variable that controls the visibility of the tree view, in the left or right * {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @return the variable that controls the visibility of the tree view in the specified FolderPanel */ public static String getTreeVisiblityVariable(int window, boolean left) { return getTreeSection(window, left) + "." + TREE_VISIBLE; } /** * Returns the variable that holds the width of the tree view, in the left or right * {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @return the variable that holds the width of the tree view in the specified FolderPanel */ public static String getTreeWidthVariable(int window, boolean left) { return getTreeSection(window, left) + "." + TREE_WIDTH; } /** * Returns the CONFIGURATION section corresponding to the specified {@link com.mucommander.ui.main.table.FileTable}, * left or right one in the {@link com.mucommander.ui.main.MainFrame} at the given index. * * @param window index of MainFrame * @param left true for the left FileTable, false for the right one * @return the CONFIGURATION section corresponding to the specified FileTable */ private static String getFileTableSection(int window, boolean left) { return getFolderPanelSection(window, left) + "." + FILE_TABLE_SECTION; } /** * Returns the CONFIGURATION section that describes the sorting of the specified {@link com.mucommander.ui.main.table.FileTable}, * left or right one in the {@link com.mucommander.ui.main.MainFrame} at the given index. * * @param window index of MainFrame * @param left true for the left FileTable, false for the right one * @return the CONFIGURATION section that describes the sorting of the specified FileTable */ private static String getFileTableSortSection(int window, boolean left) { return getFileTableSection(window, left) + "." + SORT; } /** * Returns the variable that controls the sort criteria of the file table, in the left or right * {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @return the variable that controls the sort criteria of the file table in the specified FolderPanel */ public static String getFileTableSortByVariable(int window, boolean left) { return getFileTableSortSection(window, left) + "." + SORT_BY; } /** * Returns the variable that controls the sort order (ascending\descending) of the file table, * in the left or right {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @return the variable that controls the sort order of the file table in the specified FolderPanel */ public static String getFileTableSortOrderVariable(int window, boolean left) { return getFileTableSortSection(window, left) + "." + SORT_ORDER; } /** * Returns the CONFIGURATION section corresponding to the specified column in the left or right * {@link com.mucommander.ui.main.table.FileTable} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param column column, see {@link Column} for possible values * @param left true for the left FileTable, false for the right one * @return the CONFIGURATION section corresponding to the specified FileTable */ private static String getColumnSection(int window, Column column, boolean left) { return getFileTableSection(window, left) + "." + column.toString().toLowerCase(); } /** * Returns the variable that controls the visibility of the specified column, in the left or right * {@link com.mucommander.ui.main.table.FileTable} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param column column, see {@link Column} for possible values * @param left true for the left FileTable, false for the right one * @return the variable that controls the visibility of the specified column */ public static String getShowColumnVariable(int window, Column column, boolean left) { return getColumnSection(window, column, left) + "." + SHOW_COLUMN; } /** * Returns the variable that holds the width of the specified column, in the left or right * {@link com.mucommander.ui.main.table.FileTable} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param column column, see {@link Column} for possible values * @param left true for the left FileTable, false for the right one * @return the variable that holds the width of the specified column */ public static String getColumnWidthVariable(int window, Column column, boolean left) { return getColumnSection(window, column, left) + "." + COLUMN_WIDTH; } /** * Returns the variable that holds the position of the specified column, in the left or right * {@link com.mucommander.ui.main.table.FileTable} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param column column, see {@link Column} for possible values * @param left true for the left FileTable, false for the right one * @return the variable that holds the position of the specified column */ public static String getColumnPositionVariable(int window, Column column, boolean left) { return getColumnSection(window, column, left) + "." + COLUMN_POSITION; } /** * Returns the CONFIGURATION section corresponding to the specified {@link com.mucommander.ui.main.tabs.FileTableTabs}, * left or right one in the {@link com.mucommander.ui.main.MainFrame} at the given index. * * @param window index of MainFrame * @param left true for the left FileTableTabs, false for the right one * @return the CONFIGURATION section corresponding to the specified FileTableTabs */ private static String getTabsSection(int window, boolean left) { return getFolderPanelSection(window, left) + "." + TABS_SECTION; } /** * Returns the variable that holds the number of presented tabs, in the left or right * {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @return the variable that holds the number of presented tabs in the specified FolderPanel */ public static String getTabsCountVariable(int window, boolean left) { return getTabsSection(window, left) + "." + TABS_COUNT; } /** * Returns the variable that holds the index of the selected tab, in the left or right * {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FileTableTab, false for the right one * @return the variable that holds the index of the selected tab in the specified FolderPanel */ public static String getTabsSelectionVariable(int window, boolean left) { return getTabsSection(window, left) + "." + SELECTED_TAB; } /** * Returns the CONFIGURATION section corresponding to the specified {@link com.mucommander.ui.main.tabs.FileTableTab}, * left or right one in the {@link com.mucommander.ui.main.MainFrame} at the given index. * * @param window index of MainFrame * @param left true for the left FileTableTab, false for the right one * @return the CONFIGURATION section corresponding to the specified FileTableTab */ private static String getTabSection(int window, boolean left, int index) { return getTabsSection(window, left) + "." + TAB + "-" + index; } /** * Returns the variable that holds the location presented at the tab in the given index, * in the left or right {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @param index the index of tab at the FolderPanel's tabs * @return the variable that holds the location presented at the tab in the given index in the specified FolderPanel */ public static String getTabLocationVariable(int window, boolean left, int index) { return getTabSection(window, left, index) + "." + TAB_LOCATION; } /** * Returns the variable that holds the title of the tab in the given index, * in the left or right {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index. * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @param index the index of tab at the FolderPanel's tabs * @return the variable that holds the title of the tab in the given index in the specified FolderPanel */ public static String getTabTitleVariable(int window, boolean left, int index) { return getTabSection(window, left, index) + "." + TAB_TITLE; } /** * Returns the variable that indicates whether the tab in the given index, * in the left or right {@link com.mucommander.ui.main.FolderPanel} at the {@link com.mucommander.ui.main.MainFrame} in the given index, * is locked * * @param window index of MainFrame * @param left true for the left FolderPanel, false for the right one * @param index the index of tab at the FolderPanel's tabs * @return the variable that indicates whether the tab in the given index in the specified FolderPanel is locked */ public static String getTabLockedVariable(int window, boolean left, int index) { return getTabSection(window, left, index) + "." + TAB_LOCKED; } /** * Returns the CONFIGURATION section corresponding to the specified {@link com.mucommander.core.GlobalLocationHistory} * @return the CONFIGURATION section corresponding to the specified {@link com.mucommander.core.GlobalLocationHistory} */ private static String getRecentLocationsSection() { return RECENT_LOCATIONS_SECTION; } /** * Returns the variable that holds number of the saved visited locations * * @return the variable that holds the number of saved visited locations */ public static String getRecentLocationsCountVariable() { return getRecentLocationsSection() + "." + LOCATIONS_COUNT; } /** * Returns the variable that holds a location contained in the global locations history at the given index * * @param index the index of location in the visited location history * @return the variable that holds a location contained in the global locations history at the given index */ public static String getRecentLocationVariable(int index) { return getRecentLocationsSection() + "." + LOCATION + "-" + index; } private static final String ROOT_ELEMENT = "snapshot"; // - Instance fields ----------------------------------------------------- // ----------------------------------------------------------------------- private final Configuration configuration; /** * Prevents instantiation of this class from outside of this package. */ TcSnapshot() { configuration = new Configuration(TcSnapshotFile.getSnapshotFile(), new VersionedXmlConfigurationReaderFactory(), new VersionedXmlConfigurationWriterFactory(ROOT_ELEMENT)); } /** * * @return size of screen */ public static Dimension getScreenSize() { if (screenSize != null) { return screenSize; } synchronized (TcSnapshot.class) { if (screenSize == null) { // Getting DefaultToolkit and screen sizes try { screenSize = Toolkit.getDefaultToolkit().getScreenSize(); } catch (HeadlessException e) { getLogger().debug("Could not fetch screen size: " + e.getMessage()); } } } return screenSize; } /** * TODO: change this method such that it will return a more specific API */ Configuration getConfiguration() { return configuration; } // - Configuration reading / writing ------------------------------------- // ----------------------------------------------------------------------- /** * Loads the muCommander CONFIGURATION. * Here, we don't try to convert preferences that were changed between muCommander versions, * as those are 'dynamic' preferences and as such can be ignored when the user installs new version. * * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a CONFIGURATION related error occurs. */ void read() throws IOException, ConfigurationException { VersionedXmlConfigurationReader reader = new VersionedXmlConfigurationReader(); configuration.read(reader); } /** * Saves the muCommander CONFIGURATION. * @throws IOException if an I/O error occurs. * @throws ConfigurationException if a CONFIGURATION related error occurs. */ void write() throws IOException, ConfigurationException { //Clear the configuration before saving to drop preferences which are unused anymore configuration.clear(); // Get opened main frames list List mainFrames = WindowManager.getMainFrames(); // Save windows count int nbMainFrames = mainFrames.size(); configuration.setVariable(WINDOWS_COUNT, nbMainFrames); // Save the index of the selected window int indexOfSelectedWindow = WindowManager.getCurrentWindowIndex(); configuration.setVariable(WINDOWS_SELECTION, indexOfSelectedWindow); // Save attributes for each window for (int i=0; i < nbMainFrames; ++i) setFrameAttributes(mainFrames.get(i), i); if (getScreenSize() != null) { configuration.setVariable(TcSnapshot.SCREEN_WIDTH, screenSize.width); configuration.setVariable(TcSnapshot.SCREEN_HEIGHT, screenSize.height); } setGlobalHistory(); setTextPresenterProperties(); configuration.write(); } private void setTextPresenterProperties() { // configuration.setVariable(MuSnapshot.TEXT_FILE_PRESENTER_FULL_SCREEN, TextViewer.isFullScreen()); configuration.setVariable(TcSnapshot.TEXT_FILE_PRESENTER_LINE_WRAP, TextViewer.isLineWrap()); configuration.setVariable(TcSnapshot.TEXT_FILE_PRESENTER_LINE_NUMBERS, TextViewer.isLineNumbers()); } private void setFrameAttributes(MainFrame mainFrame, int index) { // Save window position, size and screen resolution setWindowAttributes(index, mainFrame); // Save left panel dynamic properties setPanelAttributes(index, true, mainFrame.getLeftPanel()); // Save right panel dynamic properties setPanelAttributes(index, false, mainFrame.getRightPanel()); } private void setPanelAttributes(int index, boolean isLeft, FolderPanel panel) { // Save tree folders preferences setTreeAttributes(index, isLeft, panel); setTableAttributes(index, isLeft, panel.getFileTable()); setTabsAttributes(index, isLeft, panel.getTabs()); } private void setGlobalHistory() { List locations = GlobalLocationHistory.getInstance().getHistory(); configuration.setVariable(getRecentLocationsCountVariable(), locations.size()); int i = 0; for (FileURL url : locations) { configuration.setVariable(getRecentLocationVariable(i++), url.toString()); } } private void setTabsAttributes(int index, boolean isLeft, FileTableTabs tabs) { int tabsCounter = 0; Iterator tabsIterator = tabs.iterator(); // Save tabs locations while (tabsIterator.hasNext()) { FileTableTab tab = tabsIterator.next(); configuration.setVariable(getTabLocationVariable(index, isLeft, tabsCounter), tab.getLocation().toString()); configuration.setVariable(getTabLockedVariable(index, isLeft, tabsCounter), tab.isLocked()); configuration.setVariable(getTabTitleVariable(index, isLeft, tabsCounter), tab.getTitle()); ++tabsCounter; } if (tabsCounter > 0) { // Save tabs count configuration.setVariable(getTabsCountVariable(index, isLeft), tabsCounter); // Save the index of the selected tab configuration.setVariable(getTabsSelectionVariable(index, isLeft), tabs.getSelectedIndex()); } } private void setTableAttributes(int index, boolean isLeft, FileTable table) { // Saves table sort order. configuration.setVariable(TcSnapshot.getFileTableSortByVariable(index, isLeft), table.getSortInfo().getCriterion().toString().toLowerCase()); configuration.setVariable(TcSnapshot.getFileTableSortOrderVariable(index, isLeft), table.getSortInfo().getAscendingOrder() ? TcSnapshot.SORT_ORDER_ASCENDING : TcSnapshot.SORT_ORDER_DESCENDING); // Loop on columns for (Column c : Column.values()) { if (c != Column.NAME) { // Skip the special name column (always enabled, width automatically calculated) TcConfigurations.getSnapshot().setVariable( TcSnapshot.getShowColumnVariable(index, c, isLeft), table.isColumnEnabled(c) ); TcConfigurations.getSnapshot().setVariable( TcSnapshot.getColumnWidthVariable(index, c, isLeft), table.getColumnWidth(c) ); } TcConfigurations.getSnapshot().setVariable( TcSnapshot.getColumnPositionVariable(index, c, isLeft), table.getColumnPosition(c) ); } } private void setTreeAttributes(int index, boolean isLeft, FolderPanel panel) { configuration.setVariable(TcSnapshot.getTreeVisiblityVariable(index, isLeft), panel.isTreeVisible()); configuration.setVariable(TcSnapshot.getTreeWidthVariable(index, isLeft), panel.getTreeWidth()); } private void setWindowAttributes(int index, MainFrame currentMainFrame) { Rectangle bounds = currentMainFrame.getJFrame().getBounds(); configuration.setVariable(getX(index), (int)bounds.getX()); configuration.setVariable(getY(index), (int)bounds.getY()); configuration.setVariable(getWidth(index), (int)bounds.getWidth()); configuration.setVariable(getHeight(index), (int)bounds.getHeight()); // Save split pane orientation // Note: the vertical/horizontal terminology used in muCommander is just the opposite of the one used // in JSplitPane which is anti-natural / confusing configuration.setVariable(getSplitOrientation(index), currentMainFrame.getSplitPane().getOrientation()==JSplitPane.HORIZONTAL_SPLIT? TcSnapshot.VERTICAL_SPLIT_ORIENTATION: TcSnapshot.HORIZONTAL_SPLIT_ORIENTATION); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(TcSnapshot.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/conf/TcSnapshotFile.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import java.io.FileNotFoundException; /** * * @author Arik Hadas */ class TcSnapshotFile extends TcConfigurationFile { private static final String DEFAULT_SNAPSHOT_FILE_NAME = "snapshot.xml"; static TcSnapshotFile getSnapshotFile() { try { return new TcSnapshotFile(); } catch (FileNotFoundException e) { // Not possible exception?? return null; } } private TcSnapshotFile() throws FileNotFoundException { super(null, DEFAULT_SNAPSHOT_FILE_NAME); } } ================================================ FILE: src/main/java/com/mucommander/conf/VersionedXmlConfigurationReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import com.mucommander.commons.conf.XmlConfigurationReader; import org.xml.sax.Attributes; import org.xml.sax.SAXException; /** * @author Maxence Bernard */ public class VersionedXmlConfigurationReader extends XmlConfigurationReader { /** True until the root element has been parsed */ private boolean isRootElement = true; /** the version that was used to write the configuration file */ private String version; /** * Returns the muCommander version that was used to write the configuration file, null if it is unknown. *

    * Note: the version attribute was introduced in muCommander 0.8.4. * * @return the muCommander version that was used to write the configuration file, null if it is unknown. */ public String getVersion() { return version; } //////////////////////// // Overridden methods // //////////////////////// @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { super.startElement(uri, localName, qName, attributes); if (isRootElement) { version = attributes.getValue(TcPreferences.VERSION_ATTRIBUTE); isRootElement = false; } } } ================================================ FILE: src/main/java/com/mucommander/conf/VersionedXmlConfigurationReaderFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import com.mucommander.commons.conf.ConfigurationReader; import com.mucommander.commons.conf.ConfigurationReaderFactory; /** * @author Maxence Bernard */ public class VersionedXmlConfigurationReaderFactory implements ConfigurationReaderFactory { @Override public ConfigurationReader getReaderInstance() { return new VersionedXmlConfigurationReader(); } } ================================================ FILE: src/main/java/com/mucommander/conf/VersionedXmlConfigurationWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import com.mucommander.RuntimeConstants; import com.mucommander.commons.conf.ConfigurationException; import com.mucommander.commons.conf.XmlConfigurationWriter; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import java.io.Writer; /** * @author Maxence Bernard */ class VersionedXmlConfigurationWriter extends XmlConfigurationWriter { //////////////////////// // Overridden methods // //////////////////////// public VersionedXmlConfigurationWriter(Writer out, String rootElementName) { super(out, rootElementName); } @Override public void startConfiguration() throws ConfigurationException { // Version the file. // Note: the version attribute was introduced in muCommander 0.8.4. AttributesImpl attributes; attributes = new AttributesImpl(); attributes.addAttribute("", TcPreferences.VERSION_ATTRIBUTE, TcPreferences.VERSION_ATTRIBUTE, "string", RuntimeConstants.VERSION); try {out.startElement("", rootElementName, rootElementName, attributes);} catch(SAXException e) {throw new ConfigurationException(e);} } } ================================================ FILE: src/main/java/com/mucommander/conf/VersionedXmlConfigurationWriterFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.conf; import com.mucommander.commons.conf.ConfigurationBuilder; import com.mucommander.commons.conf.ConfigurationWriterFactory; import java.io.Writer; /** * @author Maxence Bernard */ class VersionedXmlConfigurationWriterFactory extends ConfigurationWriterFactory { /** * Constructor * * @param rootElementName the name of the root element in the XML file */ public VersionedXmlConfigurationWriterFactory(String rootElementName) { super(rootElementName); } @Override public ConfigurationBuilder getWriterInstance(Writer out) { return new VersionedXmlConfigurationWriter(out, getRootElementName()); } } ================================================ FILE: src/main/java/com/mucommander/conf/package.html ================================================ Application-specific implementation of the com.mucommander.commons.conf API. ================================================ FILE: src/main/java/com/mucommander/core/FolderChangeMonitor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.core; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.filter.AbstractFileFilter; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.OrFileFilter; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.main.FolderPanel; /** * This file monitors changes in the current folder of a FolderPanel, checking periodically if the current folder's * date has changed. If a change has been detected, the FolderPanel will be asked to refresh its current folder. * *

    If the MainFrame which contains the monitored FolderPanel becomes inactive (lies in the background), monitoring * on will be not happen until the MainFrame becomes active again. * *

    Implementation note: the monitoring is done in one single thread for all folders, each folder being monitored * one after another. Current folder refreshes are performed in a separate thread. * * @author Maxence Bernard * @see FolderAutoRefresh wiki entry */ public class FolderChangeMonitor implements Runnable, WindowListener, LocationListener { private static final Logger LOGGER = LoggerFactory.getLogger(FolderChangeMonitor.class); /** * If not null then refresh folder that contains this files */ private static final List forceRefreshFilePath = new ArrayList<>(); /** Thread in which the actual monitoring is performed */ private static Thread monitorThread; /** FolderChangeMonitor instances */ private static final List instances; private static final OrFileFilter disableAutoRefreshFilter = new OrFileFilter(); /** Milliseconds period between checks to current folder's date */ private static final long checkPeriod; /** Delay in milliseconds before folder date check after a folder has been refreshed */ private static final long waitAfterRefresh; /** If folder change check took an average of N milliseconds, thread will wait at least N*WAIT_MULTIPLIER before next check */ private final static int WAIT_MULTIPLIER = 50; /** Granularity of the thread check (number of milliseconds to sleep before next loop) */ private final static int TICK = 300; /** Folder panel we are monitoring */ private final FolderPanel folderPanel; /** Current file table's folder */ private AbstractFile currentFolder; /** True when the current folder is currently being changed */ private boolean folderChanging; /** Current folder's date */ private long currentFolderDate; /** Folder check/refresh while be skipped while this field is set to true */ private boolean paused; /** Number of milliseconds to wait before next folder check */ private long waitBeforeCheckTime; /** Timestamp of the last folder change check */ private long lastCheckTimestamp; /** Total time spent checking for folder changes in current folder */ private long totalCheckTime = 0; /** Number of checks in current folder */ private int nbSamples = 0; static { instances = Collections.synchronizedList(new ArrayList<>()); // Retrieve configuration values checkPeriod = TcConfigurations.getPreferences().getVariable(TcPreference.REFRESH_CHECK_PERIOD, TcPreferences.DEFAULT_REFRESH_CHECK_PERIOD); waitAfterRefresh = TcConfigurations.getPreferences().getVariable(TcPreference.WAIT_AFTER_REFRESH, TcPreferences.DEFAULT_WAIT_AFTER_REFRESH); disableAutoRefreshFilter.addFileFilter(new AbstractFileFilter() { public boolean accept(AbstractFile file) { return file.getURL().getScheme().equals(FileProtocols.S3); } }); } /** * Adds the given {@link FileFilter} to the list of filters that match folders for which auto-refresh is disabled. * One use case for disabling auto-refresh is for protocols that involve a cost ($$$) when looking for changes * or refreshing the folder. This is the case for Amazon S3 for which auto-refresh is disabled by default. * * @param filter matches folders for which auto-refresh will be disabled */ public static void addDisableAutoRefreshFilter(FileFilter filter) { disableAutoRefreshFilter.addFileFilter(filter); } public FolderChangeMonitor(FolderPanel folderPanel) { this.folderPanel = folderPanel; // Listen to folder changes to know when a folder is being / has been changed folderPanel.getLocationManager().addLocationListener(this); this.currentFolder = folderPanel.getCurrentFolder(); this.currentFolderDate = currentFolder.getLastModifiedDate(); // Folder contents is up-to-date let's wait before checking it for changes this.lastCheckTimestamp = System.currentTimeMillis(); this.waitBeforeCheckTime = waitAfterRefresh; folderPanel.getMainFrame().getJFrame().addWindowListener(this); instances.add(this); // create and start the monitor thread on first FolderChangeMonitor instance if (monitorThread == null && checkPeriod >= 0) { monitorThread = new Thread(this, getClass().getName()); monitorThread.setDaemon(true); monitorThread.start(); } } public void run() { // TODO: it would be more efficient to use a wait/notify scheme rather than sleeping. // It would also allow folders to be checked immediately upon certain conditions such as a window becoming activated. int needToClearRefreshQueueCounter = 0; while (monitorThread != null) { // Sleep for a while try { Thread.sleep(TICK); } catch(InterruptedException ignore) {} // Loop on instances try { for (FolderChangeMonitor instance : instances) { checkForMonitor(instance); } // clean up the refresh queue if it doesn't empty a "long" time if (needToClearRefreshQueueCounter > 10) { needToClearRefreshQueueCounter = 0; synchronized (forceRefreshFilePath) { forceRefreshFilePath.clear(); } } else if (!forceRefreshFilePath.isEmpty()) { needToClearRefreshQueueCounter++; } } catch (Throwable t) { LOGGER.error("Execution error", t); } } } private void checkForMonitor(FolderChangeMonitor monitor) { // Check for changes in current folder and refresh it only if : // - MainFrame is in the foreground // - monitor is not paused // - current folder is not being changed if (!monitor.folderPanel.getMainFrame().isForegroundActive() || monitor.folderChanging || monitor.paused) { return; } if (disableAutoRefreshFilter.match(monitor.currentFolder)) { monitor.lastCheckTimestamp = System.currentTimeMillis(); monitor.waitBeforeCheckTime = checkPeriod; return; } // By checking FolderPanel.getLastFolderChangeTime(), we ensure that we don't check right after // the folder has been refreshed. if (System.currentTimeMillis() - Math.max(monitor.lastCheckTimestamp, monitor.folderPanel.getLastFolderChangeTime()) > monitor.waitBeforeCheckTime) { // Checks folder contents and refreshes view if necessary boolean folderRefreshed = monitor.checkAndRefresh(); monitor.lastCheckTimestamp = System.currentTimeMillis(); // If folder change check took an average of N milliseconds, we will wait at least N*WAIT_MULTIPLIER before next check monitor.waitBeforeCheckTime = calcWaitBeforeCheckTime(monitor, folderRefreshed); } } private long calcWaitBeforeCheckTime(FolderChangeMonitor monitor, boolean folderRefreshed) { if (monitor.nbSamples == 0) { return checkPeriod; } else { long refreshPeriod = folderRefreshed ? waitAfterRefresh : checkPeriod; long perSamplePeriod = (long) (WAIT_MULTIPLIER * (monitor.totalCheckTime / (float) monitor.nbSamples)); return Math.max(refreshPeriod, perSamplePeriod); } } /** * Stops monitoring (stops monitoring thread). */ public void stop() { monitorThread = null; } /** * Suspends or resumes this monitor. * * @param paused true to suspend, false to resume */ public void setPaused(boolean paused) { // Note: this method should *not* be synchronized as it would potentially lock while the folder is being // checked/refreshed this.paused = paused; // Check folder for changes immediately as setPaused(false) is often called after a FileJob if (!paused) { this.waitBeforeCheckTime = 0; } } /** * Forces this monitor to update current folder information. This method should be called when a folder has been * manually refreshed, so that this monitor doesn't detect changes and try to refresh the table again. * * @param folder the new current folder */ private void updateFolderInfo(AbstractFile folder) { this.currentFolder = folder; this.currentFolderDate = currentFolder.getLastModifiedDate(); // Reset time average totalCheckTime = 0; nbSamples = 0; } /** * Checks if current file table's folder has changed and if it hasn't, checks if current folder's date has changed * and if it has, refresh the file table. * * @return true if the folder was refreshed. */ private synchronized boolean checkAndRefresh() { // if (disableAutoRefreshFilter.match(currentFolder)) { // return false; // } // Update time average next loop long timeStamp = System.currentTimeMillis(); // Check folder's date long date = currentFolder.getLastModifiedDate(); totalCheckTime += System.currentTimeMillis() - timeStamp; nbSamples++; // Has date changed ? // Note that date will be 0 if the folder is no longer available, and thus yield a refresh: this is exactly // what we want (the folder will be changed to a 'workable' folder). boolean result = false; if (date != currentFolderDate) { LOGGER.debug("{} ({}) Detected changes in current folder, refreshing table!", this, currentFolder.getName()); // Try and refresh current folder in a separate thread as to not lock monitor thread folderPanel.tryRefreshCurrentFolder(); result = true; } if (!forceRefreshFilePath.isEmpty()) { synchronized (forceRefreshFilePath) { String folderPath = currentFolder.getAbsolutePath(); for (String path : forceRefreshFilePath) { if (path.startsWith(folderPath)) { forceRefreshFilePath.remove(path); folderPanel.tryRefreshCurrentFolder(); result = true; break; } } } } return result; } @Override public void locationChanging(LocationEvent locationEvent) { folderChanging = true; } @Override public void locationChanged(LocationEvent locationEvent) { // Update new current folder info updateFolderInfo(locationEvent.getFolderPanel().getCurrentFolder()); folderChanging = false; } @Override public void locationCancelled(LocationEvent locationEvent) { folderChanging = false; } @Override public void locationFailed(LocationEvent locationEvent) { folderChanging = false; } @Override public void windowActivated(WindowEvent e) {} @Override public void windowDeactivated(WindowEvent e) {} @Override public void windowIconified(WindowEvent e) {} @Override public void windowDeiconified(WindowEvent e) {} @Override public void windowOpened(WindowEvent e) {} @Override public void windowClosing(WindowEvent e) {} @Override public void windowClosed(WindowEvent e) { // Remove the MainFrame from the list of monitored instances instances.remove(this); LOGGER.debug("nbInstances= {}", instances.size()); } /** * Force to refresh folder that contains this file * @param path path to file */ public static void addFileToRefresh(String path) { synchronized (forceRefreshFilePath) { forceRefreshFilePath.add(path); } } } ================================================ FILE: src/main/java/com/mucommander/core/GlobalLocationHistory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.core; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.conf.Configuration; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcSnapshot; import com.mucommander.ui.event.LocationAdapter; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; /** * This class tracks location changing events, in every {@link FolderPanel} or {@link MainFrame}, * and saves those locations, thus creating a global location history tracking. * *

    FolderHistory also keeps track of the last visited location so that it can be saved and recalled the next time the * application is started. * * @author Arik Hadas */ public class GlobalLocationHistory extends LocationAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(GlobalLocationHistory.class); /** Singleton instance */ private static GlobalLocationHistory instance; /** Locations that were accessed */ private final Set history = new LinkedHashSet<>(); /** Maximum number of location that would be saved */ private static final int MAX_CAPACITY = 100; /** * Private Constructor */ private GlobalLocationHistory() { Configuration snapshot = TcConfigurations.getSnapshot(); // Restore the global history from last init int nbLocations = snapshot.getIntegerVariable(TcSnapshot.getRecentLocationsCountVariable()); for (int i = 0; i < nbLocations; ++i) { String filePath = snapshot.getVariable(TcSnapshot.getRecentLocationVariable(i)); try { history.add(FileURL.getFileURL(filePath)); } catch (MalformedURLException e) { LOGGER.debug("Got invalid URL from the snapshot file: {}", filePath, e); } catch (Throwable t) { LOGGER.debug("Can't process URL from the snapshot file: {}", filePath, t); } } } /** * Returns Singleton instance of this class * * @return Singleton instance of this class */ public static GlobalLocationHistory getInstance() { if (instance == null) { instance = new GlobalLocationHistory(); } return instance; } /** * Returns all the tracked locations as a {@link Set} of {@link AbstractFile} * The locations are turned in a reverse insertion-order, that means that the last accessed location would be the first * * @return all the tracked locations */ public List getHistory() { return new ArrayList<>(history); } /** * Returns true if the global history contains the given FileURL */ boolean historyContains(FileURL folderURL) { return history.contains(folderURL); } /////////////////////// /// LocationAdapter /// /////////////////////// public void locationChanged(LocationEvent locationEvent) { FileURL file = locationEvent.getFolderURL(); // remove the new location from the history as it should be put // at the end of the list to preserve the insertion order of the history boolean alreadyExists = history.remove(file); // ensure that we won't cross the maximum number of saved locations if (!alreadyExists && MAX_CAPACITY == history.size()) { history.remove(history.iterator().next()); } // add the location as last one in the history history.add(locationEvent.getFolderURL()); } } ================================================ FILE: src/main/java/com/mucommander/core/LocalLocationHistory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.core; import java.util.List; import java.util.Vector; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.ui.main.FolderPanel; /** * This class maintains a history of visited locations for a given tab, and provides methods to go back and go forward * in the history. * *

    There is a limit to the number of locations the history can contain, defined by {@link #HISTORY_CAPACITY}. * * @author Maxence Bernard, Arik Hadas */ public class LocalLocationHistory { private static final Logger LOGGER = LoggerFactory.getLogger(LocalLocationHistory.class); /** Maximum number of elements the folder history can contain */ private final static int HISTORY_CAPACITY = 100; /** List of visited locations, ordered by last visit date */ private final List history = new Vector<>(HISTORY_CAPACITY+1); /** Index of current folder in history */ private int historyIndex = -1; /** FolderPanel which is being monitored */ private final FolderPanel folderPanel; /** Last folder which can be recalled on next startup * -- GETTER -- * Returns the last visited folder that can be saved when the application terminates, and recalled next time * the application is started. *

    The returned folder will NOT be a folder on a remote filesystem which would be likely not to be reachable next * time the app is started, or a removable media drive (cd/dvd/floppy) under Windows, which would trigger a nasty * 'drive not ready' popup dialog if the drive is not available or the media has changed. */ @Getter private String lastRecallableFolder; /** * Creates a new FolderHistory instance which will keep track of visited folders in the given FolderPanel. * * @param folderPanel folder panel */ public LocalLocationHistory(FolderPanel folderPanel) { this.folderPanel = folderPanel; } /** * Adds the specified folder to history. The folder won't be added if the previous folder is the same. * *

    This method is called by FolderPanel each time a folder is changed. * * @param folderURL url to add */ public void tryToAddToHistory(FileURL folderURL) { // Do not add folder to history if new current folder is the same as previous folder if (historyIndex < 0 || !folderURL.equals(history.get(historyIndex), false, false)) { addToHistory(folderURL); } // Save last recallable folder on startup, only if : // - it is a directory on a local filesytem // - it doesn't look like a removable media drive (cd/dvd/floppy), especially in order to prevent // Java from triggering that dreaded 'Drive not ready' popup. LOGGER.trace("folder="+folderURL); if (folderURL.getScheme().equals(FileProtocols.FILE)) { AbstractFile folder = FileFactory.getFile(folderURL); if (folder instanceof LocalFile && folder.isDirectory() && !((LocalFile)folder.getRoot()).guessRemovableDrive()) { this.lastRecallableFolder = folder.getAbsolutePath(); LOGGER.trace("lastRecallableFolder= "+lastRecallableFolder); } } } private void addToHistory(FileURL folderURL) { int historySize = history.size(); historyIndex++; // Delete 'forward' history items if any if (historySize > historyIndex) { history.subList(historyIndex, historySize).clear(); } // If capacity is reached, remove first folder if (history.size() >= HISTORY_CAPACITY) { history.removeFirst(); historyIndex--; } // Add previous folder to history history.add(folderURL); } /** * Changes current folder to be the previous one in folder history. * Does nothing if there is no previous folder in history. */ public synchronized void goBack() { if (historyIndex == 0) { return; } folderPanel.tryChangeCurrentFolder(history.get(--historyIndex)); } /** * Changes current folder to be the next one in folder history. * Does nothing if there is no next folder in history. */ public synchronized void goForward() { if (historyIndex == history.size() - 1) { return; } folderPanel.tryChangeCurrentFolder(history.get(++historyIndex)); } /** * Returns true if there is at least one folder 'back' in the history. * @return true if there is at least one folder 'back' in the history. */ public boolean hasBackFolder() { return historyIndex>0; } /** * Returns true if there is at least one folder 'forward' in the history. * @return true if there is at least one folder 'forward' in the history. */ public boolean hasForwardFolder() { return historyIndex!=history.size()-1; } /** * Returns a list of 'back' folders, most recently visited folder first. The returned array may be empty if there * currently isn't any 'back' folder in history, but may never be null. * * @return a array of 'back' folders, most recently visited folder first */ public FileURL[] getBackFolders() { if (!hasBackFolder()) { return new FileURL[0]; } int backLen = historyIndex; FileURL[] urls = new FileURL[backLen]; int cur = 0; for (int i = historyIndex-1; i >= 0; i--) { urls[cur++] = history.get(i); } return urls; } /** * Returns a list of 'forward' folders, most recently visited folder first. The returned array may be empty if there * currently isn't any 'forward' folder in history, but may never be null. * * @return a array of 'forward' folders, most recently visited folder first */ public FileURL[] getForwardFolders() { if (!hasForwardFolder()) { return new FileURL[0]; } int historySize = history.size(); FileURL[] urls = new FileURL[historySize-historyIndex-1]; int cur = 0; for(int i=historyIndex+1; i. */ package com.mucommander.core; import com.mucommander.auth.CredentialsManager; import com.mucommander.auth.CredentialsMapping; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.CachedFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.dialog.auth.AuthDialog; import com.mucommander.ui.dialog.file.DownloadDialog; import com.mucommander.ui.event.LocationManager; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.utils.text.Translator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.*; import java.net.MalformedURLException; /** * * @author Arik Hadas, Maxence Bernard */ public class LocationChanger { private static final Logger LOGGER = LoggerFactory.getLogger(LocationChanger.class); /** Last time folder has changed */ private long lastFolderChangeTime; private ChangeFolderThread changeFolderThread; private final GlobalLocationHistory globalHistory = GlobalLocationHistory.getInstance(); /** The lock object used to prevent simultaneous folder change operations */ private final Object FOLDER_CHANGE_LOCK = new Object(); private final static int CANCEL_ACTION = 0; private final static int BROWSE_ACTION = 1; private final static int DOWNLOAD_ACTION = 2; private final static String CANCEL_TEXT = Translator.get("cancel"); private final static String BROWSE_TEXT = Translator.get("browse"); private final static String DOWNLOAD_TEXT = Translator.get("download"); private final MainFrame mainFrame; private final FolderPanel folderPanel; private final LocationManager locationManager; public LocationChanger(MainFrame mainFrame, FolderPanel folderPanel, LocationManager locationManager) { this.mainFrame = mainFrame; this.folderPanel = folderPanel; this.locationManager = locationManager; } /** * This method is triggered internally (i.e not by user request) to change the current * folder to the given folder * * @param folderURL the URL of the folder to switch to * @param callback the {@link Runnable#run()} method will be called when folder has changed */ public void tryChangeCurrentFolderInternal(final FileURL folderURL, Runnable callback) { mainFrame.setNoEventsMode(true); showWaitCursor(); Thread setLocationThread = new Thread(() -> { AbstractFile folder = getWorkableLocation(folderURL); try { locationManager.setCurrentFolder(folder, null, true); } finally { mainFrame.setNoEventsMode(false); restoreDefaultCursor(); // Notify callback that the folder has been set callback.run(); } }); if (EventQueue.isDispatchThread()) { setLocationThread.start(); } else { setLocationThread.run(); } } private void restoreDefaultCursor() { mainFrame.getJFrame().setCursor(Cursor.getDefaultCursor()); } private void showWaitCursor() { mainFrame.getJFrame().setCursor(new Cursor(Cursor.WAIT_CURSOR)); } /** * Return a workable location according the following logic: * - If the given folder exists, return it * - if the given folder is local file, find workable location * according to the logic used for inaccessible local files * - Otherwise, return the non-exist remote location */ private AbstractFile getWorkableLocation(FileURL folderURL) { AbstractFile folder = FileFactory.getFile(folderURL); if (folder != null && folder.exists()) { return folder; } if (folder == null) { folder = new NullableFile(folderURL); } return FileProtocols.FILE.equals(folderURL.getScheme()) ? getWorkableFolder(folder) : folder; } /** * Tries to change the current folder to the new specified one and notifies the user in case of a problem. * *

    This method spawns a separate thread that takes care of the actual folder change and returns it. * It does nothing and returns null if another folder change is already underway. * *

    * This method is not I/O-bound and returns immediately, without any chance of locking the calling thread. * * @param folder the folder to be made current folder * @param changeLockedTab - flag that indicates whether to change the presented folder in the currently selected tab although it's locked * @return the thread that performs the actual folder change, null if another folder change is already underway */ public ChangeFolderThread tryChangeCurrentFolder(AbstractFile folder, boolean changeLockedTab) { // TODO branch setBranchView(false); return tryChangeCurrentFolder(folder, null, false, changeLockedTab); } /** * Tries to change current folder to the new specified one, and selects the given file after the folder has been * changed. The user is notified by a dialog if the folder could not be changed. * *

    If the current folder could not be changed to the requested folder and findWorkableFolder is * true, the current folder will be changed to the first existing parent of the request folder if there * is one, to the first existing local volume otherwise. In the unlikely event that no local volume is workable, * the user will be notified that the folder could not be changed. * *

    This method spawns a separate thread that takes care of the actual folder change and returns it. * It does nothing and returns null if another folder change is already underway. * *

    * This method is not I/O-bound and returns immediately, without any chance of locking the calling thread. * * @param folder the folder to be made current folder * @param selectThisFileAfter the file to be selected after the folder has been changed (if it exists in the folder), can be null in which case FileTable rules will be used to select current file * @param changeLockedTab - flag that indicates whether to change the presented folder in the currently selected tab although it's locked * @return the thread that performs the actual folder change, null if another folder change is already underway */ public ChangeFolderThread tryChangeCurrentFolder(AbstractFile folder, AbstractFile selectThisFileAfter, boolean findWorkableFolder, boolean changeLockedTab) { LOGGER.debug("folder= {} selectThisFileAfter={}", folder, selectThisFileAfter); synchronized(FOLDER_CHANGE_LOCK) { // Make sure a folder change is not already taking place. This can happen under rare but normal // circumstances, if this method is called before the folder change thread has had the time to call // MainFrame#setNoEventsMode. if (isChangeFolderThreadAlreadyTakingPlace()) return null; // Important: the ChangeFolderThread instance must be kept in a local variable (as opposed to the // changeFolderThread field only) before being returned. The reason for this is that ChangeFolderThread // changes the changeFolderThread field to null when finished, and it may do so before this method has // returned (I've seen this happening). Relying solely on the changeFolderThread field could thus cause // a null value to be returned, which is particularly problematic during startup (would cause an NPE). ChangeFolderThread thread = new ChangeFolderThread(folder, findWorkableFolder, changeLockedTab); if (selectThisFileAfter != null) { thread.selectThisFileAfter(selectThisFileAfter); } thread.start(); changeFolderThread = thread; return thread; } } /** * Tries to change the current folder to the specified path and notifies the user in case of a problem. * *

    This method spawns a separate thread that takes care of the actual folder change and returns it. * It does nothing and returns null if another folder change is already underway or if the given * path could not be resolved. * *

    * This method is not I/O-bound and returns immediately, without any chance of locking the calling thread. * * @param folderPath path to the new current folder. If this path does not resolve into a file, an error message will be displayed. * @return the thread that performs the actual folder change, null if another folder change is already underway or if the given path could not be resolved */ public ChangeFolderThread tryChangeCurrentFolder(String folderPath) { try { return tryChangeCurrentFolder(FileURL.getFileURL(folderPath), null, false); } catch (MalformedURLException e) { // FileURL could not be resolved, notify the user that the folder doesn't exist showFolderDoesNotExistDialog(); return null; } } /** * Tries to change current folder to the new specified URL and notifies the user in case of a problem. * *

    This method spawns a separate thread that takes care of the actual folder change and returns it. * It does nothing and returns null if another folder change is already underway. * *

    * This method is not I/O-bound and returns immediately, without any chance of locking the calling thread. * * @param folderURL location to the new current folder. If this URL does not resolve into a file, an error message will be displayed. * @return the thread that performs the actual folder change, null if another folder change is already underway */ public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL) { return tryChangeCurrentFolder(folderURL, null, false); } public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL, boolean changeLockedTab) { return tryChangeCurrentFolder(folderURL, null, changeLockedTab); } /** * Tries to change current folder to the new specified path and notifies the user in case of a problem. * If not null, the specified {@link com.mucommander.auth.CredentialsMapping} is used to authenticate * the folder, and added to {@link CredentialsManager} if the folder has been successfully changed. * *

    This method spawns a separate thread that takes care of the actual folder change and returns it. * It does nothing and returns null if another folder change is already underway. * *

    * This method is not I/O-bound and returns immediately, without any chance of locking the calling thread. * * @param folderURL folder's URL to be made current folder. If this URL does not resolve into an existing file, an error message will be displayed. * @param credentialsMapping the CredentialsMapping to use for authentication, can be null * @param changeLockedTab flag that indicates whether to change the presented folder in the currently selected tab although it's locked * @return the thread that performs the actual folder change, null if another folder change is already underway */ public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL, CredentialsMapping credentialsMapping, boolean changeLockedTab) { LOGGER.debug("folderURL=" + folderURL); synchronized(FOLDER_CHANGE_LOCK) { // Make sure a folder change is not already taking place. This can happen under rare but normal // circumstances, if this method is called before the folder change thread has had the time to call // MainFrame#setNoEventsMode. if (isChangeFolderThreadAlreadyTakingPlace()) return null; // Important: the ChangeFolderThread instance must be kept in a local variable (as opposed to the // changeFolderThread field only) before being returned. The reason for this is that ChangeFolderThread // changes the changeFolderThread field to null when finished, and it may do so before this method has // returned (I've seen this happening). Relying solely on the changeFolderThread field could thus cause // a null value to be returned, which is particularly problematic during startup (would cause an NPE). ChangeFolderThread thread = new ChangeFolderThread(folderURL, credentialsMapping, changeLockedTab); thread.start(); changeFolderThread = thread; return thread; } } private boolean isChangeFolderThreadAlreadyTakingPlace() { if (changeFolderThread != null) { LOGGER.debug("A folder change is already taking place ({}), returning null", changeFolderThread); return true; } return false; } /** * Shorthand for {@link #tryRefreshCurrentFolder(AbstractFile)} called with no specific file (null) * to select after the folder has been changed. * * @return the thread that performs the actual folder change, null if another folder change is already underway */ public ChangeFolderThread tryRefreshCurrentFolder() { return tryRefreshCurrentFolder(null); } /** * Refreshes the current folder's contents. If the folder is no longer available, the folder will be changed to a * 'workable' folder (see {@link #tryChangeCurrentFolder(AbstractFile, AbstractFile, boolean, boolean)}. * *

    This method spawns a separate thread that takes care of the actual folder change and returns it. * It does nothing and returns null if another folder change is already underway. * *

    This method is not I/O-bound and returns immediately, without any chance of locking the calling thread. * * @param selectThisFileAfter file to be selected after the folder has been refreshed (if it exists in the folder), * can be null in which case FileTable rules will be used to select current file * @return the thread that performs the actual folder change, null if another folder change is already underway * @see #tryChangeCurrentFolder(AbstractFile, AbstractFile, boolean, boolean) */ public ChangeFolderThread tryRefreshCurrentFolder(AbstractFile selectThisFileAfter) { folderPanel.getFoldersTreePanel().refreshFolder(locationManager.getCurrentFolder()); return tryChangeCurrentFolder(locationManager.getCurrentFolder(), selectThisFileAfter, true, true); } /** * Changes current folder using the given folder and children files. * *

    * This method is I/O-bound and locks the calling thread until the folder has been changed. It may under * certain circumstances lock indefinitely, for example when accessing network-based filesystems. * * @param folder folder to be made current folder * @param fileToSelect file to be selected after the folder has been refreshed (if it exists in the folder), can be null in which case FileTable rules will be used to select current file * @param changeLockedTab - flag that indicates whether to change the presented folder in the currently selected tab although it's locked */ private void setCurrentFolder(AbstractFile folder, AbstractFile fileToSelect, boolean changeLockedTab) { // Update the timestamp right before the folder is set in case FolderChangeMonitor checks the timestamp // while FileTable#setCurrentFolder is being called. lastFolderChangeTime = System.currentTimeMillis(); locationManager.setCurrentFolder(folder, fileToSelect, changeLockedTab); } /** * Returns the time at which the last folder change completed successfully. * * @return the time at which the last folder change completed successfully. */ public long getLastFolderChangeTime() { return lastFolderChangeTime; } /** * Returns the thread that is currently changing the current folder, null is the folder is not being * changed. * * @return the thread that is currently changing the current folder, null is the folder is not being * changed */ public ChangeFolderThread getChangeFolderThread() { return changeFolderThread; } /** * Returns true ´if the current folder is currently being changed, false otherwise. * * @return true ´if the current folder is currently being changed, false otherwise */ public boolean isFolderChanging() { return changeFolderThread != null; } private void showFailedToReadFolderDialog() { InformationDialog.showErrorDialog(mainFrame.getJFrame(), Translator.get("table.folder_access_error_title"), Translator.get("failed_to_read_folder")); } /** * Displays a popup dialog informing the user that the requested folder doesn't exist or isn't available. */ private void showFolderDoesNotExistDialog() { InformationDialog.showErrorDialog(mainFrame.getJFrame(), Translator.get("table.folder_access_error_title"), Translator.get("folder_does_not_exist")); } /** * Displays a popup dialog informing the user that the requested folder couldn't be opened. * * @param e the Exception that was caught while changing the folder */ private void showAccessErrorDialog(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), Translator.get("table.folder_access_error_title"), Translator.get("table.folder_access_error"), e == null ? null : e.getMessage(), e); } /** * Pops up an {@link AuthDialog authentication dialog} prompting the user to select or enter credentials in order to * be granted the access to the file or folder represented by the given {@link FileURL}. * The AuthDialog instance is returned, allowing to retrieve the credentials that were selected * by the user (if any). * * @param fileURL the file or folder to ask credentials for * @param errorMessage optional (can be null), an error message describing a prior authentication failure * @return the AuthDialog that contains the credentials selected by the user (if any) */ private AuthDialog popAuthDialog(FileURL fileURL, boolean authFailed, String errorMessage) { AuthDialog authDialog = new AuthDialog(mainFrame, fileURL, authFailed, errorMessage); authDialog.showDialog(); return authDialog; } /** * Displays a download dialog box where the user can choose where to download the given file or cancel * the operation. * * @param file the file to download */ private void showDownloadDialog(AbstractFile file) { FileSet fileSet = new FileSet(locationManager.getCurrentFolder()); fileSet.add(file); // Show confirmation/path modification dialog new DownloadDialog(mainFrame, fileSet).showDialog(); } /** * Returns a 'workable' folder as a substitute for the given non-existing folder. This method will return the * first existing parent if there is one, to the first existing local volume otherwise. In the unlikely event * that no local volume exists, null will be returned. * * @param folder folder for which to find a workable folder * @return a 'workable' folder for the given non-existing folder, null if there is none. */ private AbstractFile getWorkableFolder(AbstractFile folder) { // Look for an existing parent AbstractFile newFolder = folder; do { newFolder = newFolder.getParent(); if (newFolder != null && newFolder.exists()) { return newFolder; } } while (newFolder != null); // Fall back to the first existing volume AbstractFile[] localVolumes = LocalFile.getVolumes(); for (AbstractFile volume : localVolumes) { if (volume.exists()) { return volume; } } // No volume could be found, return null return null; } /** * This thread takes care of changing current folder without locking the main * thread. The folder change can be cancelled. * *

    A little note out of nowhere: never ever call JComponent.paintImmediately() from a thread * other than the Event Dispatcher Thread, as will create nasty repaint glitches that * then become very hard to track. Sun's Javadoc doesn't make it clear enough... just don't! * * @author Maxence Bernard */ public class ChangeFolderThread extends Thread { private AbstractFile folder; private boolean findWorkableFolder; private final boolean changeLockedTab; private FileURL folderURL; private AbstractFile fileToSelect; private CredentialsMapping credentialsMapping; /** True if this thread has been interrupted by the user using #tryKill */ private boolean killed; /** True if an attempt to kill this thread using Thread#interrupt() has already been made */ private boolean killedByInterrupt; /** True if an attempt to kill this thread using Thread#stop() has already been made */ private boolean killedByStop; /** True if it is unsafe to kill this thread */ private boolean doNotKill; private boolean disposed; /** Lock object used to ensure consistency and thread safeness when killing the thread */ private final Object KILL_LOCK = new Object(); /* TODO branch private ArrayList childrenList; */ ChangeFolderThread(AbstractFile folder, boolean findWorkableFolder, boolean changeLockedTab) { // Ensure that we work on a raw file instance and not a cached one this.folder = (folder instanceof CachedFile)?((CachedFile)folder).getProxiedFile():folder; this.folderURL = folder.getURL(); this.findWorkableFolder = findWorkableFolder; this.changeLockedTab = changeLockedTab; setPriority(Thread.MAX_PRIORITY); } /** * * @param folderURL folder's URL to be made current folder. If this URL does not resolve into an existing file, an error message will be displayed. * @param credentialsMapping the CredentialsMapping to use for accessing the folder, null for none * @param changeLockedTab flag that indicates whether to change the presented folder in the currently selected tab although it's locked */ ChangeFolderThread(FileURL folderURL, CredentialsMapping credentialsMapping, boolean changeLockedTab) { this.folderURL = folderURL; this.changeLockedTab = changeLockedTab; this.credentialsMapping = credentialsMapping; setPriority(Thread.MAX_PRIORITY); } /** * Sets the file to be selected after the folder has been changed, null for none. * * @param fileToSelect the file to be selected after the folder has been changed */ void selectThisFileAfter(AbstractFile fileToSelect) { this.fileToSelect = fileToSelect; } /** * Returns true if the given file should have its canonical path followed. In that case, the * AbstractFile instance must be resolved again. * *

    HTTP files MUST have their canonical path followed. For all other file protocols, this is an option in * the preferences. * * @param file the file to test * @return true if the given file should have its canonical path followed */ private boolean followCanonicalPath(AbstractFile file) { return (followsSymlinkEnabled() || file.getURL().getScheme().equals(FileProtocols.HTTP)) && !file.getAbsolutePath(false).equals(file.getCanonicalPath(false)); } /** * Attempts to stop this thread and returns true if an attempt was made. * An attempt to stop this thread will be made using one of the methods detailed hereunder, only if * it is still safe to do so: if the thread is too far into the process of changing the current folder, * this method will have no effect and return false. * *

    The first time this method is called, {@link #interrupt()} is called, giving the thread a chance to stop * gracefully should it be waiting for a thread or blocked in an interruptible operation such as an * InterruptibleChannel. This may have no immediate effect if the thread is blocked in a non-interruptible * operation. This thread will however be marked as 'killed' which will sooner or later cause {@link #run()} * to stop the thread by simply returning. * *

    The second time this method is called, the deprecated (and unsafe) {@link #stop()} method is called, * forcing the thread to abort. * *

    Any subsequent calls to this method will have no effect and return false. * * @return true if an attempt was made to stop this thread. */ public boolean tryKill() { synchronized(KILL_LOCK) { if (killedByStop) { LOGGER.debug("Thread already killed by #interrupt() and #stop(), there's nothing we can do, returning"); return false; } if (doNotKill) { LOGGER.debug("Can't kill thread now, it's too late, returning"); return false; } // This field needs to be set before actually killing the thread, #init() relies on it killed = true; // Call Thread#interrupt() the first time this method is called to give the thread a chance to stop // gracefully if it is waiting in Thread#sleep() or Thread#wait() or Thread#join() or in an // interruptible operation such as java.nio.channel.InterruptibleChannel. If this is the case, // InterruptedException or ClosedByInterruptException will be thrown and thus need to be catched by // #init(). if (!killedByInterrupt) { LOGGER.debug("Killing thread using #interrupt()"); // This field needs to be set before actually interrupting the thread, #init() relies on it killedByInterrupt = true; interrupt(); } else { // Call Thread#stop() the first time this method is called LOGGER.debug("Killing thread using #stop()"); killedByStop = true; // Execute #cleanup() as it would have been done by #init() had the thread not been stopped. // Note that #init() may end pseudo-gracefully and catch the underlying Exception. In this case // it will also call #cleanup() but the (2nd) call to #cleanup() will be ignored. cleanup(false); } return true; } } @Override public void start() { // Notify listeners that location is changing locationManager.fireLocationChanging(folder == null ? folderURL : folder.getURL()); super.start(); } @Override public void run() { LOGGER.debug("starting folder change..."); boolean folderChangedSuccessfully = false; // Show some progress in the progress bar to give hope folderPanel.setProgressValue(10); CredentialsMapping newCredentialsMapping = null; // True if Guest authentication was selected in the authentication dialog (guest credentials must not be // added to CredentialsManager) boolean guestCredentialsSelected = false; boolean userCancelled = false; if (credentialsMapping != null) { newCredentialsMapping = credentialsMapping; CredentialsManager.authenticate(folderURL, newCredentialsMapping); } else if (shouldDisplayAuthDialog()) { AuthDialog authDialog = popAuthDialog(folderURL, false, null); newCredentialsMapping = authDialog.getCredentialsMapping(); guestCredentialsSelected = authDialog.guestCredentialsSelected(); // User cancelled the authentication dialog, stop if (newCredentialsMapping == null) { userCancelled = true; } else { // Use the provided credentials and invalidate the folder AbstractFile instance (if any) so that // it gets recreated with the new credentials CredentialsManager.authenticate(folderURL, newCredentialsMapping); folder = null; } } if (!userCancelled) { boolean canonicalPathFollowed = false; do { showWaitCursor(); // Render all actions inactive while changing folder mainFrame.setNoEventsMode(true); try { // 2 cases here : // - Thread was created using an AbstractFile instance // - Thread was created using a FileURL, corresponding AbstractFile needs to be resolved if (folder == null) { // Thread was created using a FileURL if (processFile(FileFactory.getFile(folderURL, true))) { break; } } else if (!folder.exists()) { // Thread was created using an AbstractFile instance, check file existence // Find a 'workable' folder if the requested folder doesn't exist anymore if (findWorkableFolder) { AbstractFile newFolder = getWorkableFolder(folder); if (folder.equals(newFolder)) { // If we've already tried the returned folder, give up (avoids a potentially endless loop) showFolderDoesNotExistDialog(); break; } // Try again with the new folder folder = newFolder; folderURL = folder != null ? folder.getURL() : null; // Discard the file to select, if any fileToSelect = null; continue; } else { showFolderDoesNotExistDialog(); break; } } else if (!folder.canRead()) { showFailedToReadFolderDialog(); break; } // Checks if canonical should be followed. If that is the case, the file is invalidated // and resolved again. This happens only once at most, to avoid a potential infinite loop // in the event that the absolute path still didn't match canonical one after the file is // resolved again. if (!canonicalPathFollowed && followCanonicalPath(folder)) { try { // Recreate the FileURL using the file's canonical path FileURL newURL = FileURL.getFileURL(folder.getCanonicalPath()); // Keep the credentials and properties (if any) newURL.setCredentials(folderURL.getCredentials()); newURL.importProperties(folderURL); this.folderURL = newURL; // Invalidate the AbstractFile instance this.folder = null; // There won't be any further attempts after this one canonicalPathFollowed = true; // Loop the resolve the file continue; } catch (MalformedURLException e) { // In the unlikely event of the canonical path being malformed, the AbstractFile // and FileURL instances are left untouched } } synchronized(KILL_LOCK) { if (killed) { LOGGER.debug("this thread has been killed, returning"); break; } } // File tested -> 50% complete folderPanel.setProgressValue(50); /* TODO branch AbstractFile children[] = new AbstractFile[0]; if (branchView) { childrenList = new ArrayList(); readBranch(folder); children = (AbstractFile[]) childrenList.toArray(children); } else { children = folder.ls(chainedFileFilter); }*/ synchronized(KILL_LOCK) { if (killed) { LOGGER.debug("this thread has been killed, returning"); break; } // From now on, thread cannot be killed (would comprise table integrity) doNotKill = true; } // files listed -> 75% complete folderPanel.setProgressValue(75); LOGGER.trace("calling setCurrentFolder"); // Change the file table's current folder and select the specified file (if any) setCurrentFolder(folder, fileToSelect, changeLockedTab); // folder set -> 95% complete folderPanel.setProgressValue(95); // If new credentials were entered by the user, these can now be considered valid // (folder was changed successfully), so we add them to the CredentialsManager. // Do not add the credentials if guest credentials were selected by the user. if (newCredentialsMapping != null && !guestCredentialsSelected) { CredentialsManager.addCredentials(newCredentialsMapping); } // All good ! folderChangedSuccessfully = true; break; } catch(Exception e) { LOGGER.debug("Caught exception", e); if (killed) { // If #tryKill() called #interrupt(), the exception we just caught was most likely // thrown as a result of the thread being interrupted. // // The exception can be a java.lang.InterruptedException (Thread throws those), // a java.nio.channels.ClosedByInterruptException (InterruptibleChannel throws those) // or any other exception thrown by some code that swallowed the original exception // and threw a new one. LOGGER.debug("Thread was interrupted, ignoring exception"); break; } // Restore default cursor restoreDefaultCursor(); if (e instanceof AuthException authException) { // Retry (loop) if user provided new credentials, if not stop AuthDialog authDialog = popAuthDialog(authException.getURL(), true, authException.getMessage()); newCredentialsMapping = authDialog.getCredentialsMapping(); guestCredentialsSelected = authDialog.guestCredentialsSelected(); if (newCredentialsMapping != null) { // Invalidate the existing AbstractFile instance folder = null; // Use the provided credentials CredentialsManager.authenticate(folderURL, newCredentialsMapping); continue; } } else { // Find a 'workable' folder if the requested folder doesn't exist anymore if (findWorkableFolder) { AbstractFile newFolder = getWorkableFolder(folder); if (folder.equals(newFolder)) { // If we've already tried the returned folder, give up (avoids a potentially endless loop) showFolderDoesNotExistDialog(); break; } // Try again with the new folder folder = newFolder; folderURL = folder != null ? folder.getURL() : null; // Discard the file to select, if any fileToSelect = null; continue; } showAccessErrorDialog(e); } // Stop looping! break; } } while(true); } synchronized(KILL_LOCK) { cleanup(folderChangedSuccessfully); } } private boolean processFile(AbstractFile file) { synchronized(KILL_LOCK) { if (killed) { LOGGER.debug("this thread has been killed, returning"); return true; } } // File resolved -> 25% complete folderPanel.setProgressValue(25); // Popup an error dialog and abort folder change if the file could not be resolved // or doesn't exist if (file == null || !file.exists()) { // Restore default cursor restoreDefaultCursor(); showFolderDoesNotExistDialog(); return true; } if (!file.canRead()) { // Restore default cursor restoreDefaultCursor(); showFailedToReadFolderDialog(); return true; } // File is a regular directory, all good if (file.isDirectory()) { // Just continue } else if (file.isBrowsable()) { // File is a browsable file (Zip archive for instance) but not a directory : Browse or Download ? => ask the user // If history already contains this file, do not ask the question again and assume // the user wants to 'browse' the file. In particular, this prevent the 'Download or browse' // dialog from popping up when going back or forward in history. // The dialog is also not displayed if the file corresponds to the currently selected file, // which is a weak (and not so accurate) way to know if the folder change is the result // of the OpenAction (enter pressed on the file). This works well enough in practice. if (!globalHistory.historyContains(folderURL) && !file.equals(folderPanel.getFileTable().getSelectedFile())) { restoreDefaultCursor(); // Download or browse file ? QuestionDialog dialog = new QuestionDialog(mainFrame.getJFrame(), null, Translator.get("table.download_or_browse"), mainFrame.getJFrame(), new String[] {BROWSE_TEXT, DOWNLOAD_TEXT, CANCEL_TEXT}, new int[] {BROWSE_ACTION, DOWNLOAD_ACTION, CANCEL_ACTION}, 0); int ret = dialog.getActionValue(); if (ret == -1 || ret == CANCEL_ACTION) { return true; } // Download file if (ret == DOWNLOAD_ACTION) { showDownloadDialog(file); return true; } // Continue if BROWSE_ACTION // Set cursor to hourglass/wait showWaitCursor(); } // else just continue and browse file's contents } else { // File is a regular file: show download dialog which allows to download (copy) the file // to a directory specified by the user showDownloadDialog(file); return true; } this.folder = file; return false; } private boolean shouldDisplayAuthDialog() { // If the URL doesn't contain any credentials and authentication for this file protocol is required, or // optional and CredentialsManager has credentials for this location, popup the authentication dialog to // avoid waiting for an AuthException to be thrown. if (folderURL.containsCredentials()) { return false; } AuthenticationType authenticationType = folderURL.getAuthenticationType(); if (authenticationType == AuthenticationType.AUTHENTICATION_REQUIRED) { return true; } return authenticationType == AuthenticationType.AUTHENTICATION_OPTIONAL && CredentialsManager.getMatchingCredentials(folderURL).length > 0; } void cleanup(boolean folderChangedSuccessfully) { // Ensures that this method is called only once synchronized(KILL_LOCK) { if (disposed) { LOGGER.debug("already called, returning"); return; } disposed = true; } LOGGER.trace("cleaning up, folderChangedSuccessfully="+folderChangedSuccessfully); // Clear the interrupted flag in case this thread has been killed using #interrupt(). // Not doing this could cause some of the code called by this method to be interrupted (because this thread // is interrupted) and throw an exception interrupted(); // Reset location field's progress bar folderPanel.setProgressValue(0); // Restore normal mouse cursor restoreDefaultCursor(); synchronized(FOLDER_CHANGE_LOCK) { changeFolderThread = null; } // Make all actions active again mainFrame.setNoEventsMode(false); if (!folderChangedSuccessfully) { FileURL failedURL = folder == null ? folderURL : folder.getURL(); // Notifies listeners that location change has been cancelled by the user or has failed if (killed) { locationManager.fireLocationCancelled(failedURL); } else { locationManager.fireLocationFailed(failedURL); } } } // For debugging purposes public String toString() { return super.toString() + " folderURL=" + folderURL + " folder=" + folder; } } private boolean followsSymlinkEnabled() { return TcConfigurations.getPreferences().getVariable(TcPreference.CD_FOLLOWS_SYMLINKS, TcPreferences.DEFAULT_CD_FOLLOWS_SYMLINKS); } /* TODO branch *//* private void readBranch(AbstractFile parent) { AbstractFile[] children; try { children = parent.ls(chainedFileFilter); for (int i=0; i. */ package com.mucommander.desktop; import com.mucommander.commons.file.AbstractFile; /** * AbstractTrash is an abstract representation of a file trash, i.e. a temporary place where deleted files are stored * before the trash is emptied. A trash implementation provides methods to access basic trash operations: move files * to the trash, empty the trash, ... * *

    Since AbstractTrash implementations are system-dependent, they should not be instantiated directly. * Use {@link DesktopManager#getTrash()} to retrieve an instance of a trash implementation that can * be used on the current platform.
    * Also, some AbstractTrash subclasses may not be able to provide working implementations for all trash operations; * probe methods are provided to find out if a particular operation is available. * * @see TrashProvider * @see DesktopManager#getTrash() * @author Maxence Bernard */ public abstract class AbstractTrash { /** * Returns true if the specified file is eligible for being moved to the trash. This doesn't mean that * a call to {@link #moveToTrash(com.mucommander.commons.file.AbstractFile)} will necessarily succeed, but it should at least ensure that basic * prerequisites are met. * * @param file the file to test * @return true if the given file can be moved to the trash */ public abstract boolean canMoveToTrash(AbstractFile file); /** * Attempts to move the given file to the trash and returns true if the file could be moved successfully. * * @param file the file to move to the trash * @return true if the file could successfully be moved to the trash */ public abstract boolean moveToTrash(AbstractFile file); /** * Returns true if this trash can be emptied. * * @return true if the trash can be emptied. */ public abstract boolean canEmpty(); /** * Attempts to empty this trash and returns true if it was successfully emptied. * * @return true if the trash was successfully emptied */ public abstract boolean empty(); /** * Returns true if the given file is a trash folder, or one of its children. * For example, if /home/someuser/.Trash is a trash folder, calling this method with: *

      *
    • /home/someuser/.Trash will return true *
    • /home/someuser/.Trash/somefolder/somefile will return true *
    • /home/someuser/Desktop will return false *
    * *

    Note that this method does not check the existence of the given file, the test is solely based on the * file's path. * * @param file the file to test * @return true if the given file is a trash folder, or one of its children. */ public abstract boolean isTrashFile(AbstractFile file); /** * Returns the number of items that currently are in this trash, -1 if this information is not available. * * @return the number of items that currently are in this trash, -1 if this information is not available */ public abstract int getItemCount(); /** * Opens the trash in the default file manager of the current OS/Desktop manager. */ public abstract void open(); /** * Returns true if this trash can be opened in the default file manager of the current OS/Desktop manager * by calling {@link #open()}. * * @return true if this trash can be opened in the default file manager of the current OS/Desktop manager. */ public abstract boolean canOpen(); /** * Waits (locks the caller thread) until all pending trash operations are completed. * This method can be useful since some AbstractTrash implementations are asynchroneous, i.e. perform * operations in separate threads. */ public abstract void waitForPendingOperations(); } ================================================ FILE: src/main/java/com/mucommander/desktop/CommandBrowse.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.process.ProcessRunner; import java.io.IOException; import java.net.URL; /** * @author Nicolas Rinaudo */ class CommandBrowse extends UrlOperation { // - Desktop operation implementation -------------------------------- // ------------------------------------------------------------------- @Override public boolean isAvailable() { return CommandManager.getCommandForAlias(CommandManager.URL_OPENER_ALIAS, null) != null; } @Override public void execute(URL url) throws IOException { AbstractFile target = FileFactory.getFile(url.toString()); Command command = CommandManager.getCommandForAlias(CommandManager.URL_OPENER_ALIAS, target); if (command == null) { throw new UnsupportedOperationException(); } ProcessRunner.execute(command.getTokens(target), target); } /** * Returns the operation's name. * @return the operation's name. */ @Override public String getName() {return "openURL bridge";} } ================================================ FILE: src/main/java/com/mucommander/desktop/CommandOpen.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.process.ProcessRunner; import java.io.IOException; /** * @author Nicolas Rinaudo */ class CommandOpen extends LocalFileOperation { /** Whether the 'init as executable' command can be used if no better alternative is found. */ private final boolean allowDefault; CommandOpen(boolean allowDefault) { this.allowDefault = allowDefault; } @Override public boolean isAvailable() { if (allowDefault) { return true; } return CommandManager.getCommandForAlias(CommandManager.FILE_OPENER_ALIAS, null) != null; } @Override public boolean canExecute(AbstractFile file) { return allowDefault || CommandManager.getCommandForFile(file, false) != null; } @Override public void execute(AbstractFile file) throws IOException { Command command = CommandManager.getCommandForFile(file, allowDefault); // Attempts to find a command that matches the specified target. if (command == null) { throw new UnsupportedOperationException(); } // If found, executes it. ProcessRunner.execute(command.getTokens(file), file); } /** * Returns the operation's name. * @return the operation's name. */ @Override public String getName() { return "open bridge"; } } ================================================ FILE: src/main/java/com/mucommander/desktop/CommandOpenInFileManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.process.ProcessRunner; import java.io.IOException; /** * @author Nicolas Rinaudo */ class CommandOpenInFileManager extends LocalFileOperation { @Override public boolean isAvailable() { return CommandManager.getCommandForAlias(CommandManager.FILE_MANAGER_ALIAS, null) != null; } @Override public void execute(AbstractFile file) throws IOException { Command command = CommandManager.getCommandForAlias(CommandManager.FILE_MANAGER_ALIAS, file); if (command == null) { throw new UnsupportedOperationException(); } ProcessRunner.execute(command.getTokens(file), file); } @Override public String getName() { Command command = CommandManager.getCommandForAlias(CommandManager.FILE_MANAGER_ALIAS, null); return command == null ? null : command.getDisplayName(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/DefaultDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import java.awt.Toolkit; import java.awt.event.MouseEvent; import java.io.File; import com.mucommander.commons.runtime.OsFamily; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; /** * Provides a default implementation of the {@link DesktopAdapter} interface. *

    * This implementation is meant to help application developers by providing standard * implementations of all {@link DesktopAdapter} methods, letting subclasses concentrate * on what's important rather than mundane. * *

    * Moreover, an instance of DefaultDesktopAdapter will be used by the * {@link DesktopManager} if no valid desktop could be identifier. * * @author Nicolas Rinaudo, Maxence Bernard */ public class DefaultDesktopAdapter implements DesktopAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDesktopAdapter.class); /** Default multi-click interval when the desktop property cannot be retrieved. */ public final static int DEFAULT_MULTICLICK_INTERVAL = 500; /** Multi-click interval, cached to avoid polling the value every time {@link #getMultiClickInterval()} is called */ private static int multiClickInterval; private String defaultShell; static { try { Integer value = ((Integer)Toolkit.getDefaultToolkit().getDesktopProperty("awt.multiClickInterval")); multiClickInterval = value == null ? DEFAULT_MULTICLICK_INTERVAL : value; } catch (Exception e) { LOGGER.debug("Error while retrieving multi-click interval value desktop property", e); multiClickInterval = DEFAULT_MULTICLICK_INTERVAL; } } public String toString() { return "Default Desktop"; } /** * Returns true. * @return true. */ public boolean isAvailable() { return true; } /** * Initializes this desktop. *

    * This method is empty. See {@link DesktopAdapter#init(boolean)} for information on * how to override it. * * @param install true if this is the application's first boot, false otherwise. * @throws DesktopInitializationException if any error occurs. */ public void init(boolean install) throws DesktopInitializationException { } /** * Returns true if the specified mouse event describes a left click. *

    * This method will return true if (e.getModifiers() & MouseEvent.BUTTON1_MASK) * doesn't equal 0. * * @param e event to check. * @return true if the specified event is a left-click, false otherwise. * @see #isRightMouseButton(MouseEvent) * @see #isMiddleMouseButton(MouseEvent) */ public boolean isLeftMouseButton(MouseEvent e) { return (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0; } /** * Returns true if the specified mouse event describes a middle click. *

    * This method will return true if (e.getModifiers() & MouseEvent.BUTTON3_MASK) * doesn't equal 0. * * @param e event to check. * @return true if the specified event is a middle-click, false otherwise. * @see #isRightMouseButton(MouseEvent) * @see #isLeftMouseButton(MouseEvent) */ public boolean isRightMouseButton(MouseEvent e) { return (e.getModifiersEx() & MouseEvent.BUTTON3_DOWN_MASK) !=0; } /** * Returns true if the specified mouse event describes a right click. *

    * This method will return true if (e.getModifiers() & MouseEvent.BUTTON2_MASK) * doesn't equal 0. * * @param e event to check. * @return true if the specified event is a right-click, false otherwise. * @see #isLeftMouseButton(MouseEvent) * @see #isMiddleMouseButton(MouseEvent) */ public boolean isMiddleMouseButton(MouseEvent e) { return (e.getModifiersEx() & MouseEvent.BUTTON2_DOWN_MASK) != 0; } /** * Returns the value of the "awt.multiClickInterval" desktop property that AWT/Swing uses internally * for generating the {@link MouseEvent#getClickCount() click count} returned by MouseListener * mouse events. If the property is not set, {@link #DEFAULT_MULTICLICK_INTERVAL} is returned. * @see MouseEvent#getClickCount() * @see java.awt.Toolkit#getDesktopProperty(String) * @return the value of the "awt.multiClickInterval" desktop property that AWT/Swing uses internally * for generating the {@link MouseEvent#getClickCount() click count} returned by MouseListener * mouse events */ public int getMultiClickInterval() { return multiClickInterval; } /** * Returns /bin/sh -l -c". * @return /bin/sh -l -c". */ public String getDefaultShell() { return getDefaultShellPath() + " -l -c"; } private String getDefaultShellPath() { if (OsFamily.WINDOWS.isCurrent()) { return "cmd.exe"; } if (defaultShell == null) { if (new File("/bin/zsh").exists()) { defaultShell = "/bin/zsh"; } else if (new File("/bin/sh").exists()) { defaultShell = "/bin/sh"; } else if (new File("/bin/bash").exists()) { defaultShell = "/bin/bash"; } else { defaultShell = "/bin/sh"; } } return defaultShell; } public String getDefaultTerminalShellCommand() { String path = getDefaultShellPath(); if (!OsFamily.WINDOWS.isCurrent()) { return path + " --login"; } return path; } /** * Always returns false. * @return false, always. */ public boolean isApplication(AbstractFile file) { return false; } @Override public String getDefaultTerminalAppCommand() { return switch (OsFamily.getCurrent()) { case WINDOWS -> "cmd /c start cmd.exe /K \"cd /d $p\""; case LINUX -> "gnome-terminal --working-directory=$p"; case MAC_OS_X -> "open -a Terminal ."; default -> ""; }; } } ================================================ FILE: src/main/java/com/mucommander/desktop/DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.util.Pair; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.notifier.AbstractNotifier; import javax.swing.*; import java.awt.*; import java.awt.event.MouseEvent; import java.util.Collections; import java.util.List; import java.util.function.Consumer; /** * Contract for classes that provide desktop integration features. *

    * There are two main steps to writing a desktop adapter: *

      *
    • Desktop detection
    • *
    • Desktop initialization
    • *
    * *

    Desktop detection

    *

    * This is achieved through the {@link #isAvailable()} method. While it has a fairly * simple contract, this method can prove quite difficult to implement properly.
    * The com.mucommander.commons.runtime package provides helpfull classes for this, * but application developers might end up having to try to init commands to see if they work * (this can be done through the {@link com.mucommander.process.ProcessRunner} class), query * environment variables, ... * *

    Desktop initialization

    *

    * This is achieved through the {@link #init(boolean)} method. Application developers are * expected to register all of their desktop specific tools there: {@link DesktopOperation desktop operations}, * {@link com.mucommander.command.Command commands}, * {@link com.mucommander.command.CommandManager#registerDefaultAssociation(String, FileFilter) associations}...
    * * @author Nicolas Rinaudo, Maxence Bernard */ public interface DesktopAdapter { /** * Checks whether the desktop is available on the current platform. * @return true if the desktop is available on the current platform, false otherwise. */ boolean isAvailable(); /** * Initializes this desktop. *

    * This method is called when an instance of DesktopAdapter has been chosen as the * best fit for the current system.
    * This gives the instance an opportunity to set itself up - * default {@link com.mucommander.command.Command} and {@link TcAction} registration, * trash management... *

    * If the install parameter is set to true, this is the first time the * application has been started. The desktop instance should use this opportunity to install platform * dependant things such as {@link com.mucommander.bookmark.Bookmark} or {@link com.mucommander.ui.action.ActionKeymap}. * * @param install true if this is the application's first boot, false otherwise. * @throws DesktopInitializationException if any error occurs. */ void init(boolean install) throws DesktopInitializationException; // - Mouse management ------------------------------------------------ // ------------------------------------------------------------------- /** * Checks whether the specified MouseEvent is a left-click for this desktop. *

    * There are some cases where Java doesn't detect mouse events properly - for example, * CONTROL + LEFT CLICK is a RIGHT CLICK under Mac OS X.
    * The goal of this method is to allow desktop to check for such non-standard behaviors. * * @param e event to check. * @return true if the specified event is a left-click for this desktop, false otherwise. * @see #isRightMouseButton(MouseEvent) * @see #isMiddleMouseButton(MouseEvent) */ boolean isLeftMouseButton(MouseEvent e); /** * Checks whether the specified MouseEvent is a left-click for this desktop. *

    * There are some cases where Java doesn't detect mouse events properly - for example, * CONTROL + LEFT CLICK is a RIGHT CLICK under Mac OS X.
    * The goal of this method is to allow desktop to check for such non-standard behaviors. * * @param e event to check. * @return true if the specified event is a left-click for this desktop, false otherwise. * @see #isMiddleMouseButton(MouseEvent) * @see #isLeftMouseButton(MouseEvent) */ boolean isRightMouseButton(MouseEvent e); /** * Checks whether the specified MouseEvent is a left-click for this desktop. *

    * There are some cases where Java doesn't detect mouse events properly - for example, * CONTROL + LEFT CLICK is a RIGHT CLICK under Mac OS X.
    * The goal of this method is to allow desktop to check for such non-standard behaviors. * * @param e event to check. * @return true if the specified event is a left-click for this desktop, false otherwise. * @see #isRightMouseButton(MouseEvent) * @see #isLeftMouseButton(MouseEvent) */ boolean isMiddleMouseButton(MouseEvent e); /** * Returns the maximum interval in milliseconds between mouse clicks for them to be considered as 'multi-clicks' * (e.g. double-clicks). The returned value should reflects the desktop's multi-click (or double-click) interval, * which may or may not correspond to the one Java uses for double-clicks. * @return the maximum interval in milliseconds between mouse clicks for them to be considered as 'multi-clicks'. */ int getMultiClickInterval(); // - Misc. ----------------------------------------------------------- // ------------------------------------------------------------------- /** * Returns the command used to start shell processes. *

    * The returned command must set the shell in its 'init script' mode. * For example, for bash, the returned command should be /bin/bash -l -c". * @return the command used to start shell processes. */ String getDefaultShell(); String getDefaultTerminalShellCommand(); /** * Returns true if the given file is an application file. What an application file actually is * is system-dependent and can take various forms. * It can be a simple executable file, as in the case of Windows .exe files, or a directory * containing an executable and various meta-information files, like Mac OS X's .app files. * * @param file the file to test * @return true if the given file is an application file */ boolean isApplication(AbstractFile file); String getDefaultTerminalAppCommand(); default TrashProvider getTrash() { return null; } default AbstractNotifier getNotifier() { return null; } default Consumer getTabbedPaneCustomizer() { return null; } default void postCopy(AbstractFile source, AbstractFile target) { } default void customizeMainFrame(Window window) { } default List> getExtendedFileProperties(AbstractFile file) { return Collections.emptyList(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/DesktopInitializationException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; /** * Encapsulates errors that occur at {@link DesktopAdapter} initialization time. *

    * This class can contain basic error information from either the com.mucommander.desktop API * or the application. Application writers can subclass it to provide additional functionality. *

    * If the application needs to pass through other types of exceptions, it must wrap them in a * DesktopInitializationException or an exception derived from it. * * @author Nicolas Rinaudo */ public class DesktopInitializationException extends Exception { /** * Creates a new desktop initialization exception. * @param message the error message. */ public DesktopInitializationException(String message) {super(message);} /** * Creates a new desktop initialization exception wrapping an existing exception. *

    * The existing exception will be embedded in the new one, and its message will * become the default message for the DesktopInitializationException. * * @param cause the exception to be wrapped in a DesktopInitializationException. */ public DesktopInitializationException(Throwable cause) {super(cause);} /** * Creates a new desktop initialization exception from an existing exception. *

    * The existing exception will be embedded in the new one, but the new exception will have its own message. * * @param message the detail message. * @param cause the exception to be wrapped in a DesktopInitializationException. */ public DesktopInitializationException(String message, Throwable cause) {super(message, cause);} } ================================================ FILE: src/main/java/com/mucommander/desktop/DesktopManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import java.awt.*; import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.*; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.util.Pair; import com.mucommander.ui.notifier.AbstractNotifier; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.desktop.gnome.ConfiguredGnomeDesktopAdapter; import com.mucommander.desktop.gnome.GuessedGnomeDesktopAdapter; import com.mucommander.desktop.kde.ConfiguredKde3DesktopAdapter; import com.mucommander.desktop.kde.ConfiguredKde4DesktopAdapter; import com.mucommander.desktop.kde.GuessedKde3DesktopAdapter; import com.mucommander.desktop.kde.GuessedKde4DesktopAdapter; import com.mucommander.desktop.openvms.OpenVMSDesktopAdapter; import com.mucommander.desktop.macos.OSXDesktopAdapter; import com.mucommander.desktop.windows.Win9xDesktopAdapter; import com.mucommander.desktop.windows.WinNtDesktopAdapter; import com.mucommander.desktop.xfce.GuessedXfceDesktopAdapter; import javax.swing.*; /** * @author Nicolas Rinaudo */ @Slf4j public class DesktopManager { /** * Represents "browse" operations. *

    * These operations are used to open URL in a browser. * * @see #canBrowse() * @see #browse(URL) */ public static final String BROWSE = "browse"; /** * Represents "file manager" operations. *

    * These operations are used to reveal local files in a file manager. * * @see #canOpenInFileManager() * @see #openInFileManager(File) */ private static final String OPEN_IN_FILE_MANAGER = "openFM"; /** * Represents "open" operations. *

    * These operations are used to open local files. * * @see #canOpen() * @see #open(File) */ static final String OPEN = "open"; /** * Represents system operations. *

    * These operations are for internal use only, and cannot be registered through the * {@link #registerOperation(String,int,DesktopOperation)} method. * */ private static final int SYSTEM_OPERATION = 0; /** * Non-system operations. *

    * These operations are treated with a lower priority than {@link #SYSTEM_OPERATION} ones, but higher * than {@link #FALLBACK_OPERATION} ones. They are meant for plugin specific operations and are expected * to be fairly reliable. * */ private static final int CUSTOM_OPERATION = 1; /** * Last resort operations. *

    * These operations will only ever be used if nothing else is available. They need only be a best-effort solution. * */ private static final int FALLBACK_OPERATION = 2; /** All available desktop operations. */ private static final List>> operations = new ArrayList<>(); /** All known desktops. */ private static final List desktops = new CopyOnWriteArrayList<>(); /** Current desktop. */ private static DesktopAdapter desktop; /** Object used to create instances of {@link AbstractTrash}. */ private static TrashProvider trashProvider; /** AbstractNotifier instance, null if none is available on the current platform */ private static AbstractNotifier notifier; private static Consumer tabbedPaneCustomizer; /** * Prevents instantiation of the class. */ private DesktopManager() {} /* * Static initialization. * Bear in mind that adapters and operations are registered the 'wrong' way around: * the earlier they are registered, the lower their priority. */ static { for (int i = 0; i < 3; i++) { operations.add(new ConcurrentHashMap<>()); } // The default desktop adapter must be registered first, as we only want to use it if nothing else worked. registerAdapter(new DefaultDesktopAdapter()); // Unix desktops: // - check for Gnome before KDE, as it seems to be more popular. // - check for 'configured' before 'guessed', as guesses are less reliable and more expensive. if (OsFamily.getCurrent() == OsFamily.LINUX) { registerAdapter(new GuessedXfceDesktopAdapter()); registerAdapter(new GuessedKde3DesktopAdapter()); registerAdapter(new GuessedKde4DesktopAdapter()); registerAdapter(new GuessedGnomeDesktopAdapter()); registerAdapter(new ConfiguredKde3DesktopAdapter()); registerAdapter(new ConfiguredKde4DesktopAdapter()); registerAdapter(new ConfiguredGnomeDesktopAdapter()); } // Known OS adapters. registerAdapter(new OpenVMSDesktopAdapter()); if (OsFamily.getCurrent() == OsFamily.WINDOWS) { registerAdapter(new OSXDesktopAdapter()); registerAdapter(new Win9xDesktopAdapter()); registerAdapter(new WinNtDesktopAdapter()); } // Having 1.6 specific operations registered as the lowest priority system // ones ensures that: // - they are executed after CommandXXX operations (important, since CommandXXX // operations are user configurable). // - they are executed before any other operations (if available, they will // provide safer and better integration than any other operation). innerRegisterOperation(OPEN, SYSTEM_OPERATION, new InternalOpen()); innerRegisterOperation(BROWSE, SYSTEM_OPERATION, new InternalBrowse()); // Registers CommandXXX operations. innerRegisterOperation(BROWSE, SYSTEM_OPERATION, new CommandBrowse()); innerRegisterOperation(OPEN_IN_FILE_MANAGER, SYSTEM_OPERATION, new CommandOpenInFileManager()); innerRegisterOperation(OPEN, SYSTEM_OPERATION, new CommandOpen(false)); // The only FALLBACK operation we have at the time of writing is for OPEN, // where we can try to init the file as if it was an executable. innerRegisterOperation(OPEN, FALLBACK_OPERATION, new CommandOpen(true)); } /** * Initializes desktop management. *

    * If install is set to true, this method might result in installing desktop specific * data such as bookmarks, keyboard shortcuts... * * @param install whether to install desktop specific information. * @throws DesktopInitializationException if an error occurred while initializing desktops. */ public static void init(boolean install) throws DesktopInitializationException { // Browses desktop from the last registered to the first, to make sure that // custom desktop adapters are used before the default ones. for (int i = desktops.size() - 1; i >= 0; i--) { DesktopAdapter current = desktops.get(i); if (current.isAvailable()) { desktop = current; log.debug("Using desktop: {}", desktop); desktop.init(install); setTrashProvider(desktop.getTrash()); setNotifier(desktop.getNotifier()); setTabbedPaneCustomizer(desktop.getTabbedPaneCustomizer()); return; } } } /** * Makes sure that we have a {@link DesktopAdapter} to work with. *

    * If the {@link #init(boolean)} method wasn't called, the DesktopManager * will find itself in a situation where it doesn't know which desktop it's running on. * Calling this method ensures that, if a {@link DesktopAdapter} instance is required, * we have at least the {@link DefaultDesktopAdapter} to work with. */ private static void checkInit() { if (desktop == null) { desktop = new DefaultDesktopAdapter(); } } /** * Registers the specified {@link DesktopAdapter}. *

    * Note that the later an adapter is registered, the higher its priority. Since all * default adapters are registered at initialization time, any call to this method * will result in the new adapter to be checked before them. * * @param adapter desktop adapter to register. */ public static void registerAdapter(DesktopAdapter adapter) { desktops.add(adapter); } /** * Registers the specified operation for the specified type and priority. */ private static void innerRegisterOperation(String type, int priority, DesktopOperation operation) { // Makes sure we have a container for operations of the specified priority. if (operations.get(priority) == null) { operations.set(priority, new ConcurrentHashMap<>()); } // Makes sure we have a container for operations of the specified type. List container = operations.get(priority).computeIfAbsent(type, k -> new CopyOnWriteArrayList<>()); // Creates the requested entry. container.add(operation); } public static void registerOperation(String type, int priority, DesktopOperation operation) { if (priority != FALLBACK_OPERATION && priority != CUSTOM_OPERATION) { throw new IllegalArgumentException(); } innerRegisterOperation(type, priority, operation); } private static List getOperations(String type, int priority) { if (operations.get(priority) == null) { return null; } return operations.get(priority).get(type); } private static DesktopOperation getAvailableOperation(String type, int priority) { List container = getOperations(type, priority); // If the operation vector is null, no need to look further. if (container != null) { for (int i = container.size() - 1; i >= 0; i--) { DesktopOperation operation = container.get(i); if (operation.isAvailable()) { return operation; } } } return null; } private static DesktopOperation getSupportedOperation(String type, int priority, Object[] target) { List container = getOperations(type, priority); // If the operation vector is null, no need to look further. if (container != null) for (int i = container.size() - 1; i >= 0; i--) { DesktopOperation operation = container.get(i); if (operation.canExecute(target)) { return operation; } } return null; } private static DesktopOperation getSupportedOperation(String type, Object[] target) { DesktopOperation operation = getSupportedOperation(type, SYSTEM_OPERATION, target); if (operation != null) { return operation; } operation = getSupportedOperation(type, CUSTOM_OPERATION, target); if (operation != null) { return operation; } operation = getSupportedOperation(type, FALLBACK_OPERATION, target); return operation; } private static DesktopOperation getAvailableOperation(String type) { DesktopOperation systemOperation = getAvailableOperation(type, SYSTEM_OPERATION); if (systemOperation != null) { return systemOperation; } DesktopOperation customOperation = getAvailableOperation(type, CUSTOM_OPERATION); if (customOperation != null) { return customOperation; } return getAvailableOperation(type, FALLBACK_OPERATION); } public static boolean isOperationAvailable(String type) { return getAvailableOperation(type) != null; } public static boolean isOperationSupported(String type, Object[] target) { return getSupportedOperation(type, target) != null; } public static void executeOperation(String type, Object[] target) throws IOException, UnsupportedOperationException { DesktopOperation operation = getSupportedOperation(type, target); if (operation == null) { log.info("Unsupported operation {} {}", type, target); throw new UnsupportedOperationException(); } operation.execute(target); } public static String getOperationName(String type) throws UnsupportedOperationException { DesktopOperation operation = getAvailableOperation(type); if (operation == null) { throw new UnsupportedOperationException(); } return operation.getName(); } public static String getOperationName(String type, Object[] target) throws UnsupportedOperationException { DesktopOperation operation = getSupportedOperation(type, target); if (operation == null) { throw new UnsupportedOperationException(); } return operation.getName(); } public static boolean canBrowse() { return isOperationAvailable(BROWSE); } public static boolean canBrowse(URL url) { return isOperationSupported(BROWSE, new Object[] {url}); } public static boolean canBrowse(String url) { return isOperationSupported(BROWSE, new Object[] {url}); } public static boolean canBrowse(AbstractFile url) { return isOperationSupported(BROWSE, new Object[] {url}); } public static void browse(URL url) throws IOException, UnsupportedOperationException { executeOperation(BROWSE, new Object[] {url}); } public static void browse(String url) throws IOException, UnsupportedOperationException { executeOperation(BROWSE, new Object[] {url}); } public static void browse(AbstractFile url) throws IOException, UnsupportedOperationException { executeOperation(BROWSE, new Object[] {url}); } public static boolean canOpen() { return isOperationAvailable(OPEN); } public static boolean canOpen(File file) { return isOperationSupported(OPEN, new Object[] {file}); } public static boolean canOpen(String file) { return isOperationSupported(OPEN, new Object[] {file}); } public static boolean canOpen(AbstractFile file) { return isOperationSupported(OPEN, new Object[] {file}); } public static void open(File file) throws IOException, UnsupportedOperationException { executeOperation(OPEN, new Object[] {file}); } public static void open(String file) throws IOException, UnsupportedOperationException { executeOperation(OPEN, new Object[] {file}); } public static void open(AbstractFile file) throws IOException, UnsupportedOperationException { if (file.isDirectory()) { executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}); } else { executeOperation(OPEN, new Object[]{file}); } } public static boolean canOpenInFileManager() { return isOperationAvailable(OPEN_IN_FILE_MANAGER); } public static boolean canOpenInFileManager(File file) { return isOperationSupported(OPEN_IN_FILE_MANAGER, new Object[] {file}); } public static boolean canOpenInFileManager(String file) { return isOperationSupported(OPEN_IN_FILE_MANAGER, new Object[] {file}); } public static boolean canOpenInFileManager(AbstractFile file) { return isOperationSupported(OPEN_IN_FILE_MANAGER, new Object[] {file}); } public static void openInFileManager(File file) throws IOException, UnsupportedOperationException { executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}); } public static void openInFileManager(String file) throws IOException, UnsupportedOperationException { executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}); } public static void openInFileManager(AbstractFile file) throws IOException, UnsupportedOperationException { executeOperation(OPEN_IN_FILE_MANAGER, new Object[] {file}); } private static String getFileManagerName(DesktopOperation operation) throws UnsupportedOperationException { if (operation == null) { throw new UnsupportedOperationException(); } return operation.getName(); } public static String getFileManagerName() throws UnsupportedOperationException { return getFileManagerName(getAvailableOperation(OPEN_IN_FILE_MANAGER)); } public static String getFileManagerName(File file) throws UnsupportedOperationException { return getFileManagerName(getSupportedOperation(OPEN_IN_FILE_MANAGER, new Object[] {file})); } public static String getFileManagerName(String file) throws UnsupportedOperationException { return getFileManagerName(getSupportedOperation(OPEN_IN_FILE_MANAGER, new Object[] {file})); } public static String getFileManagerName(AbstractFile file) throws UnsupportedOperationException { return getFileManagerName(getSupportedOperation(OPEN_IN_FILE_MANAGER, new Object[] {file})); } /** * Returns an instance of the {@link com.mucommander.desktop.AbstractTrash} implementation that can be used on the current platform. * @return an instance of the AbstractTrash implementation that can be used on the current platform, or null if none is available. */ public static AbstractTrash getTrash() { TrashProvider provider = getTrashProvider(); return provider == null ? null : provider.getTrash(); } /** * Returns the object used to create instances of {@link com.mucommander.desktop.AbstractTrash}. * @return the object used to create instances of {@link AbstractTrash} if any, null otherwise. */ public static TrashProvider getTrashProvider() { return trashProvider; } /** * Sets the object that is used to create instances of {@link com.mucommander.desktop.AbstractTrash}. * @param provider object that will be used to create instances of {@link com.mucommander.desktop.AbstractTrash}. */ public static void setTrashProvider(TrashProvider provider) { trashProvider = provider; } private static void setTabbedPaneCustomizer(Consumer tabbedPaneCustomizer) { DesktopManager.tabbedPaneCustomizer = tabbedPaneCustomizer; } private static void setNotifier(AbstractNotifier notifier) { DesktopManager.notifier = notifier; } public static AbstractNotifier getNotifier() { return notifier; } /** * Checks whether the specified MouseEvent is a left-click for this desktop. *

    * There are some cases where Java doesn't detect mouse events properly - for example, * CONTROL + LEFT CLICK is a RIGHT CLICK under Mac OS X.
    * The goal of this method is to allow desktop to check for such non-standard behaviors. * * @param e event to check. * @return true if the specified event is a left-click for this desktop, false otherwise. * @see #isRightMouseButton(MouseEvent) * @see #isMiddleMouseButton(MouseEvent) */ public static boolean isLeftMouseButton(MouseEvent e) { checkInit(); return desktop.isLeftMouseButton(e); } /** * Checks whether the specified MouseEvent is a left-click for this desktop. *

    * There are some cases where Java doesn't detect mouse events properly - for example, * CONTROL + LEFT CLICK is a RIGHT CLICK under Mac OS X.
    * The goal of this method is to allow desktop to check for such non-standard behaviors. * * @param e event to check. * @return true if the specified event is a left-click for this desktop, false otherwise. * @see #isMiddleMouseButton(MouseEvent) * @see #isLeftMouseButton(MouseEvent) */ public static boolean isRightMouseButton(MouseEvent e) { checkInit(); return desktop.isRightMouseButton(e); } /** * Checks whether the specified MouseEvent is a left-click for this desktop. *

    * There are some cases where Java doesn't detect mouse events properly - for example, * CONTROL + LEFT CLICK is a RIGHT CLICK under Mac OS X.
    * The goal of this method is to allow desktop to check for such non-standard behaviors. * * @param e event to check. * @return true if the specified event is a left-click for this desktop, false otherwise. * @see #isRightMouseButton(MouseEvent) * @see #isLeftMouseButton(MouseEvent) */ public static boolean isMiddleMouseButton(MouseEvent e) { checkInit(); return desktop.isMiddleMouseButton(e); } /** * Returns the maximum interval in milliseconds between mouse clicks for them to be considered as 'multi-clicks' * (e.g. double-clicks). The returned value should reflects the desktop's multi-click (or double-click) interval, * which may or may not correspond to the one Java uses for double-clicks. * @return the maximum interval in milliseconds between mouse clicks for them to be considered as 'multi-clicks'. */ public static int getMultiClickInterval() { checkInit(); return desktop.getMultiClickInterval(); } /** * Returns the command used to start shell processes. *

    * The returned command must set the shell in its 'init script' mode. * For example, for bash, the returned command should be /bin/bash -l -c". * * @return the command used to start shell processes. */ public static String getDefaultShell() { checkInit(); return desktop.getDefaultShell(); } public static String getDefaultTerminalShellCommand() { checkInit(); return desktop.getDefaultTerminalShellCommand(); } public static String getDefaultTerminalAppCommand() { checkInit(); return desktop.getDefaultTerminalAppCommand(); } /** * Returns true if the given file is an application file. What an application file actually is * is system-dependent and can take various forms. * It can be a simple executable file, as in the case of Windows .exe files, or a directory * containing an executable and various meta-information files, like Mac OS X's .app files. * * @param file the file to test * @return true if the given file is an application file */ public static boolean isApplication(AbstractFile file) { return desktop.isApplication(file); } public static void customizeTabbedPaneUI(JTabbedPane tabbedPane) { if (tabbedPaneCustomizer != null) tabbedPaneCustomizer.accept(tabbedPane); } public static void postCopy(AbstractFile source, AbstractFile target) { desktop.postCopy(source, target); } public static void customizeMainFrame(Window window) { desktop.customizeMainFrame(window); } public static List> getExtendedFileProperties(AbstractFile file) { return desktop.getExtendedFileProperties(file); } } ================================================ FILE: src/main/java/com/mucommander/desktop/DesktopOperation.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import java.io.IOException; /** * Contract for basic desktop operations. *

    * A desktop operation is an system dependent operation tied to the desktop, such as opening a file * or launching a web browser on a specified URL. *

    * They are meant to be as extensible as possible. This, however, comes with a cost: * they can prove to be quite complex to understand and use. Before writing an implementation of * this interface, application developers should make sure they understand the following points: *

      *
    • Generic execution
    • *
    • Available Vs Supported
    • *
    *

    Generic execution

    *

    * Desktop operations receive an Object[] as parameter to their {@link #execute(Object[])} method. * This allows the API to be rather flexible, if a bit more obscure.
    * The basic unwritten contract that all operations must respect is that, for a given operation type, the same * parameters classes must be accepted. There is no way to enforce that and keep the flexibility, which means that the * responsibility for this lies on the application developer. * *

    * At the time of writing, the API uses two different types of operations: {@link DesktopManager#BROWSE URL browsing} * and {@link DesktopManager#OPEN local file opening}. * Adapters have been provided for these: {@link UrlOperation} and {@link LocalFileOperation}. * *

    Available Vs Supported

    *

    * An operation is said to available if it will accept any parameter that matches its contract. For example, * an operation that works on local files will be available if it accepts any java.io.File, String * or {@link com.mucommander.commons.file.impl.local.LocalFile} parameter. * *

    * An operation is said to be supported for a specific parameter subset it will accept any parameter that * match that subset. *

    * An available operation is always supported. However, it's entirely possible for an operation to be supported * for some parameters but not others, and thus not to be available.
    * For example, an operation that deals with XML files only will be supported for any parameter that describes a local XML file, * but won't be available as it will refuse plain text files. * * @author Nicolas Rinaudo */ public interface DesktopOperation { /** * Returns the operation's name. *

    * The returned value might be displayed to the user. It should thus be made as human-readable as possible * and, if possible, localised. * * @return the operation's name. */ String getName(); /** * Checks whether the operation is available. *

    * An operation is said to be available if and only if any call to * {@link #canExecute(Object[])} with parameters that match its constraints * will return true. *

    * For example, an operation of type {@link DesktopManager#BROWSE} that accepts * any and all HTTP URLs is available. However, an operation of type * {@link DesktopManager#OPEN} that only accepts XML files isn't. * * @return true if the operation is available, false otherwise. * @see #canExecute(Object[]) */ boolean isAvailable(); /** * Checks whether an operation is supported for the specified parameters. *

    * If the operation is {@link #isAvailable() available}, then this method must always * return true. *

    * If the operation isn't available, but the specified parameters match a subset of legal * parameters that it knows how to deal with, this method must return true. *

    * In any other case, this method must return false. *

    * For example, a {@link DesktopManager#OPEN} operation that only accept XML files will return: *

      *
    • * true if the parameter array contains a single instance of either java.io.File, * String or {@link com.mucommander.commons.file.impl.local.LocalFile} and that instance describes the * path to a valid XML file. *
    • *
    • * false if the specified parameters are not valid or if they do not describe the path to * a valid XML file. *
    • *
    * @param target parameters to check. * @return true if the operation can be executed with the specified parameters, false otherwise. */ boolean canExecute(Object[] target); /** * Executes the operation on the specified parameters. *

    * There is no guarantee that this method is available for the specified parameters. This must be checked * through {@link #canExecute(Object[])}. * * @param target parameters on which to execute the operation. * @throws IOException if an error occurs. * @throws UnsupportedOperationException if the operation is not supported for the specified parameters. */ void execute(Object[] target) throws IOException, UnsupportedOperationException; } ================================================ FILE: src/main/java/com/mucommander/desktop/InternalBrowse.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import java.awt.*; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; /** * @author Nicolas Rinaudo */ class InternalBrowse extends UrlOperation { /** Underlying desktop instance. */ private Desktop desktop; private boolean initialized = false; /** * Creates a new InternalOpenUrl instance. */ InternalBrowse() { } private Desktop getDesktop() { if (!initialized) { if (Desktop.isDesktopSupported()) { desktop = Desktop.getDesktop(); } initialized = true; } return desktop; } /** * Returns true if this operation is available. *

    * This operation is available if: *

      *
    • Desktops are supported by the current system (Desktop.isDesktopSupported() returns true).
    • *
    • Browsing is supported by the desktop (Desktop.isSupported(Desktop.Action.BROWSE) returns true).
    • *
    * * @return true if this operations is available, false otherwise. */ @Override public boolean isAvailable() { return getDesktop() != null && getDesktop().isSupported(Desktop.Action.BROWSE); } /** * Opens the specified URL in the system's default browser. * @param url URL to browse. * @throws IOException if an error occured. */ @Override public void execute(URL url) throws IOException { // If java.awt.Desktop browsing is available, use it. if (isAvailable()) { try { getDesktop().browse(url.toURI()); } catch(URISyntaxException e) { throw new IOException(e.getMessage()); } } throw new UnsupportedOperationException(); } /** * Returns the action's label. * @return the action's label. */ @Override public String getName() { return "java.awt.Desktop open URL"; } } ================================================ FILE: src/main/java/com/mucommander/desktop/InternalOpen.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.commons.file.AbstractFile; import java.awt.*; import java.io.File; import java.io.IOException; /** * @author Nicolas Rinaudo */ class InternalOpen extends LocalFileOperation { /** Underlying desktop instance. */ private Desktop desktop; private boolean initialized = false; /** * Creates a new InternalOpen instance. */ InternalOpen() { } private Desktop getDesktop() { if (!initialized) { if (Desktop.isDesktopSupported()) { desktop = Desktop.getDesktop(); } initialized = true; } return desktop; } /** * Returns true if this operation is available. *

    * This operation is available if: *

      *
    • Desktops are supported by the current system (Desktop.isDesktopSupported() returns true).
    • *
    • File opening is supported by the desktop (Desktop.isSupported(Desktop.Action.OPEN) returns true).
    • *
    * @return true if this operations is available, false otherwise. */ @Override public boolean isAvailable() {return getDesktop() != null && getDesktop().isSupported(Desktop.Action.OPEN);} @Override public void execute(AbstractFile file) throws IOException { if (isAvailable()) { getDesktop().open(new File(file.getAbsolutePath())); } else { throw new UnsupportedOperationException(); } } /** * @return the action's label. */ @Override public String getName() { return "java.awt.Desktop open file"; } } ================================================ FILE: src/main/java/com/mucommander/desktop/LocalFileOperation.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.impl.local.SpecialWindowsLocation; import java.io.File; import java.io.IOException; /** * {@link DesktopOperation} implementation meant for actions that involve local files. *

    * Instead of having to deal with the {@link DesktopOperation#canExecute(Object[])} * and {@link DesktopOperation#execute(Object[])}, instances of LocalFileOperation * can use {@link #canExecute(AbstractFile)} and {@link #execute(AbstractFile)} and ignore the complexity of * the desktop API's genericity. * * @author Nicolas Rinaudo */ public abstract class LocalFileOperation implements DesktopOperation { public abstract String getName(); public abstract boolean isAvailable(); /** * Executes the operation on the specified file. * @param file file on which to execute the operation. * @throws IOException if an error occurs. * @throws UnsupportedOperationException if the operation is not supported. */ public abstract void execute(AbstractFile file) throws IOException, UnsupportedOperationException; /** * Checks whether the operation knows how to deal with the specified file. *

    * By default, this method returns {@link #isAvailable()}. However, some implementations * might want to overwrite it. For example, a LocalFileOperation that only works * on XML files would override this method to only return true if the specified * file is an XML one. * * @param file file to check against. * @return true if the operation is supported for the specified file, false otherwise. */ public boolean canExecute(AbstractFile file) { return isAvailable(); } /** * Returns true if the operation is supported for the specified parameters. *

    * By default, this method will call {@link #extractTarget(Object[])} on the specified parameters * and pass the resulting {@link AbstractFile} instance to {@link #canExecute(AbstractFile)}. *

    * This behavior can be overridden by implementations, although most cases can be handled through * {@link #canExecute(AbstractFile)} instead. * * @param target operation parameters. * @return true if the operation is supported for the specified parameters, false otherwise. * @see #canExecute(AbstractFile) * @see #extractTarget(Object[]) */ public boolean canExecute(Object[] target) { AbstractFile file = extractTarget(target); return file != null && canExecute(file); } /** * Analyses the specified parameters and delegates the operation execution to {@link #execute(AbstractFile)}. *

    * This method is a wrapper for {@link #extractTarget(Object[])} and {@link #execute(AbstractFile)}. Most * implementations should ignore it. * * @param target parameters of the operation. * @throws IOException if an error occurs. * @throws UnsupportedOperationException if the operation is not supported. * @see #execute(AbstractFile) * @see #extractTarget(Object[]) */ public void execute(Object[] target) throws IOException, UnsupportedOperationException { AbstractFile file = extractTarget(target); // Makes sure we received the right kind of parameters. if (file == null) { throw new UnsupportedOperationException(); } // Execute the operation. execute(file); } /** * Analyses the specified parameters and returns them in a form that can be used. *

    * By default, this method will return null unless target: *

      *
    • has a length of 1.
    • *
    • * contains an instance of either java.io.File, {@link com.mucommander.commons.file.impl.local.LocalFile}, String * or {@link com.mucommander.commons.file.impl.local.SpecialWindowsLocation}. *
    • *
    *

    * This behavior can be overridden by implementations to fit their own needs, although it's probably not a great idea. * * @param target operation parameters. * @return null if the parameters are not legal, a {@link com.mucommander.commons.file.AbstractFile} instance instead. */ protected AbstractFile extractTarget(Object[] target) { // We only deal with arrays containing 1 element. if (target.length != 1) { return null; } // If we find an instance of java.io.File, we can stop here. if (target[0] instanceof File) { return FileFactory.getFile(((File) target[0]).getAbsolutePath()); } if (target[0] instanceof SpecialWindowsLocation) { return (AbstractFile) target[0]; } // Deals with instances of LocalFile: raw instances or wrapped in another AbstractFile container (e.g. archive files) if (target[0] instanceof AbstractFile && ((AbstractFile)target[0]).hasAncestor(LocalFile.class)) { return (AbstractFile) target[0]; } // Deals with instances of String. if (target[0] instanceof String) { return FileFactory.getFile((String) target[0]); } // Illegal parameters. return null; } } ================================================ FILE: src/main/java/com/mucommander/desktop/QueuedTrash.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.commons.file.AbstractFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.main.WindowManager; import java.util.ArrayList; import java.util.List; /** * QueuedTrash is an {@link AbstractTrash} which moves files to the trash asynchroneously. * *

    * When {@link #moveToTrash(com.mucommander.commons.file.AbstractFile)} is called, the file is added to a queue. * The file is not moved to the trash immediately: the trash will wait a period of {@link #QUEUE_PERIOD} milliseconds * for additional files to be added. If files were added during that period, the trash will wait another period and * so on. When no more files are added were added during the period, {@link #moveToTrash(java.util.List)} is called * with the list of queued files to move to the trash. * *

    * This mechanism allows to group calls to the underlying trash. It is effective when the atomic operation * of moving a file to the trash has a high cost and {@link #moveToTrash(com.mucommander.commons.file.AbstractFile)} is called * repeatedly. One thing to note is since the move is performed asynchroneously, * {@link #moveToTrash(com.mucommander.commons.file.AbstractFile)} returns immediately without waiting for the file to be moved, * {@link #waitForPendingOperations()} can be used to wait for the files to have effectively been moved. * * @author Maxence Bernard */ public abstract class QueuedTrash extends AbstractTrash { /** Contains the files that are waiting to be moved to the trash */ private final static List queuedFiles = new ArrayList<>(); /** Use to synchronize access to the trash */ private final static Object moveToTrashLock = new Object(); /** Thread that performs the actual job of moving files to the trash */ private static Thread moveToTrashThread; /** Amount of time in milliseconds to wait for additional files before moving them to the trash */ protected final static int QUEUE_PERIOD = 1000; /** * Moves the {@link AbstractFile} instances contained in the given Vector to the trash. * Returns true if all files were moved successfully. * * @param queuedFiles a Vector of AbstractFile to move to the trash * @return true if all files were moved successfully */ protected abstract boolean moveToTrash(List queuedFiles); ////////////////////////////////// // AbstractTrash implementation // ////////////////////////////////// /** * Implementation notes: this method adds the given file to the queue of files to be moved to the trash and returns * immediately, i.e. without waiting for the file to be moved. The specified file will only be added to the queue if * {@link #canMoveToTrash(com.mucommander.commons.file.AbstractFile)} returned true for it. * Since the actual move is performed asynchroneously, this method has no way of * knowing if the file was successfully moved to the trash. So this method will return true if the * given file has been scheduled to be moved to the trash, but it may end up failing to be moved for whatever reason. */ @Override public boolean moveToTrash(AbstractFile file) { if (!canMoveToTrash(file)) { return false; } synchronized(moveToTrashLock) { // Queue the given file queuedFiles.add(file); // create a new thread and start it if one isn't already running if (moveToTrashThread == null) { moveToTrashThread = new MoveToTrashThread(); moveToTrashThread.start(); } } return true; } @Override public void waitForPendingOperations() { synchronized(moveToTrashLock) { if (moveToTrashThread != null) { try { // Wait until moveToTrashThread wakes this thread up moveToTrashLock.wait(); } catch(InterruptedException ignore) {} } } } /////////////////// // Inner classes // /////////////////// /** * Performs the actual job of moving files to the trash. * *

    The thread starts by waiting {@link com.mucommander.desktop.macos.OSXTrash#QUEUE_PERIOD} milliseconds before moving them to give additional * files a chance to be queued and regrouped as a single call to {@link QueuedTrash#moveToTrash(java.util.List)}. * If more files were queued during that period, the thread will wait an additional {@link com.mucommander.desktop.macos.OSXTrash# QUEUE_PERIOD}, * and so on. */ private class MoveToTrashThread extends Thread { @Override public void run() { // Loops until no files were added during the sleep period int queueSize; do { queueSize = queuedFiles.size(); try { Thread.sleep(QUEUE_PERIOD); } catch(InterruptedException ignore) {} } while (queueSize != queuedFiles.size()); synchronized(moveToTrashLock) { // Files can't be added to queue while files are moved to trash if (!moveToTrash(queuedFiles)) { InformationDialog.showErrorDialog(WindowManager.getCurrentMainFrame().getJFrame(), Translator.get("delete_dialog.move_to_trash.option"), Translator.get("delete_dialog.move_to_trash.failed")); } queuedFiles.clear(); // Wake up any thread waiting for this thread to be finished moveToTrashLock.notify(); moveToTrashThread = null; } } } } ================================================ FILE: src/main/java/com/mucommander/desktop/TrashProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; /** * TrashProvider provides a way to instantiate {@link AbstractTrash} implementations. * *

    Trash providers can be registered with {@link DesktopManager#setTrashProvider(TrashProvider)} * for them to become the default trash one. * * @see com.mucommander.desktop.AbstractTrash * @see DesktopManager#setTrashProvider(TrashProvider) * @author Nicolas Rinaudo */ public interface TrashProvider { /** * Returns a trash instance. * * @return a trash instance */ AbstractTrash getTrash(); } ================================================ FILE: src/main/java/com/mucommander/desktop/UrlOperation.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.http.HTTPFile; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; /** * {@link DesktopOperation} implementation meant for actions that involve java.net.URL. *

    * Instead of having to deal with the {@link DesktopOperation#canExecute(Object[])} * and {@link DesktopOperation#execute(Object[])}, instances of LocalFileOperation * can use {@link #canExecute(URL)} and {@link #execute(URL)} and ignore the complexity of * the desktop API's genericity. * * @author Nicolas Rinaudo */ public abstract class UrlOperation implements DesktopOperation { // - DesktopOperation methods ---------------------------------------- // ------------------------------------------------------------------- public abstract String getName(); public abstract boolean isAvailable(); // - Wrappers -------------------------------------------------------- // ------------------------------------------------------------------- /** * Executes the operation on the specified URL. * @param url URL on which to execute the operation. * @throws IOException if an error occurs. * @throws UnsupportedOperationException if the operation is not supported. */ public abstract void execute(URL url) throws IOException; /** * Checks whether the operation knows how to deal with the specified URL. *

    * By default, this method returns {@link #isAvailable()}. However, some implementations * might want to overwrite it. For example, a UrlOperation that only works * on HTTPS URLs would override this method to only return true if the specified * URL is an HTTPS one. * * @param url url to check against. * @return true if the operation is supported for the specified file, false otherwise. */ public boolean canExecute(URL url) { return isAvailable(); } // - DesktopOperation implementation --------------------------------- // ------------------------------------------------------------------- /** * Returns true if the operation is supported for the specified parameters. *

    * By default, this method will call {@link #extractTarget(Object[])} on the specified parameters * and pass the resulting java.net.URL instance to {@link #canExecute(URL)}. *

    * This behavior can be overridden by implementations, although most cases can be handled through * {@link #canExecute(URL)} instead. * * @param target operation parameters. * @return true if the operation is supported for the specified parameters, false otherwise. * @see #canExecute(URL) * @see #extractTarget(Object[]) */ public boolean canExecute(Object[] target) { URL url = extractTarget(target); return url != null && canExecute(url); } /** * Analyses the specified parameters and delegates the operation execution to {@link #execute(URL)}. *

    * This method is a wrapper for {@link #extractTarget(Object[])} and {@link #execute(URL)}. Most * implementations should ignore it. * * @param target parameters of the operation. * @throws IOException if an error occurs. * @throws UnsupportedOperationException if the operation is not supported. * @see #execute(URL) * @see #extractTarget(Object[]) */ public void execute(Object[] target) throws IOException, UnsupportedOperationException { URL url = extractTarget(target); if (url == null) { throw new UnsupportedOperationException(); } execute(url); } // - Parameter analysis ---------------------------------------------- // ------------------------------------------------------------------- /** * Analyses the specified parameters and returns them in a form that can be used. *

    * By default, this method will return null unless target: *

      *
    • has a length of 1.
    • *
    • contains an instance of either java.io.File,{@link com.mucommander.commons.file.impl.local.LocalFile} or String.
    • *
    *

    * This behavior can be overridden by implementations to fit their own needs, although it's probably not a great idea. * * @param target operation parameters. * @return null if the parameters are not legal, a java.io.File instance instead. */ protected URL extractTarget(Object[] target) { // We only deal with arrays containing 1 element. if (target.length != 1) { return null; } Object obj = target[0]; // If we find an instance of java.net.URL, we can stop here. if (obj instanceof URL) { return (URL)obj; } // Deals with instances of HTTPFile. if (obj instanceof HTTPFile) { return (URL)((AbstractFile)obj).getUnderlyingFileObject(); } // Deals with instances of String. if (obj instanceof String) { try { return new URI((String)obj).toURL(); } catch(URISyntaxException | MalformedURLException e) { return null; } } // Illegal parameters. return null; } } ================================================ FILE: src/main/java/com/mucommander/desktop/gnome/ConfiguredGnomeDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.gnome; import com.mucommander.process.ProcessRunner; import lombok.extern.slf4j.Slf4j; /** * @author Nicolas Rinaudo */ @Slf4j public class ConfiguredGnomeDesktopAdapter extends GnomeDesktopAdapter { public String toString() { return "Gnome Desktop"; } @Override public boolean isAvailable() { String desktopSession = System.getenv("DESKTOP_SESSION"); if ("gnome".equalsIgnoreCase(desktopSession)) { return true; } desktopSession = System.getenv("XDG_CURRENT_DESKTOP"); if (desktopSession != null) { desktopSession = desktopSession.toLowerCase(); if (desktopSession.contains("gnome")) return true; if (desktopSession.contains("unity")) return true; } desktopSession = System.getenv("GNOME_DESKTOP_SESSION_ID"); return desktopSession != null && !desktopSession.trim().isEmpty(); } @Override protected String getFileOpenerCommand() { try { ProcessRunner.execute(GVFS_OPEN); return GVFS_OPEN; } catch(Exception ignore) { log.debug(GVFS_OPEN + " not found"); } try { ProcessRunner.execute(GNOME_OPEN); return GNOME_OPEN; } catch(Exception ignore) { log.debug(GNOME_OPEN + " not found"); } try { ProcessRunner.execute(XDG_OPEN); return XDG_OPEN; } catch(Exception ignore) { log.debug(XDG_OPEN + " not found"); } return null; } } ================================================ FILE: src/main/java/com/mucommander/desktop/gnome/GnomeConfig.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.gnome; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; /** * Provides access to the GNOME configuration, using the gconftool command. * * @author Maxence Bernard */ @Slf4j public class GnomeConfig { /** Name of the command to invoke for retrieving configuration values */ private static final String CONFIG_COMMAND = "gconftool"; /** Timeout for the configuration command execution in seconds */ private static final long COMMAND_TIMEOUT = 5; /** * Returns the GNOME configuration value corresponding to the given key, null if this key has no value. * * @param key key to the configuration value to retrieve. * @return the configuration value corresponding to the given key, null if this key has no value. * @throws IOException if an error occurred while invoking the gconftool command, for instance if the * command isn't available in the path. */ public static String getValue(String key) throws IOException { ProcessBuilder processBuilder = new ProcessBuilder(CONFIG_COMMAND, "-g", key); processBuilder.redirectErrorStream(true); try { Process process = processBuilder.start(); try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line = br.readLine(); // Wait for process completion with timeout if (!process.waitFor(COMMAND_TIMEOUT, TimeUnit.SECONDS)) { process.destroyForcibly(); log.warn("Command timed out for key: {}", key); throw new IOException("Command timed out: " + CONFIG_COMMAND); } int exitCode = process.exitValue(); if (exitCode != 0) { log.debug("Command returned exit code {} for key: {}", exitCode, key); return null; } log.debug(CONFIG_COMMAND + " returned '{}' for {}", line, key); if (line == null || (line=line.trim()).isEmpty() || line.startsWith("No value set for")) { return null; } return line; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.debug("Interrupted while retrieving value for {}", key, e); throw new IOException("Command interrupted", e); } catch(IOException e) { log.debug("Error while retrieving value for {}", key, e); throw e; } } } ================================================ FILE: src/main/java/com/mucommander/desktop/gnome/GnomeDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.gnome; import java.awt.Toolkit; import java.lang.reflect.Field; import com.mucommander.desktop.DesktopInitializationException; import lombok.extern.slf4j.Slf4j; import com.mucommander.command.Command; import com.mucommander.command.CommandException; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.RegexpFilenameFilter; import com.mucommander.desktop.DefaultDesktopAdapter; import com.mucommander.desktop.DesktopManager; /** * @author Nicolas Rinaudo, Maxence Bernard */ @Slf4j abstract class GnomeDesktopAdapter extends DefaultDesktopAdapter { private static final String FILE_MANAGER_NAME = "Nautilus"; private static final String EXE_OPENER = "$f"; protected static final String GVFS_OPEN = "gvfs-open"; protected static final String GNOME_OPEN = "gnome-open"; protected static final String XDG_OPEN = "xdg-open"; protected static final String CMD_OPENER_COMMAND = "gnome-terminal --working-directory $f"; /** Key to the double-click interval value in the GNOME configuration. */ private static final String DOUBLE_CLICK_CONFIG_KEY = "/desktop/gnome/peripherals/mouse/double_click"; /** * Multi-click interval, cached to avoid polling the value every time * {@link #getMultiClickInterval()} is called. */ private int multiClickInterval; @Override public abstract boolean isAvailable(); protected abstract String getFileOpenerCommand(); @Override public void init(final boolean install) throws DesktopInitializationException { String fileOpener = String.format("%s $f", getFileOpenerCommand()); System.out.println("fileOpener: " + fileOpener); setWMClass(); // Workaround for JDK issue try { Toolkit xToolkit = Toolkit.getDefaultToolkit(); Field awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName"); awtAppClassNameField.setAccessible(true); awtAppClassNameField.set(xToolkit, "trolCommander"); } catch (Exception ignore) { } // Initializes trash management. DesktopManager.setTrashProvider(new GnomeTrashProvider()); try { CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_OPENER_ALIAS, fileOpener, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.URL_OPENER_ALIAS, fileOpener, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.EXE_OPENER_ALIAS, EXE_OPENER, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_MANAGER_ALIAS, fileOpener, CommandType.SYSTEM_COMMAND, FILE_MANAGER_NAME, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.CMD_OPENER_ALIAS, CMD_OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); FileFilter filter = new RegexpFilenameFilter("[^.]+", true); // Disabled actual permissions checking as this will break normal +x files. // With this, a +x PDF file will not be opened. /* * // Identifies which kind of IMAGE_FILTER should be used to match executable files. * if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) IMAGE_FILTER = new * PermissionsFileFilter(PermissionTypes.EXECUTE_PERMISSION, true); else */ CommandManager.registerDefaultAssociation(CommandManager.EXE_OPENER_ALIAS, filter); // Multi-click interval retrieval multiClickInterval = determineMultiClickInterval(); } catch (CommandException e) { throw new DesktopInitializationException(e); } } private int determineMultiClickInterval() { try { String value = GnomeConfig.getValue(DOUBLE_CLICK_CONFIG_KEY); if (value != null) { return Integer.parseInt(value); } } catch (Exception e) { log.debug("Error while retrieving double-click interval from gconftool", e); } return super.getMultiClickInterval(); } /** * Returns the /desktop/gnome/peripherals/mouse/double_click GNOME configuration * value. If the returned value is not defined or could not be retrieved, the value of * {@link DefaultDesktopAdapter#getMultiClickInterval()} is returned.
    * The value is retrieved on initialization and never updated thereafter. *

    * Note under Java 1.6 or below, the returned value does not match the one used by Java for * generating multi-clicks (see {@link DefaultDesktopAdapter#getMultiClickInterval()}, as * Java uses the multi-click speed declared in X Window's configuration, not in GNOME's. See * Java Bug 5076635 * for more information. * * @return the /desktop/gnome/peripherals/mouse/double_click GNOME configuration value. */ @Override public int getMultiClickInterval() { return multiClickInterval; } @Override public String getDefaultTerminalAppCommand() { return "gnome-terminal --working-directory=$p"; } @Override public String getDefaultTerminalShellCommand() { return "/bin/bash --login"; } /** * Sets the WM_CLASS for Linux window managers. */ private static void setWMClass() { try { java.awt.Toolkit toolkit = java.awt.Toolkit.getDefaultToolkit(); java.lang.reflect.Field awtAppClassNameField = toolkit.getClass().getDeclaredField("awtAppClassName"); awtAppClassNameField.setAccessible(true); awtAppClassNameField.set(null, "trolcommander-trolCommander"); } catch (NoSuchFieldException e) { // Not running on X11/Linux, or field doesn't exist in this JDK version log.info("DEBUG: Could not set WM_CLASS - field not found (probably not Linux/X11)"); } catch (IllegalAccessException e) { log.warn("Warning: Could not set WM_CLASS due to access restrictions: {}", e.getMessage()); } catch (Exception e) { log.error("Warning: Unexpected error setting WM_CLASS: {}", e.getMessage()); } } } ================================================ FILE: src/main/java/com/mucommander/desktop/gnome/GnomeTrash.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.gnome; import java.io.IOException; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.QueuedTrash; import com.mucommander.job.DeleteJob; import com.mucommander.process.ProcessRunner; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; /** * This class handles with GNOME Trash (deleting to trash, empty the trash, go to trash etc.) *

    * Implementation notes:
    *
    * This Trash class has the same possibilities as KDETrash, but is adapted to a * GNOME environment, where the trash is simple directory ~/.Trash. So working with trash means * working with this directory. * * @see GnomeTrashProvider * @author David Kovar (kowy), Maxence Bernard */ public class GnomeTrash extends QueuedTrash { private static final Logger LOGGER = LoggerFactory.getLogger(GnomeTrash.class); /** Open trash folder default file manager in Gnome. */ private static final String REVEAL_TRASH_COMMAND = "xdg-open trash:///"; /** * User trash folder, as defined by the freedesktop specification (see * http://freedesktop.org/wiki/Specifications/trash-spec) null if there is no * usable trash folder. */ private static final AbstractFile TRASH_FOLDER; /** "info" subfolder of the user trash folder. */ private static final AbstractFile TRASH_INFO_SUBFOLDER; /** "files" subfolder of the user trash folder. */ private static final AbstractFile TRASH_FILES_SUBFOLDER; /** * Volume on which the trash folder resides, used for checking whether a file can be moved to * the trash or not. */ private static final AbstractFile TRASH_VOLUME; /** Formats dates in trash info files. */ private static final SimpleDateFormat INFO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); static { TRASH_FOLDER = getTrashFolder(); if (TRASH_FOLDER != null) { TRASH_INFO_SUBFOLDER = TRASH_FOLDER.getChildSilently("info"); TRASH_FILES_SUBFOLDER = TRASH_FOLDER.getChildSilently("files"); TRASH_VOLUME = TRASH_FOLDER.getVolume(); } else { TRASH_INFO_SUBFOLDER = null; TRASH_FILES_SUBFOLDER = null; TRASH_VOLUME = null; } } /** * Tries to find an existing user Trash folder in one of the two common locations and returns * it. If no existing Trash folder was found, creates the standard GNOME user Trash folder * and returns it. * * @return the user Trash folder, null if no user trash folder could be found or * created */ private static AbstractFile getTrashFolder() { AbstractFile userHome = LocalFile.getUserHome(); // Primary trash location: new distro's trash path AbstractFile primaryTrashDir = userHome.getChildSilently(".local/share/Trash/"); if (isTrashFolder(primaryTrashDir)) { return primaryTrashDir; } else { // Secondary trash location: old/standard path defined in specification AbstractFile secondaryTrashDir = userHome.getChildSilently("Trash/"); if (isTrashFolder(secondaryTrashDir)) { return secondaryTrashDir; } } // No existing user trash was found: create the folder, only if it doesn't already exist. if (!primaryTrashDir.exists()) { try { primaryTrashDir.mkdirs(); primaryTrashDir.getChild("info").mkdir(); primaryTrashDir.getChild("files").mkdir(); return primaryTrashDir; } catch (IOException e) { // Will return null } } return null; } /** * Return true if the specified file is a GNOME Trash folder, i.e. is a * directory and has two subdirectories named "info" and "files". * * @param file * the file to test * @return true if the specified file is a GNOME Trash folder */ private static boolean isTrashFolder(final AbstractFile file) { try { return file != null && file.isDirectory() && file.getChild("info").isDirectory() && file.getChild("files").isDirectory(); } catch (IOException e) { return false; } } /** * Implementation notes: always returns true. * * @return True if trash can be emptied, otherwise false */ @Override public final boolean canEmpty() { return TRASH_FOLDER != null; } /** * Return trash files count *

    * We assume the count of items in trash equals the count of files in * TRASH_PATH + "/info" folder. * * @return Count of files in trash */ @Override public final int getItemCount() { // Abort if there is no usable trash folder if (TRASH_FOLDER == null) { return -1; } try { return TRASH_INFO_SUBFOLDER.ls().length; } catch (java.io.IOException ex) { // can't access trash folder return -1; } } /** * Empty the trash. *

    * Implementation notes:
    * Simply free the TRASH_PATH directory * * @return True if everything went well */ @Override public final boolean empty() { // Abort if there is no usable trash folder if (TRASH_FOLDER == null) { return false; } FileSet filesToDelete = new FileSet(TRASH_FOLDER); try { // delete real files filesToDelete.addAll(TRASH_FILES_SUBFOLDER.ls()); // delete spec files filesToDelete.addAll(TRASH_INFO_SUBFOLDER.ls()); } catch (java.io.IOException ex) { LOGGER.debug("Failed to list files", ex); return false; } if (filesToDelete.size() > 0) { // Starts deleting files MainFrame mainFrame = WindowManager.getCurrentMainFrame(); ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("delete_dialog.deleting")); DeleteJob deleteJob = new DeleteJob(progressDialog, mainFrame, filesToDelete, false); progressDialog.start(deleteJob); } return true; } @Override public final boolean canOpen() { return TRASH_FOLDER != null; } /** * Opens the trash in default environment filemanager. */ @Override public final void open() { try { ProcessRunner.execute(REVEAL_TRASH_COMMAND).waitFor(); } catch (Exception e) { // IOException, InterruptedException LOGGER.debug("Caught an exception running command \"" + REVEAL_TRASH_COMMAND + "\"", e); } } @Override public final boolean isTrashFile(final AbstractFile file) { return TRASH_FOLDER != null && (file.getTopAncestor() instanceof LocalFile) && TRASH_FOLDER.isParentOf(file); } /** * Implementation notes: returns true only for local files that are not archive * entries and that reside on the same volume as the trash folder. */ @Override public final boolean canMoveToTrash(final AbstractFile file) { return TRASH_FOLDER != null && file.getTopAncestor() instanceof LocalFile && file.getVolume().equals(TRASH_VOLUME); } /** * Implementation of {@link com.mucommander.desktop.QueuedTrash} moveToTrash method. *

    * Try to copy a collection of files to the GNOME's Trash. * * @param queuedFiles * Collection of files to the trash * @return true if movement has been successful or false otherwise */ @Override protected final boolean moveToTrash(final List queuedFiles) { String fileInfoContent; String trashFileName; boolean retVal = true; // overall return value (if everything went OK or at least one file // wasn't moved properly for (AbstractFile fileToDelete : queuedFiles) { // generate content of info file and new filename try { fileInfoContent = getFileInfoContent(fileToDelete); trashFileName = getUniqueFilename(fileToDelete); } catch (IOException ex) { LOGGER.debug("Failed to create filename for new trash item: " + fileToDelete.getName(), ex); // continue with other file (do not move file, because info file cannot be properly // created continue; } AbstractFile infoFile = null; OutputStreamWriter infoWriter = null; try { // create info file infoFile = TRASH_INFO_SUBFOLDER.getChild(trashFileName + ".trashinfo"); infoWriter = new OutputStreamWriter(infoFile.getOutputStream()); infoWriter.write(fileInfoContent); } catch (IOException ex) { retVal = false; LOGGER.debug("Failed to create trash info file: " + trashFileName, ex); // continue with other file (do not move file, because info file wasn't properly // created) continue; } finally { if (infoWriter != null) { try { infoWriter.close(); } catch (IOException e) { // Not much else to do } } } try { // rename original file fileToDelete.renameTo(TRASH_FILES_SUBFOLDER.getChild(trashFileName)); } catch (IOException ex) { try { // remove info file infoFile.delete(); } catch (IOException ex1) { // simply ignore } retVal = false; LOGGER.debug("Failed to move file to trash: " + trashFileName, ex); } } return retVal; } /** * Make a content of .trashinfo file. * * @param file * File for which the content is built * @return Final content */ private String getFileInfoContent(final AbstractFile file) { synchronized (INFO_DATE_FORMAT) { // SimpleDateFormat is not thread safe return "[Trash Info]\n" + "Path=" + file.getAbsolutePath() + "\n" + "DeletionDate=" + INFO_DATE_FORMAT.format(new Date()); } } /** * It is possible to add several files with same name to the Trash. These files are * distinguished by _N appended to the name, where _N is rising int number.
    * This method tries to find first empty filename_N.ext. * * @param file * File to be deleted * @return Suitable filename in trash (without .trashinfo extension) * @throws IOException * If trash file cannot be accessed */ private String getUniqueFilename(final AbstractFile file) throws IOException { // try if no previous file in trash exists if (!TRASH_FILES_SUBFOLDER.getChild(file.getName()).exists()) { return file.getName(); } String rawName = file.getNameWithoutExtension(); String extension = file.getExtension(); // find first empty filename in format filename_N.ext String filename; int count = 1; while (true) { filename = rawName + "_" + count++; if (extension != null) { filename += "." + extension; } if (!TRASH_FILES_SUBFOLDER.getChild(filename).exists()) { return filename; } } } } ================================================ FILE: src/main/java/com/mucommander/desktop/gnome/GnomeTrashProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.gnome; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.TrashProvider; /** * Provider for the Trash in GNOME Desktop Environment * * @see GnomeTrash * @author David Kovar (kowy) */ public class GnomeTrashProvider implements TrashProvider { public AbstractTrash getTrash() { return new GnomeTrash(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/gnome/GuessedGnomeDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.gnome; import com.mucommander.process.ProcessRunner; /** * 'Guessed' desktop adapter for GNOME. The availability of this desktop depends on the presence of the * gnome-open command. * * @author Nicolas Rinaudo */ public class GuessedGnomeDesktopAdapter extends GnomeDesktopAdapter { private String fileOpenerCommand; public String toString() { return "Gnome Desktop (guessed)"; } @Override public boolean isAvailable() { try { ProcessRunner.execute(GVFS_OPEN); fileOpenerCommand = GVFS_OPEN; return true; } catch(Exception ignore) {} try { ProcessRunner.execute(GNOME_OPEN); fileOpenerCommand = GNOME_OPEN; return true; } catch(Exception ignore) {} return false; } @Override protected String getFileOpenerCommand() { return fileOpenerCommand; } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/ConfiguredKde3DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; /** * 'Configured' desktop adapter for KDE 3. The availability of this desktop depends on the presence of the * KDE_FULL_SESSION environment variable. * * @author Nicolas Rinaudo */ public class ConfiguredKde3DesktopAdapter extends Kde3DesktopAdapter { private static final String KDE_FULL_SESSION_VAR = "KDE_FULL_SESSION"; @Override protected String getConfiguredEnvVariable(String name) { return System.getenv(name); } public String toString() { return "KDE 3 Desktop"; } @Override public boolean isAvailable() { String var = getConfiguredEnvVariable(KDE_FULL_SESSION_VAR); return var != null && !var.trim().isEmpty(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/ConfiguredKde4DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; /** * 'Configured' desktop adapter for KDE 4. The availability of this desktop depends on the presence of the * KDE_SESSION_VERSION environment variable that was introduced in KDE 4. * * @author Maxence Bernard */ public class ConfiguredKde4DesktopAdapter extends Kde4DesktopAdapter { private static final String KDE_SESSION_VERSION_VAR = "KDE_SESSION_VERSION"; public String toString() { return "KDE 4 Desktop"; } @Override public boolean isAvailable() { String var = getConfiguredEnvVariable(KDE_SESSION_VERSION_VAR); return var != null && !var.trim().isEmpty(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/GuessedKde3DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import com.mucommander.process.ProcessRunner; /** * 'Guessed' desktop adapter for KDE 3. The availability of this desktop depends on the presence of the * kfmclient command. * * @author Nicolas Rinaudo */ public class GuessedKde3DesktopAdapter extends Kde3DesktopAdapter { public String toString() { return "KDE 3 Desktop (guessed)"; } @Override public boolean isAvailable() { try { ProcessRunner.execute(BASE_COMMAND); return true; } catch(Exception e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/GuessedKde4DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import com.mucommander.process.ProcessRunner; /** * 'Guessed' desktop adapter for KDE 4. The availability of this desktop depends on the presence of the * kioclient command. * * @author Maxence Bernard */ public class GuessedKde4DesktopAdapter extends Kde4DesktopAdapter { public String toString() { return "KDE 4 Desktop (guessed)"; } @Override public boolean isAvailable() { try { ProcessRunner.execute(BASE_COMMAND); return true; } catch(Exception e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/Kde3DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import com.mucommander.desktop.TrashProvider; /** * @author Maxence Bernard */ abstract class Kde3DesktopAdapter extends KdeDesktopAdapter { static String BASE_COMMAND = "kfmclient"; @Override protected String getFileManagerName() { return "Konqueror"; } @Override protected String getBaseCommand() { return BASE_COMMAND; } @Override protected TrashProvider getTrashProvider() { return new Kde3TrashProvider(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/Kde3TrashProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.TrashProvider; /** * This class is a trash provider for the {@link KdeTrash KDE 3 trash}. * * @see KdeTrash * @author Maxence Bernard */ class Kde3TrashProvider implements TrashProvider { public AbstractTrash getTrash() { return new KdeTrash(Kde3DesktopAdapter.BASE_COMMAND); } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/Kde4DesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import com.mucommander.desktop.TrashProvider; /** * @author Maxence Bernard */ abstract class Kde4DesktopAdapter extends KdeDesktopAdapter { static String BASE_COMMAND = "kioclient"; @Override protected String getFileManagerName() { return "Dolphin"; } @Override protected String getBaseCommand() { return BASE_COMMAND; } @Override protected TrashProvider getTrashProvider() { return new Kde4TrashProvider(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/Kde4TrashProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.TrashProvider; /** * This class is a trash provider for the {@link KdeTrash KDE 3 trash}. * * @see KdeTrash * @author Maxence Bernard */ class Kde4TrashProvider implements TrashProvider { public AbstractTrash getTrash() { return new KdeTrash(Kde4DesktopAdapter.BASE_COMMAND); } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/KdeConfig.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; /** * Provides access to the KDE configuration, using the kreadconfig command. * * @author Maxence Bernard */ @Slf4j public class KdeConfig { /** Name of the command to invoke for retrieving configuration values */ private static final String CONFIG_COMMAND = "kreadconfig"; /** Timeout for the configuration command execution in seconds */ private static final long COMMAND_TIMEOUT = 5; /** * Returns the KDE configuration value corresponding to the given key, null if this key has no value. * * @param key key to the configuration value to retrieve. * @return the configuration value corresponding to the given key, null if this key has no value. * @throws IOException if an error occurred while invoking the kreadconfig command, for instance if the * command isn't available in the path. */ public static String getValue(String key) throws IOException { ProcessBuilder processBuilder = new ProcessBuilder(CONFIG_COMMAND, "--key", key); processBuilder.redirectErrorStream(true); try { Process process = processBuilder.start(); try (BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line = br.readLine(); // Wait for process completion with timeout if (!process.waitFor(COMMAND_TIMEOUT, TimeUnit.SECONDS)) { process.destroyForcibly(); log.warn("Command timed out for key: {}", key); throw new IOException("Command timed out: " + CONFIG_COMMAND); } int exitCode = process.exitValue(); if (exitCode != 0) { log.debug("Command returned exit code {} for key: {}", exitCode, key); return null; } log.debug(CONFIG_COMMAND + " returned '{}' for {}", line, key); if (line == null || (line = line.trim()).isEmpty()) return null; return line; } } catch(IOException e) { log.debug("Error while retrieving value for {}", key, e); throw e; } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.debug("Interrupted while retrieving value for {}", key, e); throw new IOException("Command interrupted", e); } } } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/KdeDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.command.Command; import com.mucommander.command.CommandException; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.desktop.DefaultDesktopAdapter; import com.mucommander.desktop.DesktopInitializationException; import com.mucommander.desktop.DesktopManager; import com.mucommander.desktop.TrashProvider; /** * @author Nicolas Rinaudo, Maxence Bernard */ abstract class KdeDesktopAdapter extends DefaultDesktopAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(KdeDesktopAdapter.class); /** Multi-click interval, cached to avoid polling the value every time {@link #getMultiClickInterval()} is called */ private int multiClickInterval; /** Key to the double-click interval value in the KDE configuration */ private final String DOUBLE_CLICK_CONFIG_KEY = "DoubleClickInterval"; @Override public void init(boolean install) throws DesktopInitializationException { // Initialises trash management. DesktopManager.setTrashProvider(getTrashProvider()); // Registers KDE specific commands. try { String execCommand = getBaseCommand()+" exec $f"; CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_OPENER_ALIAS, execCommand, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.URL_OPENER_ALIAS, execCommand, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_MANAGER_ALIAS, execCommand, CommandType.SYSTEM_COMMAND, getFileManagerName(), null)); } catch(CommandException e) { throw new DesktopInitializationException(e); } // Multi-click interval retrieval try { String value = KdeConfig.getValue(DOUBLE_CLICK_CONFIG_KEY); if(value==null) multiClickInterval = super.getMultiClickInterval(); multiClickInterval = Integer.parseInt(value); } catch(Exception e) { LOGGER.debug("Error while retrieving double-click interval from gconftool", e); multiClickInterval = super.getMultiClickInterval(); } } /** * Returns the DoubleClickInterval KDE configuration value. * If the returned value is not defined or could not be retrieved, the value of * {@link DefaultDesktopAdapter#getMultiClickInterval()} is returned.
    * The value is retrieved on initialization and never updated thereafter. *

    * Note under Java 1.6 or below, the returned value does not match the one used by Java for generating multi-clicks * (see {@link DefaultDesktopAdapter#getMultiClickInterval()}, as Java uses the multi-click speed declared in * X Window's configuration, not in KDE's. See * Java Bug 5076635 for more information. * * @return the DoubleClickInterval KDE configuration value. */ @Override public int getMultiClickInterval() { return multiClickInterval; } //////////////////// // Helper methods // //////////////////// /** * Returns the 'configured' value of the given environment variable, null if the variable has no value. * * @param name name of the environment variable to retrieve * @return the 'configured' value of the given environment variable, null if the variable has no value. */ protected String getConfiguredEnvVariable(String name) { return System.getenv(name); } ///////////////////////////////// // KDE version-specific values // ///////////////////////////////// /** * Returns the name of KDE's file manager. * * @return the name of KDE's file manager. */ protected abstract String getFileManagerName(); /** * Returns the base command that is used for interacting with KDE. * * @return the base command that is used for interacting with KDE. */ protected abstract String getBaseCommand(); /** * Returns an instance of {@link TrashProvider} giving access to the KDE trash. * * @return an instance of {@link TrashProvider} giving access to the KDE trash. */ protected abstract TrashProvider getTrashProvider(); } ================================================ FILE: src/main/java/com/mucommander/desktop/kde/KdeTrash.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.kde; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.desktop.QueuedTrash; import com.mucommander.process.ProcessRunner; /** * This class provides access to the KDE trash. Only local files (or locally mounted files) can be moved to the trash. * *

    * Implementation notes:
    *
    * This trash is implemented as a {@link com.mucommander.desktop.QueuedTrash} as it spawns a process to move a file to * the trash and it is thus more effective to group files to be moved instead of spawning multiple processes.
    * * @see Kde3TrashProvider * @author Maxence Bernard */ class KdeTrash extends QueuedTrash { private static final Logger LOGGER = LoggerFactory.getLogger(KdeTrash.class); /** Command that empties the trash */ private final static String EMPTY_TRASH_COMMAND = "ktrash --empty"; /** Command that allows to interact with the trash */ private String baseCommand; /** * Creates a new KDETrash instance using the specified command for interacting with the trash. * * @param baseCommand command that allows to interact with the trash. */ KdeTrash(String baseCommand) { this.baseCommand = baseCommand; } /** * Executes the given command and waits for the process termination. * Returns true if the command was executed without any error. * * @param command the command to execute * @return true if the command was executed without any error */ private static boolean executeAndWait(String command) { try { ProcessRunner.execute(command).waitFor(); return true; } catch(Exception e) { // IOException, InterruptedException LOGGER.debug("Caught exception", e); return false; } } /** * Executes the given command and waits for the process termination. * Returns true if the command was executed without any error. * * @param command the command tokens * @return true if the command was executed without any error */ private static boolean executeAndWait(String command[]) { try { ProcessRunner.execute(command).waitFor(); return true; } catch(Exception e) { // IOException, InterruptedException LOGGER.debug("Caught exception", e); return false; } } ////////////////////////////////// // AbstractTrash implementation // ////////////////////////////////// /** * Implementation notes: returns true only for local files that are not archive entries. */ @Override public boolean canMoveToTrash(AbstractFile file) { return file.getTopAncestor() instanceof LocalFile; } /** * Implementation notes: always returns true. */ @Override public boolean canEmpty() { return true; } @Override public boolean empty() { return executeAndWait(EMPTY_TRASH_COMMAND); } @Override public boolean isTrashFile(AbstractFile file) { return (file.getTopAncestor() instanceof LocalFile) && (file.getAbsolutePath(true).contains("/.local/share/Trash/")); } /** * Implementation notes: always returns -1 (information not available). */ @Override public int getItemCount() { return -1; } @Override public void open() { executeAndWait(baseCommand+" exec trash:/"); } /** * Implementation notes: always returns true. */ @Override public boolean canOpen() { return true; } //////////////////////////////// // QueuedTrash implementation // //////////////////////////////// @Override protected boolean moveToTrash(List queuedFiles) { int nbFiles = queuedFiles.size(); String tokens[] = new String[nbFiles+3]; tokens[0] = baseCommand; tokens[1] = "move"; for(int i=0; i. */ package com.mucommander.desktop.macos; import com.mucommander.commons.runtime.OsFamily; import java.io.File; public class OSXApplications { public static boolean iTermInstalled() { return OsFamily.getCurrent() == OsFamily.MAC_OS_X && new File("/Applications/iTerm.app").exists(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/macos/OSXDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.macos; import com.apple.eawt.FullScreenUtilities; import com.apple.eio.FileManager; import com.dd.plist.BinaryPropertyListParser; import com.dd.plist.NSString; import com.dd.plist.PropertyListFormatException; import com.mucommander.ui.macosx.OSXIntegration; import com.mucommander.ui.text.MultiLineLabel; import com.sun.jna.platform.mac.XAttrUtils; import com.mucommander.command.Command; import com.mucommander.command.CommandException; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.OSXFileUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.commons.util.Pair; import com.mucommander.desktop.DefaultDesktopAdapter; import com.mucommander.desktop.DesktopInitializationException; import com.mucommander.desktop.DesktopManager; import com.mucommander.desktop.TrashProvider; import com.mucommander.ui.macosx.AppleScript; import com.mucommander.ui.macosx.TabbedPaneUICustomizer; import com.mucommander.ui.notifier.AbstractNotifier; import com.mucommander.ui.notifier.GrowlNotifier; import com.mucommander.utils.text.Translator; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.MouseEvent; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.function.Consumer; import static com.mucommander.command.CommandManager.registerDefaultCommand; /** * @author Nicolas Rinaudo */ public class OSXDesktopAdapter extends DefaultDesktopAdapter { private static final String OPENER_COMMAND = "open $f"; // private static final String FINDER_COMMAND = "open $f -R"; private static final String FINDER_COMMAND = "open -a Finder $f"; private static final String FINDER_NAME = "Finder"; /** The key of the comment attribute in file metadata */ public static final String COMMENT_PROPERTY_NAME = "com.apple.metadata:kMDItemFinderComment"; public static final String TAGS_PROPERTY_NAME = "com.apple.metadata:_kMDItemUserTags"; private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(OSXDesktopAdapter.class); public String toString() { return "Mac OS X Desktop"; } @Override public boolean isAvailable() { return OsFamily.MAC_OS_X.isCurrent(); } @Override public void init(boolean install) throws DesktopInitializationException { // Initializes trash management. DesktopManager.setTrashProvider(new OSXTrashProvider()); // Registers OS X specific commands. try { registerDefaultCommand(new Command(CommandManager.FILE_OPENER_ALIAS, OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); registerDefaultCommand(new Command(CommandManager.URL_OPENER_ALIAS, OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); registerDefaultCommand(new Command(CommandManager.FILE_MANAGER_ALIAS, FINDER_COMMAND, CommandType.SYSTEM_COMMAND, FINDER_NAME, null)); new OSXIntegration(); } catch(CommandException e) { throw new DesktopInitializationException(e); } } @Override public boolean isLeftMouseButton(MouseEvent e) { int modifiers = e.getModifiersEx(); return (modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0 && !e.isControlDown(); } @Override public boolean isRightMouseButton(MouseEvent e) { int modifiers = e.getModifiersEx(); return (modifiers & MouseEvent.BUTTON3_DOWN_MASK) != 0 || ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0 && e.isControlDown()); } /** * Returns true for directories with an app extension (case-insensitive comparison). * * @param file the file to test * @return true for directories with an app extension (case-insensitive comparison). */ @Override public boolean isApplication(AbstractFile file) { String extension = file.getExtension(); // the isDirectory() test comes last as it is I/O bound return "app".equalsIgnoreCase(extension) && file.isDirectory(); } @Override public TrashProvider getTrash() { return new OSXTrashProvider(); } @Override public AbstractNotifier getNotifier() { return GrowlNotifier.isGrowlRunning() ? new GrowlNotifier() : null; } @Override public Consumer getTabbedPaneCustomizer() { return TabbedPaneUICustomizer::customizeTabbedPaneUI; } @Override public void postCopy(AbstractFile sourceFile, AbstractFile destFile) { if (sourceFile.hasAncestor(LocalFile.class) && destFile.hasAncestor(LocalFile.class)) { String sourcePath = sourceFile.getAbsolutePath(); String destPath = destFile.getAbsolutePath(); copyFileUserTags(sourcePath, destPath); copyFileTypeAndCreator(sourcePath, destPath); copyFileComment(sourcePath, destPath); } } private void copyFileUserTags(String sourcePath, String destPath) { byte[] bytes = XAttrUtils.read(sourcePath, TAGS_PROPERTY_NAME); if (bytes != null) { XAttrUtils.write(destPath, TAGS_PROPERTY_NAME, bytes); } } private void copyFileTypeAndCreator(String sourcePath, String destPath) { try { FileManager.setFileTypeAndCreator(destPath, FileManager.getFileType(sourcePath), FileManager.getFileCreator(sourcePath)); } catch(IOException e) { // Swallow the exception and do not interrupt the transfer LOGGER.debug("Error while setting macOS file type and creator on destination", e); } } private void copyFileComment(String sourcePath, String destPath) { byte[] bytes = XAttrUtils.read(sourcePath, COMMENT_PROPERTY_NAME); if (bytes == null) { return; } String comment = null; try { NSString value = (NSString) BinaryPropertyListParser.parse(bytes); if (value != null) { comment = value.getContent(); } } catch (IOException | PropertyListFormatException e) { // Swallow the exception and do not interrupt the transfer LOGGER.debug("Error while parsing macOS file comment of source", e); } if (comment != null && !(comment = comment.trim()).isEmpty() && !setFileComment(destPath, comment)) { LOGGER.error("Error while copying macOS file comment to %s", destPath); } } private boolean setFileComment(String path, String comment) { String script = String.format(OSXFileUtils.SET_COMMENT_APPLESCRIPT, path, comment); return AppleScript.execute(script, null); } public void customizeMainFrame(Window window) { FullScreenUtilities.setWindowCanFullScreen(window, true); } @Override public List> getExtendedFileProperties(AbstractFile file) { if (OsVersion.MAC_OS_X_10_4.isCurrentOrHigher()) { String comment = OSXFileUtils.getSpotlightComment(file); JLabel commentLabel = new JLabel(Translator.get("comment")+":"); commentLabel.setAlignmentY(JLabel.TOP_ALIGNMENT); commentLabel.setVerticalAlignment(SwingConstants.TOP); return Collections.singletonList(new Pair<>(commentLabel, new MultiLineLabel(comment))); } return Collections.emptyList(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/macos/OSXTerminal.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.macos import com.mucommander.commons.file.AbstractFile import com.mucommander.conf.TcConfigurations import com.mucommander.conf.TcPreference import com.mucommander.conf.TcPreferences import com.mucommander.ui.macosx.AppleScript object OSXTerminal { @JvmStatic fun addNewTabWithCommands(currentFolder: AbstractFile, vararg commands: String): Boolean = if (getTerminalType() == TerminalApp.ITERM2) { addNewITermTabWithCommands(currentFolder, *commands) } else { addNewTerminalTabWithCommands(currentFolder, *commands) } @JvmStatic fun openNewWindowAndRun(currentFolder: AbstractFile, vararg commands: String): Boolean = if (getTerminalType() == TerminalApp.ITERM2) { openNewITermWindowAndRun(currentFolder, *commands) } else { openNewTerminalWindowAndRun(currentFolder, *commands) } private fun addNewTerminalTabWithCommands(currentFolder: AbstractFile, vararg commands: String): Boolean { val dir = currentFolder.absolutePath.replace("'", "'\\''") val script = StringBuilder().apply { append("on run argv\n") append("tell application \"Terminal\"\n") append("if not (exists window 1) then reopen\n") append("activate\n") append("do script \"cd '").append(dir).append("' && clear\" in front window\n") append("delay 0.3\n") for (cmd in commands) { val escapedCmd = cmd.replace("\"", "\\\"") append("do script \"").append(escapedCmd).append("\" in front window\n") append("delay 0.3\n") } append("end tell\n") append("end run\n") }.toString() return AppleScript.execute(script, null, currentFolder) } private fun addNewITermTabWithCommands(currentFolder: AbstractFile, vararg commands: String): Boolean { val dir = currentFolder.absolutePath.replace("'", "'\\''") val script = StringBuilder().apply { append("on run argv\n") append("tell application \"iTerm2\"\n") append("activate\n") // Create tab or new window append("if (count of windows) > 0 then\n") append(" tell current window\n") append(" create tab with default profile\n") append(" tell current session\n") append(" write text \"cd '").append(dir).append("'\"\n") // script.append(" write text \"clear\"\n"); for (cmd in commands) { val escapedCmd = cmd.replace("\"", "\\\"").replace("\\", "\\\\") append(" write text \"").append(escapedCmd).append("\"\n") append(" delay 0.15\n") } append(" end tell\n") append(" end tell\n") append("else\n") // If window doesn't exist append(" create window with default profile\n") append(" tell current session of current window\n") append(" write text \"cd '").append(dir).append("'\"\n") // script.append(" write text \"clear\"\n"); for (cmd in commands) { val escapedCmd = cmd.replace("\"", "\\\"").replace("\\", "\\\\") append(" write text \"").append(escapedCmd).append("\"\n") append(" delay 0.15\n") } append(" end tell\n") append("end if\n") append("end tell\n") append("end run\n") }.toString() return AppleScript.execute(script, null, currentFolder) } private fun openNewTerminalWindowAndRun(currentFolder: AbstractFile, vararg commands: String): Boolean { val escapedPath = currentFolder.absolutePath .replace("\\", "\\\\") .replace("'", "'\\\\''") val script = StringBuilder().apply { append("tell application \"Terminal\"\n") append("activate\n") val cmdBuilder = StringBuilder() cmdBuilder.append("cd '").append(escapedPath).append("'") for (cmd in commands) { val escapedCmd = cmd.replace("\\", "\\\\").replace("'", "'\\\\''") cmdBuilder.append("; ").append(escapedCmd) } append("do script \"").append(cmdBuilder).append("\"\n") append("end tell\n") }.toString() return AppleScript.execute(script, null, currentFolder) } private fun openNewITermWindowAndRun(currentFolder: AbstractFile, vararg commands: String): Boolean { val dir = currentFolder.absolutePath.replace("'", "'\\''") val script = StringBuilder().apply { append("tell application \"iTerm2\"\n") append("activate\n") append("create window with default profile\n") append("tell current session of current window\n") append(" write text \"cd '").append(dir).append("'\"\n") // script.append(" write text \"clear\"\n"); for (cmd in commands) { val escapedCmd = cmd.replace("\"", "\\\"").replace("\\", "\\\\") append(" write text \"").append(escapedCmd).append("\"\n") append(" delay 0.15\n") } append("end tell\n") append("end tell\n") }.toString() return AppleScript.execute(script, null, currentFolder) } private fun getTerminalType(): TerminalApp { val term = TcConfigurations.getPreferences().getVariable(TcPreference.EXTERNAL_TERMINAL_TYPE, TcPreferences.DEFAULT_TERMINAL_TYPE) return if (term == TcPreferences.TERMINAL_ITERM && OSXApplications.iTermInstalled()) TerminalApp.ITERM2 else TerminalApp.TERMINAL } internal enum class TerminalApp { TERMINAL, ITERM2 } } ================================================ FILE: src/main/java/com/mucommander/desktop/macos/OSXTrash.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.macos; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.desktop.QueuedTrash; import com.mucommander.ui.macosx.AppleScript; import com.sun.jna.platform.mac.MacFileUtils; import lombok.extern.slf4j.Slf4j; import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; import java.util.stream.Collectors; /** * OSXTrash provides access to the Mac OS X Finder's trash. Only local files (or locally mounted files) can be moved * to the trash. * *

    * Implementation notes:
    * This trash is implemented as a {@link com.mucommander.desktop.QueuedTrash} for several reasons: *

      *
    • the Finder plays a sound when it has been told to move a file to the trash and is done with it. * Moving files to the trash repeatedly would play the sound as many times as the Finder has been told to move a * file, which is obviously ugly.
    • *
    • executing an AppleScript has a cost as it has to be compiled first. When files are moved repeatedly, it is * more efficient to group files and execute only one AppleScript.
    • *
    *
    * This class uses {@link com.mucommander.ui.macosx.AppleScript} to interact with the trash. * * @see OSXTrashProvider * @author Maxence Bernard */ @Slf4j public class OSXTrash extends QueuedTrash { /** AppleScript that reveals the trash in Finder */ private final static String REVEAL_TRASH_APPLESCRIPT = "tell application \"Finder\" to open trash\nactivate application \"Finder\"\n"; /** AppleScript that counts and returns the number of items in Trash */ private final static String COUNT_TRASH_ITEMS_APPLESCRIPT = "tell application \"Finder\" to return count of items in trash"; /** AppleScript that empties the trash */ private final static String EMPTY_TRASH_APPLESCRIPT = "tell application \"Finder\" to empty trash"; private static final MacFileUtils macFileUtils = new MacFileUtils(); /** * AppleScript that moves files to the trash, for versions of AppleScript (1.10 or lower )that do not allow Unicode * in the script itself (only MacRoman). As a result, this script is more complicated as the only way to deal with * Unicode text is to read them from a file. See http://www.satimage.fr/software/en/unicode_and_applescript.html * for more info about this workaround. */ private final static String MOVE_TO_TRASH_APPLESCRIPT_NO_UNICODE = // Loads the contents of the UTF8-encoded file which path is contained in the 'tmpFilePath' variable. // This variable must be set before the beginning of the script. This file contains the list of files to move // to the trash, separated by EOL characters. The file must NOT end with a trailing EOL. "set tmpFile to (open for access (POSIX file tmpFilePath))\n" + "set tmpFileContents to (read tmpFile for (get eof tmpFile) as «class utf8»)\n" + "close access tmpFile\n" + // Split the file contents into a list of lines, each line representing a POSIX file path to delete "set posixFileList to every paragraph of tmpFileContents\n" + // Convert the list of POSIX paths into a list of file objects. Note that internally AppleScript uses // a Mac-specific colon-separated path notation rather than the POSIX one. "set fileCount to the number of items in posixFileList\n" + "set fileList to {}\n" + "repeat with i from 1 to the fileCount\n" + "set posixFile to item i of posixFileList\n" + "copy POSIX file posixFile to the end of fileList\n" + "end repeat\n" + // Tell the Finder to move those files to the trash. Note that the file list must contain file objects and not // POSIX paths, hence the previous step. "tell application \"Finder\" to move fileList to the trash"; /** * Implementation notes: returns true only for local files that are not archive entries. */ @Override public boolean canMoveToTrash(AbstractFile file) { return file.getTopAncestor() instanceof LocalFile; } /** * Implementation notes: always returns true. */ @Override public boolean canEmpty() { return true; } @Override public boolean empty() { return AppleScript.execute(EMPTY_TRASH_APPLESCRIPT, null); } @Override public boolean isTrashFile(AbstractFile file) { return (file.getTopAncestor() instanceof LocalFile) && (file.getAbsolutePath(true).contains("/.Trash/")); } /** * Implementation notes: this method is implemented and returns -1 only if an error ocurred while * retrieving the trash item count. */ @Override public int getItemCount() { StringBuilder output = new StringBuilder(); if (!AppleScript.execute(COUNT_TRASH_ITEMS_APPLESCRIPT, output)) { return -1; } try { return Integer.parseInt(output.toString().trim()); } catch(NumberFormatException e) { log.debug("Caught an exception", e); return -1; } } @Override public void open() { AppleScript.execute(REVEAL_TRASH_APPLESCRIPT, null); } /** * Implementation notes: always returns true. */ @Override public boolean canOpen() { return true; } /** * Performs the actual job of moving files to the trash using JNA. */ @Override protected boolean moveToTrash(List queuedFiles) { if (queuedFiles.isEmpty()) return true; var partitionedByIsSmb = queuedFiles.stream().collect(Collectors.partitioningBy(this::isSmb)); var nonSmbFiles = partitionedByIsSmb.get(false); if (moveToTrashViaJna(nonSmbFiles)) { var smbFiles = partitionedByIsSmb.get(true); return moveToTrashViaAppleScript(smbFiles); } else { log.error("failed to move files to trash using JNA, fall back to AppleScript"); return moveToTrashViaAppleScript(queuedFiles); } } private boolean isSmb(AbstractFile file) { try { File underlyingFile = (File) file.getUnderlyingFileObject(); java.nio.file.Path path = underlyingFile.toPath(); java.nio.file.FileStore fs = java.nio.file.Files.getFileStore(path); return "smbfs".equals(fs.type()); } catch (IOException e) { log.warn("failed to retrieve FileStore of {}", file, e); return false; } } private boolean moveToTrashViaJna(List queuedFiles) { File[] files = queuedFiles.stream().map(AbstractFile::getAbsolutePath).map(File::new).toArray(File[]::new); try { macFileUtils.moveToTrash(files); } catch (IOException e) { log.error("failed to move files to trash", e); return false; } return true; } private boolean moveToTrashViaAppleScript(List queuedFiles) { if (queuedFiles.isEmpty()) { return true; } // Simple script for AppleScript versions with Unicode support, i.e. that allows Unicode characters in the // script (AppleScript 2.0 / Mac OS X 10.5 or higher). if (AppleScript.getScriptEncoding().equals(AppleScript.UTF8)) { String appleScript = queuedFiles.stream() .map(AbstractFile::getAbsolutePath) .map(path -> String.format("posix file \"%s\"", path)) .collect(Collectors.joining(", ", "tell application \"Finder\" to move {", "} to the trash")); return AppleScript.execute(appleScript, null); } // Script for AppleScript versions without Unicode support (AppleScript 1.10 / Mac OS X 10.4 or lower) else { AbstractFile tmpFile = null; OutputStreamWriter tmpOut = null; try { // Create the temporary file that contains the list of files to move, encoded as UTF-8 and separated by // EOL characters. The file must NOT end with a trailing EOL. int nbFiles = queuedFiles.size(); tmpFile = FileFactory.getTemporaryFile("trash_files.tc", false); tmpOut = new OutputStreamWriter(tmpFile.getOutputStream(), StandardCharsets.UTF_8); for (int i = 0; i < nbFiles; i++) { tmpOut.write(queuedFiles.get(i).getAbsolutePath()); if (i < nbFiles-1) { tmpOut.write("\n"); } } tmpOut.close(); // Set the 'tmpFilePath' variable to the path of the temporary file we just created String appleScript = "set tmpFilePath to \"" + tmpFile.getAbsolutePath() + "\"\n"; appleScript += MOVE_TO_TRASH_APPLESCRIPT_NO_UNICODE; boolean success = AppleScript.execute(appleScript, null); // AppleScript has been executed, we can now safely close and delete the temporary file tmpFile.delete(); return success; } catch(IOException e) { log.debug("Caught IOException", e); if (tmpOut != null) { try { tmpOut.close(); } catch(IOException ignore) { } } if (tmpFile != null) { try { tmpFile.delete(); } catch(IOException ignore) { } } return false; } } } } ================================================ FILE: src/main/java/com/mucommander/desktop/macos/OSXTrashProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.macos; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.TrashProvider; /** * This class is a trash provider for the {@link OSXTrash Mac OS X trash}. * * @see OSXTrash * @author Maxence Bernard */ public class OSXTrashProvider implements TrashProvider { public AbstractTrash getTrash() { return new OSXTrash(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/openvms/OpenVMSDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.openvms; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.desktop.DefaultDesktopAdapter; /** * A desktop adapter for OpenVMS. * *

    This adapter currently doesn't bring any improvement over {@link com.mucommander.desktop.DefaultDesktopAdapter} * -- its purpose is simply to bypass other desktop adapters tests, some of which are costly. * * @author Maxence Bernard */ public class OpenVMSDesktopAdapter extends DefaultDesktopAdapter { @Override public boolean isAvailable() { return OsFamily.OPENVMS.isCurrent(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/windows/Win9xDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.windows; import com.mucommander.command.Command; import com.mucommander.command.CommandException; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.desktop.DesktopInitializationException; /** * @author Nicolas Rinaudo */ public class Win9xDesktopAdapter extends WindowsDesktopAdapter { private static final String OPENER_COMMAND = "start \"$f\""; public String toString() {return "Windows 9x Desktop";} @Override public boolean isAvailable() {return super.isAvailable() && OsVersion.getCurrent().compareTo(OsVersion.WINDOWS_NT) < 0;} @Override public void init(boolean install) throws DesktopInitializationException { super.init(install); try { CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_OPENER_ALIAS, OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.URL_OPENER_ALIAS, OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_MANAGER_ALIAS, OPENER_COMMAND, CommandType.SYSTEM_COMMAND, EXPLORER_NAME, null)); } catch(CommandException e) { throw new DesktopInitializationException(e); } } @Override public String getDefaultShell() {return "command.com /c";} } ================================================ FILE: src/main/java/com/mucommander/desktop/windows/WinNtDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.windows; import com.mucommander.command.Command; import com.mucommander.command.CommandException; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.file.filter.RegexpFilenameFilter; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.desktop.DesktopInitializationException; /** * @author Nicolas Rinaudo */ public class WinNtDesktopAdapter extends WindowsDesktopAdapter { //private static final String FILE_OPENER_COMMAND = "cmd /c start \"\" \"$f\""; private static final String FILE_OPENER_COMMAND = "cmd /c cd \"$f\" && start ."; private static final String EXE_OPENER_COMMAND = "cmd /c $f"; private static final String EXE_REGEXP = ".*\\.exe"; public String toString() { return "Windows NT+ Desktop"; } @Override public boolean isAvailable() {return super.isAvailable() && OsVersion.getCurrent().compareTo(OsVersion.WINDOWS_NT) >= 0;} @Override public void init(boolean install) throws DesktopInitializationException { super.init(install); try { CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_OPENER_ALIAS, FILE_OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.URL_OPENER_ALIAS, FILE_OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_MANAGER_ALIAS, FILE_OPENER_COMMAND, CommandType.SYSTEM_COMMAND, EXPLORER_NAME, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.EXE_OPENER_ALIAS, EXE_OPENER_COMMAND, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultAssociation(CommandManager.EXE_OPENER_ALIAS, new RegexpFilenameFilter(EXE_REGEXP, false)); } catch(CommandException e) { throw new DesktopInitializationException(e); } } @Override public String getDefaultShell() { return "cmd /c"; } } ================================================ FILE: src/main/java/com/mucommander/desktop/windows/WindowsDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.windows; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.desktop.DefaultDesktopAdapter; import com.mucommander.desktop.DesktopInitializationException; import com.mucommander.desktop.DesktopManager; /** * @author Nicolas Rinaudo */ class WindowsDesktopAdapter extends DefaultDesktopAdapter { static final String EXPLORER_NAME = "Explorer"; public String toString() {return "Windows Desktop";} @Override public void init(boolean install) throws DesktopInitializationException { // The Windows trash requires access to the Shell32 DLL, register the provider only if the Shell32 DLL // is available on the current runtime environment. if (WindowsTrashProvider.isAvailable()) { DesktopManager.setTrashProvider(new WindowsTrashProvider()); } } @Override public boolean isAvailable() { return OsFamily.WINDOWS.isCurrent(); } /** * Returns true for regular files (not directories) with an exe extension * (case-insensitive comparison). * * @param file the file to test * @return true for regular files (not directories) with an exe extension * (case-insensitive comparison). */ @Override public boolean isApplication(AbstractFile file) { String extension = file.getExtension(); // the isDirectory() test comes last as it is I/O bound return extension != null && extension.equalsIgnoreCase("exe") && !file.isDirectory(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/windows/WindowsTrash.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.windows; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.impl.local.SpecialWindowsLocation; import com.mucommander.commons.file.util.Shell32; import com.mucommander.commons.file.util.Shell32API; import com.mucommander.desktop.DesktopManager; import com.mucommander.desktop.QueuedTrash; import java.io.IOException; import java.util.List; /** * WindowsTrash is an AbstractTrash implementation for the Microsoft Windows' Recycle Bin. * *

    Native methods in the Shell32 Windows API are used to access the Recycle Bin. There is an overhead associated with * invoking those methods (via JNA), so for performance reasons, this trash is implemented as a {@link com.mucommander.desktop.QueuedTrash} * in order to group calls to {@link #moveToTrash(com.mucommander.commons.file.AbstractFile)}. * * @see WindowsTrashProvider * @author Maxence Bernard */ public class WindowsTrash extends QueuedTrash { /** * Implementation notes: returns true only for local files that are not archive entries. */ @Override public boolean canMoveToTrash(AbstractFile file) { return file.getTopAncestor() instanceof LocalFile; } /** * Implementation notes: always returns true: {@link #empty()} is implemented. */ @Override public boolean canEmpty() { return true; } @Override public boolean empty() { return Shell32.isAvailable() && Shell32.getInstance().SHEmptyRecycleBin(null, null, Shell32API.SHERB_NOCONFIRMATION) == 0; } /** * Implementation notes: always returns false. */ @Override public boolean isTrashFile(AbstractFile file) { // Quote from http://en.wikipedia.org/wiki/Recycle_Bin_(Windows): // "The actual location of the Recycle Bin varies depending on the operating system and filesystem. On the older // FAT filesystems (typically Windows 98 and prior), it is located in Drive:\RECYCLED. In the NTFS filesystem // (Windows 2000, XP, NT) it can be found in Drive:\RECYCLER, with the exception of Windows Vista which stores // it in the Drive:\$Recycle.Bin folder." // => for the test to be accurate, we'd have to go thru the trouble of testing the kind of filesystem // (FAT or NTFS) and the Windows version. It's a lot of work for little added value. return false; } /** * Implementation notes: returns the number of items for all Recycle Bins on all drives. This information is not * available on certain versions of Windows such as Windows 2000. */ @Override public int getItemCount() { if (!Shell32.isAvailable()) { return -1; } Shell32API.SHQUERYRBINFO queryRbInfo = new Shell32API.SHQUERYRBINFO(); // pszRootPath is null to retrieve the information for all Recycle Bins on all drives. Microsoft's documentation // states that this fails on certain versions of Windows such as Windows 2000. If it does, we simply return -1. int ret = Shell32.getInstance().SHQueryRecycleBin( null, queryRbInfo ); return ret == 0 ? (int)queryRbInfo.i64NumItems : -1; } /** * Implementation notes: always returns true: {@link #open()} is implemented. */ @Override public boolean canOpen() { return true; } @Override public void open() { try { DesktopManager.openInFileManager(SpecialWindowsLocation.RECYCLE_BIN); } catch(IOException e) { // TODO: report error. } } //////////////////////////////// // QueuedTrash implementation // //////////////////////////////// @Override protected boolean moveToTrash(List queuedFiles) { if (!Shell32.isAvailable()) { return false; } Shell32API.SHFILEOPSTRUCT fileop = new Shell32API.SHFILEOPSTRUCT(); fileop.wFunc = Shell32API.FO_DELETE; fileop.fFlags = Shell32API.FOF_ALLOWUNDO| Shell32API.FOF_NOCONFIRMATION| Shell32API.FOF_SILENT; int nbFiles = queuedFiles.size(); String[] paths = new String[nbFiles]; for (int i = 0; i. */ package com.mucommander.desktop.windows; import com.mucommander.commons.file.util.Shell32; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.TrashProvider; /** * This class is a trash provider for the {@link WindowsTrash Windows trash}. * * @see WindowsTrash * @author Maxence Bernard */ public class WindowsTrashProvider implements TrashProvider { public AbstractTrash getTrash() { return new WindowsTrash(); } /** * Returns true if the Windows Trash can be used on the current runtime environment. * * @return true if the Windows Trash can be used on the current runtime environment. */ public static boolean isAvailable() { return OsFamily.WINDOWS.isCurrent() && Shell32.isAvailable(); } } ================================================ FILE: src/main/java/com/mucommander/desktop/xfce/GuessedXfceDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.xfce; import com.mucommander.process.ProcessRunner; /** * 'Guessed' desktop adapter for Xfce. The availability of this desktop depends on the presence of the * exo-open command. * * @author Arik Hadas */ public class GuessedXfceDesktopAdapter extends XfceDesktopAdapter { public String toString() {return "Xfce Desktop (guessed)";} @Override public boolean isAvailable() { try { ProcessRunner.execute("exo-open"); return true; } catch(Exception e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/desktop/xfce/XfceDesktopAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.xfce; import com.mucommander.command.Command; import com.mucommander.command.CommandException; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.desktop.DefaultDesktopAdapter; import com.mucommander.desktop.DesktopInitializationException; import com.mucommander.desktop.DesktopManager; /** * @author Arik Hadas */ abstract class XfceDesktopAdapter extends DefaultDesktopAdapter { private static final String FILE_MANAGER_NAME = "Thunar"; private static final String FILE_OPENER = "exo-open $f"; private static final String EXE_OPENER = "$f"; @Override public void init(boolean install) throws DesktopInitializationException { // Initialises trash management. DesktopManager.setTrashProvider(new XfceTrashProvider()); // Registers KDE specific commands. try { CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_OPENER_ALIAS, FILE_OPENER, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.URL_OPENER_ALIAS, FILE_OPENER, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.EXE_OPENER_ALIAS, EXE_OPENER, CommandType.SYSTEM_COMMAND, null, null)); CommandManager.registerDefaultCommand(new Command(CommandManager.FILE_MANAGER_ALIAS, FILE_OPENER, CommandType.SYSTEM_COMMAND, FILE_MANAGER_NAME, null)); } catch(CommandException e) {throw new DesktopInitializationException(e);} } } ================================================ FILE: src/main/java/com/mucommander/desktop/xfce/XfceTrash.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.xfce; import java.io.IOException; import java.io.OutputStreamWriter; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.QueuedTrash; import com.mucommander.job.DeleteJob; import com.mucommander.process.ProcessRunner; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; /** * This class provides access to the Xfce trash. * *

    * Implementation notes:
    *
    * This trash is implemented as a {@link com.mucommander.desktop.QueuedTrash} as it spawns a process to move a file to * the trash and it is thus more effective to group files to be moved instead of spawning multiple processes.
    * * TODO: combine this trash and gnome trash to "freedesktop" trash * * @see XfceTrashProvider * @author Arik Hadas */ public class XfceTrash extends QueuedTrash { private static final Logger LOGGER = LoggerFactory.getLogger(XfceTrash.class); /** Open trash folder in Thunar */ private final static String REVEAL_TRASH_COMMAND = "thunar trash:///"; /** * User trash folder, as defined by the freedesktop specification (see http://freedesktop.org/wiki/Specifications/trash-spec) * null if there is no usable trash folder. */ private final static AbstractFile TRASH_FOLDER; /** "info" subfolder of the user trash folder */ private final static AbstractFile TRASH_INFO_SUBFOLDER; /** "files" subfolder of the user trash folder */ private final static AbstractFile TRASH_FILES_SUBFOLDER; /** Volume on which the trash folder resides, used for checking whether a file can be moved to the trash or not */ private final static AbstractFile TRASH_VOLUME; /** Formats dates in trash info files */ private final static SimpleDateFormat INFO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); static { TRASH_FOLDER = getTrashFolder(); if(TRASH_FOLDER!=null) { TRASH_INFO_SUBFOLDER = TRASH_FOLDER.getChildSilently("info"); TRASH_FILES_SUBFOLDER = TRASH_FOLDER.getChildSilently("files"); TRASH_VOLUME = TRASH_FOLDER.getVolume(); } else { TRASH_INFO_SUBFOLDER = null; TRASH_FILES_SUBFOLDER = null; TRASH_VOLUME = null; } } /** * Tries to find an existing user Trash folder and returns it. If no existing * Trash folder was found, creates the standard Xfce user Trash folder and returns it. * * @return the user Trash folder, null if no user trash folder could be found or created */ private static AbstractFile getTrashFolder() { AbstractFile userHome = LocalFile.getUserHome(); AbstractFile trashDir = userHome.getChildSilently(".local/share/Trash/"); if (isTrashFolder(trashDir)) { return trashDir; } // No existing user trash was found: create the folder, only if it doesn't already exist. if (!trashDir.exists()) { try { trashDir.mkdirs(); trashDir.getChild("info").mkdir(); trashDir.getChild("files").mkdir(); return trashDir; } catch(IOException e) { // Will return null } } return null; } /** * Return true if the specified file is a Xfce Trash folder, i.e. is a directory and has two * subdirectories named "info" and "files". * * @param file the file to test * @return true if the specified file is a Xfce Trash folder */ private static boolean isTrashFolder(AbstractFile file) { try { return file.isDirectory() && file.getChild("info").isDirectory() && file.getChild("files").isDirectory(); } catch(IOException e) { return false; } } /** * Implementation of {@link com.mucommander.desktop.QueuedTrash} moveToTrash method. *

    * Try to copy a collection of files to the Xfce's Trash. * * @param queuedFiles Collection of files to the trash * @return true if movement has been successful or false otherwise */ @Override protected boolean moveToTrash(List queuedFiles) { boolean retVal = true; // overall return value (if everything went OK or at least one file wasn't moved properly for (AbstractFile fileToDelete : queuedFiles) { String fileInfoContent; String trashFileName; // generate content of info file and new filename try { fileInfoContent = getFileInfoContent(fileToDelete); trashFileName = getUniqueFilename(fileToDelete); } catch (IOException ex) { LOGGER.debug("Failed to create filename for new trash item: " + fileToDelete.getName(), ex); // continue with other file (do not move file, because info file cannot be properly created continue; } AbstractFile infoFile = null; OutputStreamWriter infoWriter = null; try { // create info file infoFile = TRASH_INFO_SUBFOLDER.getChild(trashFileName + ".trashinfo"); infoWriter = new OutputStreamWriter(infoFile.getOutputStream()); infoWriter.write(fileInfoContent); } catch (IOException ex) { retVal = false; LOGGER.debug("Failed to create trash info file: " + trashFileName, ex); // continue with other file (do not move file, because info file wasn't properly created) continue; } finally { if (infoWriter != null) { try { infoWriter.close(); } catch (IOException e) { // Not much else to do } } } try { // rename original file fileToDelete.renameTo(TRASH_FILES_SUBFOLDER.getChild(trashFileName)); } catch (IOException ex) { try { // remove info file infoFile.delete(); } catch (IOException ex1) { // simply ignore } retVal = false; LOGGER.debug("Failed to move file to trash: " + trashFileName, ex); } } return retVal; } /** * Implementation notes: returns true only for local files that are not archive entries and that * reside on the same volume as the trash folder. */ @Override public boolean canMoveToTrash(AbstractFile file) { return TRASH_FOLDER != null && file.getTopAncestor() instanceof LocalFile && file.getVolume().equals(TRASH_VOLUME); } /** * Implementation notes: always returns true. * * @return True if trash can be emptied, otherwise false */ @Override public boolean canEmpty() { return TRASH_FOLDER!=null; } /** * Empty the trash *

    * Implementation notes:
    * Simply free the TRASH_PATH directory * * @return True if everything went well */ @Override public boolean empty() { // Abort if there is no usable trash folder if (TRASH_FOLDER == null) { return false; } FileSet filesToDelete = new FileSet(TRASH_FOLDER); try { // delete real files filesToDelete.addAll(TRASH_FILES_SUBFOLDER.ls()); // delete spec files filesToDelete.addAll(TRASH_INFO_SUBFOLDER.ls()); } catch (java.io.IOException ex) { LOGGER.debug("Failed to list files", ex); return false; } if (filesToDelete.size() > 0) { // Starts deleting files MainFrame mainFrame = WindowManager.getCurrentMainFrame(); ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("delete_dialog.deleting")); DeleteJob deleteJob = new DeleteJob(progressDialog, mainFrame, filesToDelete, false); progressDialog.start(deleteJob); } return true; } @Override public boolean isTrashFile(AbstractFile file) { return TRASH_FOLDER != null && (file.getTopAncestor() instanceof LocalFile) && TRASH_FOLDER.isParentOf(file); } /** * Return trash files count *

    * We assume the count of items in trash equals the count of files in * TRASH_PATH + "/info" folder. * * @return Count of files in trash */ @Override public int getItemCount() { // Abort if there is no usable trash folder if (TRASH_FOLDER == null) { return -1; } try { return TRASH_INFO_SUBFOLDER.ls().length; } catch (java.io.IOException ex) { // can't access trash folder return -1; } } /** * Opens the trash in Thunar. */ @Override public void open() { try { ProcessRunner.execute(REVEAL_TRASH_COMMAND).waitFor(); } catch(Exception e) { // IOException, InterruptedException LOGGER.debug("Caught an exception running command \"" + REVEAL_TRASH_COMMAND + "\"", e); } } @Override public boolean canOpen() { return TRASH_FOLDER!=null; } /** * Make a content of .trashinfo file * @param file File for which the content is built * @return Final content */ private String getFileInfoContent(AbstractFile file) { synchronized(INFO_DATE_FORMAT) { // SimpleDateFormat is not thread safe return "[Trash Info]\n" + "Path=" + file.getAbsolutePath() + "\n" + "DeletionDate=" + INFO_DATE_FORMAT.format(new Date()); } } /** * It is possible to add several files with same name to the Trash. These files are distinguished * by _N appended to the name, where _N is rising int number.
    * This method tries to find first empty filename_N.ext. * * @param file File to be deleted * @return Suitable filename in trash (without .trashinfo extension) */ private String getUniqueFilename(AbstractFile file) throws IOException { // try if no previous file in trash exists if (!TRASH_FILES_SUBFOLDER.getChild(file.getName()).exists()) { return file.getName(); } String rawName = file.getNameWithoutExtension(); String extension = file.getExtension(); // find first empty filename in format filename_N.ext int count = 1; while (true) { String filename = rawName + "_" + count++; if (extension != null) { filename += "." + extension; } if (!TRASH_FILES_SUBFOLDER.getChild(filename).exists()) { return filename; } } } } ================================================ FILE: src/main/java/com/mucommander/desktop/xfce/XfceTrashProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.desktop.xfce; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.TrashProvider; /** * This class is a trash provider for the {@link XfceTrash Xfce trash}. * * @see XfceTrash * @author Arik Hadas */ public class XfceTrashProvider implements TrashProvider { /******************************* * TrashProvider Implementation *******************************/ public AbstractTrash getTrash() { return new XfceTrash(); } } ================================================ FILE: src/main/java/com/mucommander/extension/ClassFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.extension; /** * Used to IMAGE_FILTER classes. *

    * ClassFilter implementations are meant to be used in conjonction with {@link ClassFinder}. * * @author Nicolas Rinaudo */ public interface ClassFilter { /** * Returns true if the specified class must be used. * @param c class that must be evaluated. * @return true if the specified class must be used, false otherwise. */ boolean accept(Class c); } ================================================ FILE: src/main/java/com/mucommander/extension/ClassFinder.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.extension; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Vector; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.AbstractFileClassLoader; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.OrFileFilter; /** * Finds specific classes within a browsable file. *

    * This class will explore the content of a browsable {@link com.mucommander.commons.file.AbstractFile} and match * all discovered classes to a {@link ClassFilter}. * *

    * In order for classes to be analyzed, they need to be loaded. This can be achieved in two ways: *

      *
    • By using a custom class loader through {@link #find(AbstractFile,ClassFilter,ClassLoader)}.
    • *
    • * By using an {@link com.mucommander.commons.file.AbstractFileClassLoader} *
    * * @author Nicolas Rinaudo */ public class ClassFinder { /** ClassLoader used to load classes from explored files. */ private ClassLoader loader; /** Used to IMAGE_FILTER out files that are neither classes nor directories. */ private final OrFileFilter filter; /** Used to IMAGE_FILTER out unwanted classes. */ private ClassFilter classFilter; /** * Creates a new instance of ClassFinder. */ public ClassFinder() { filter = new OrFileFilter( new ExtensionFilenameFilter(".class"), new AttributeFileFilter(FileAttribute.DIRECTORY) ); } // - File exploring ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Explores the specified file for classes that match {@link #classFilter}. * @param currentPackage package we're currently exploring (with a trailing '.'). * @param currentFile file we're currently exploring. * @return a vector containing all the classes that were found and matched classFilter. * @throws IOException if an error occurs while exploring currentFile. */ private List> find(String currentPackage, AbstractFile currentFile) throws IOException { AbstractFile[] files = currentFile.ls(filter); // All subfolders or child class files of currentFile. List> result = new ArrayList<>(); // Analyses all subdirectories and class files. for (AbstractFile file : files) { // Explores subdirectories recursively. if (file.isDirectory()) result.addAll(find(currentPackage + file.getName() + '.', file)); // Passes each class through the class IMAGE_FILTER. // Errors are treated as 'this class is not wanted'. else { try { Class currentClass; // Buffer for the current class. if (classFilter.accept(currentClass = Class.forName(currentPackage + file.getNameWithoutExtension(), false, loader))) result.add(currentClass); } catch (Throwable ignore) { } } } return result; } /** * Explores the content of the specified file and looks for classes that match the specified class IMAGE_FILTER. *

    * The browsable argument must be browsable as defined by {@link com.mucommander.commons.file.AbstractFile#isBrowsable()}. * If such is not the case, the returned vector will be empty. * * @param browsable file in which to look for classes. * @param classFilter how to decide which classes should be kept. * @param classLoader used to load each class found in browsable. * @return a vector containing all the classes that were found and matched classFilter. * @throws IOException if an error occurs while exploring browsable. * @see #find(AbstractFile,ClassFilter) */ public List> find(AbstractFile browsable, ClassFilter classFilter, ClassLoader classLoader) throws IOException { // Ignore non-browsable files. if (!browsable.isBrowsable()) { return new Vector<>(); } // Initializes exploring. loader = classLoader; this.classFilter = classFilter; // Looks for all matched classes in browsable. return find("", browsable); } /** * Explores the content of the specified file and looks for classes that match the specified class IMAGE_FILTER. *

    * This is a convenience method and is strictly equivalent to calling {@link #find(AbstractFile,ClassFilter,ClassLoader)} * with a class loader argument initialized with the following code: *

         * AbstractFileClassLoader loader;
         *
         * loader = new AbstractFileClassLoader();
         * loader.addFile(browsable);
         * 
    * * @param browsable file in which to look for classes. * @param classFilter how to decide which classes should be kept. * @return a vector containing all the classes that were found and matched classFilter. * @throws IOException if an error occurs while exploring browsable. */ public List> find(AbstractFile browsable, ClassFilter classFilter) throws IOException { // Initializes the default class loader. AbstractFileClassLoader classLoader = new AbstractFileClassLoader(); classLoader.addFile(browsable); // Explores browsable. return find(browsable, classFilter, classLoader); } } ================================================ FILE: src/main/java/com/mucommander/extension/ExtensionManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.extension; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.AbstractFileClassLoader; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import java.io.File; import java.io.IOException; import java.util.StringTokenizer; /** * Manages muCommander's extensions. *

    * Extensions must be stored in {@link #getExtensionsFolder()} in order for this class to be aware of them. * Moreover, the method {@link #addExtensionsToClasspath()} must have been called before extensions can be used. * *

    * Extensions are loaded through a custom ClassLoader. The optimal situation is for that ClassLoader * to be the system one, which can only be achieved through setting the java.system.class.loader system property * to com.mucommander.commons.file.AbstractFileClassLoader at boot time.
    * However, if for some reason such is not the case, we'll use a separate instance of that class. This will work in most cases, but * might cause conflicts under rare circumstances. Extension writers are advised to load resources through the ClassLoader * returned by {@link #getClassLoader()}, as not doing so might result in using the bootstrap classloader which doesn't have access to * resources found in {@link #getExtensionsFolder()}. * *

    * This class can also be used to load Swing look and feel from JAR files that aren't in the system's classpath. In order to achieve this, * application writers must: *

      *
    • * Call UIManager.getDefaults().put("ClassLoader", ExtensionManager.getClassLoader()); when initializing their application. * This will force Swing to use our custom classloader when loading Look&Feels. *
    • *
    • * Call UIManager.setLookAndFeel((LookAndFeel)Class.forName(lnfName, true, ExtensionManager.getClassLoader()).newInstance()); * to set a new look and feel. This will ensure that all classes and resources are available when initializing the Look&Feel. *
    • *
    *

    * Unfortunately, this is not always sufficient. Some Look&Feels suffer from a peculiar behavior in Swing that might cause resources to be loaded * through the system class loader rather than the one specified at initialization time. This happens with Look&Feels that extend system ones, such * as Quaqua. The only way to get these to load properly is to make sure the system classloader is an instance of * {@link com.mucommander.commons.file.AbstractFileClassLoader}. * * @author Nicolas Rinaudo */ public class ExtensionManager { /** ClassLoader used to load all extensions. */ private static AbstractFileClassLoader loader; /** Path to the extensions folder. */ private static AbstractFile extensionsFolder; /** Default name of the extensions folder. */ private static final String DEFAULT_EXTENSIONS_FOLDER_NAME = "extensions"; public static void init() { ClassLoader temp = ClassLoader.getSystemClassLoader(); // Initializes the extension class loader. if (temp instanceof AbstractFileClassLoader) { // If the system classloader is an instance of AbstractFileClassLoader, use it. loader = (AbstractFileClassLoader) temp; } else { // Otherwise, use a new instance of AbstractFileClassLoader. loader = new AbstractFileClassLoader(ExtensionManager.class.getClassLoader()); } // // UIManager.put("ClassLoader", loader); } /** * Prevents instantiations of this class. */ private ExtensionManager() {} /** * Sets the path to the folder in which all extensions are stored. *

    * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent * will be used instead. * * @param folder path to the folder in which extensions are stored. * @throws IOException if the specified folder or the specified file's parent couldn't be accessed. * @see #setExtensionsFolder(AbstractFile) * @see #setExtensionsFolder(String) * @see #getExtensionsFolder() */ private static void setExtensionsFolder(File folder) throws IOException { AbstractFile file = FileFactory.getFile(folder.getAbsolutePath()); setExtensionsFolder(file); } /** * Sets the path to the folder in which all extensions are stored. *

    * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent * will be used instead. * * @param folder path to the folder in which extensions are stored. * @throws IOException if the specified folder or the specified file's parent couldn't be accessed. * @see #setExtensionsFolder(File) * @see #setExtensionsFolder(String) * @see #getExtensionsFolder() */ private static void setExtensionsFolder(AbstractFile folder) throws IOException { // If the folder doesn't exist, create it. if (!folder.exists()) { folder.mkdir(); } else if (!folder.isBrowsable()) { // If it's not a browsable file, use its parent. folder = folder.getParent(); } extensionsFolder = folder; } /** * Sets the path to the folder in which all extensions are stored. *

    * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent * will be used instead. * * @param path path to the folder in which extensions are stored. * @throws IOException if the specified folder or the specified file's parent couldn't be accessed. * @see #setExtensionsFolder(File) * @see #setExtensionsFolder(String) * @see #getExtensionsFolder() */ public static void setExtensionsFolder(String path) throws IOException { AbstractFile folder = FileFactory.getFile(path); if (folder == null) { setExtensionsFolder(new File(path)); } else { setExtensionsFolder(folder); } } /** * Returns the path to the default extensions folder. *

    * The default path is: *

         * {@link PlatformManager#getPreferencesFolder()}.{@link AbstractFile#getChild(String) getChild}({@link #DEFAULT_EXTENSIONS_FOLDER_NAME});
         * 
    * * @return the path to the default extensions folder. * @throws IOException if there was an error retrieving the default extensions folder. */ private static AbstractFile getDefaultExtensionsFolder() throws IOException { AbstractFile folder = PlatformManager.getPreferencesFolder().getChild(DEFAULT_EXTENSIONS_FOLDER_NAME); // Makes sure the folder exists. if (!folder.exists()) { folder.mkdir(); } return folder; } /** * Returns the folder in which all extensions are stored. * @return the folder in which all extensions are stored. * @throws IOException if an error occured while locating the default extensions folder. * @see #setExtensionsFolder(AbstractFile) */ private static AbstractFile getExtensionsFolder() throws IOException { // If the extensions folder has been set, use it. if (extensionsFolder != null) { return extensionsFolder; } return getDefaultExtensionsFolder(); } /** * Returns an AbstractFile to the extension file with the specified filename and located in the * {@link #getExtensionsFolder() extensions folder}. The returned file may or may not exist. * @param filename the extension's filename * @return an AbstractFile to the extension file with the specified filename and located in the * extensions folder. * @throws IOException if the file could not be instantiated. */ public static AbstractFile getExtensionsFile(String filename) throws IOException { return getExtensionsFolder().getDirectChild(filename); } // - Classpath querying ----------------------------------------------------- // -------------------------------------------------------------------------- /** * Returns true if the specified file is in the extension's classloader path. * @param file file whose presence in the extensions path will be checked. * @return true if the specified file is in the extension's classloader path, false otherwise. */ private static boolean isInExtensionsPath(AbstractFile file) {return loader.contains(file);} /** * Returns true if the specified file is in the system classpath. * @param file file whose presence in the system classpath will be checked. * @return true if the specified file is in the system classpath, false otherwise. */ private static boolean isInClasspath(AbstractFile file) { String path = file.getAbsolutePath(); StringTokenizer parser = new StringTokenizer(System.getProperty("java.class.path"), System.getProperty("path.separator")); while (parser.hasMoreTokens()) { if (parser.nextToken().equals(path)) { return true; } } return false; } /** * Returns true if the specified file is either in the extension or system classpath. *

    * This is a convenience method and is equivalent to calling: * {@link #isInClasspath(AbstractFile) isInClasspath}(file) || {@link #isInExtensionsPath(AbstractFile) isInExtensionsPath}(file). * * @param file file whose availability will be checked. * @return true if the specified file is either in the extension or system classpath, false otherwise. */ public static boolean isAvailable(AbstractFile file) {return isInClasspath(file) || isInExtensionsPath(file);} // - Classpath extension ---------------------------------------------------- // -------------------------------------------------------------------------- /** * Imports the specified file in muCommander's libraries. * @param file path to the library to import. * @param force wether to overwrite eventual existing libraries of the same name. * @return true if the operation was a success, * false if a library of the same name already exists and * force is set to false. * @throws IOException if an I/O error occurs. */ public static boolean importLibrary(AbstractFile file, boolean force) throws IOException { // If the file is already in the extensions or classpath, // there's nothing to do. if (isAvailable(file)) { return true; } // If the destination file already exists, either delete it // if force is set to true or just return false. AbstractFile dest = getExtensionsFile(file.getName()); if (dest.exists()) { if (!force) { return false; } dest.delete(); } // Copies the library and adds it to the extensions classpath. file.copyTo(dest); addToClassPath(dest); return true; } /** * Adds the specified file to the extension's classpath. * @param file file to add to the classpath. */ private static void addToClassPath(AbstractFile file) {loader.addFile(file);} /** * Adds all known extensions to the current classpath. *

    * This method will create the following new classpath entries: *

      *
    • {@link #getExtensionsFolder()}.
    • *
    • All JAR files in {@link #getExtensionsFolder()}.
    • *
    * * @throws IOException if the extensions folder is not accessible. */ public static void addExtensionsToClasspath() throws IOException { // Adds the extensions folder to the classpath. addToClassPath(getExtensionsFolder()); // Adds all JAR files contained by the extensions folder to the classpath. AbstractFile[] files = getExtensionsFolder().ls(new ExtensionFilenameFilter(".jar")); for (AbstractFile file : files) { addToClassPath(file); } } /** * Returns the ClassLoader used to load all extensions. * @return the ClassLoader used to load all extensions. */ public static ClassLoader getClassLoader() { return loader; } } ================================================ FILE: src/main/java/com/mucommander/extension/LookAndFeelFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.extension; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import javax.swing.LookAndFeel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class IMAGE_FILTER for look and feels. *

    * This IMAGE_FILTER will only accept classes if: *

      *
    • They subclass javax.swing.LookAndFeel.
    • *
    • They are public and not abstract.
    • *
    • They have a public, no-arg constructor.
    • *
    • Their isSupportedLookAndFeel method returns true.
    • *
    • They are not an inner class.
    • *
    * * @author Nicolas Rinaudo */ public class LookAndFeelFilter implements ClassFilter { private static final Logger LOGGER = LoggerFactory.getLogger(LookAndFeelFilter.class); /** * Creates a new instance of LookAndFeelFilter. */ public LookAndFeelFilter() {} /** * Filters out everything but available look and feels. * @param c class to check. * @return true if c is an available look and feel, false otherwise. */ public boolean accept(Class c) { // Ignores inner classes. if (c.getDeclaringClass() != null) { return false; } return isPublicAndNotAbstract(c) && hasPublicDefaultConstructor(c) && isAvailableLookAndFeel(c); } private static boolean isPublicAndNotAbstract(Class c) { // Makes sure the class is public and non-abstract. int modifiers = c.getModifiers(); return Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers); } private static boolean hasPublicDefaultConstructor(Class c) { // Makes sure the class has a public, no-arg constructor. try { Constructor constructor = c.getDeclaredConstructor(); return Modifier.isPublic(constructor.getModifiers()); } catch(Exception e) { return false; } } private static boolean isAvailableLookAndFeel(Class c) { // Makes sure the class extends javax.swing.LookAndFeel and that if it does, // it's supported by the system. Class buffer = c; while (buffer != null) { // c is a LookAndFeel, makes sure it's supported. if (buffer.equals(LookAndFeel.class)) { return isSupportedLookAndFeel(c); } buffer = buffer.getSuperclass(); } return false; } private static boolean isSupportedLookAndFeel(Class c) { try { return ((LookAndFeel) c.getDeclaredConstructor().newInstance()).isSupportedLookAndFeel(); } catch(Throwable e) { LOGGER.debug("Class {} caught exception", c, e); return false; } } } ================================================ FILE: src/main/java/com/mucommander/extension/package.html ================================================ Provides extension mechanisms. ================================================ FILE: src/main/java/com/mucommander/io/backup/BackupConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.io.backup; /** * Defines various constants common to backup classes. * @author Nicolas Rinaudo */ interface BackupConstants { /** Character to add suffix file names with in order to mark them as backup. */ char BACKUP_SUFFIX = '~'; } ================================================ FILE: src/main/java/com/mucommander/io/backup/BackupInputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.io.backup; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import java.io.*; /** * Opens an input stream on a file that has been saved by {@link BackupOutputStream}. *

    * This class' role is to choose which of the original or backup file should be read in order to ensure * that the data is not corrupt. * * @see BackupOutputStream * @author Nicolas Rinaudo */ public class BackupInputStream extends FilterInputStream implements BackupConstants { /** * Opens a backup input stream on the specified file. * @param file file to open for reading. * @exception IOException thrown if any IO related error occurs. */ public BackupInputStream(File file) throws IOException { super(getInputStream(file)); } /** * Opens a backup input stream on the specified file. * @param path path to the file to open for reading. * @exception IOException thrown if any IO related error occurs. */ public BackupInputStream(String path) throws IOException { super(getInputStream((new File(path)))); } /** * Opens a backup input stream on the specified file. * @param file file to open for reading. * @exception IOException thrown if any IO related error occurs. */ public BackupInputStream(AbstractFile file) throws IOException { super(getInputStream(file)); } /** * Opens a stream on the right file. *

    * If a backup file is found, and is bigger than the target file, then it will be used. * * @param file file on which to open an input stream. * @return a stream on the right file. * @exception IOException thrown if any IO related error occurs. */ private static InputStream getInputStream(AbstractFile file) throws IOException { FileURL test = (FileURL)file.getURL().clone(); test.setPath(test.getPath() + BACKUP_SUFFIX); // Checks whether the backup file is a better choice than the target one. AbstractFile backup = FileFactory.getFile(test); if (backup != null && backup.exists() && (file.getSize() < backup.getSize())) { return backup.getInputStream(); } // Opens a stream on the target file. return file.getInputStream(); } /** * Opens a stream on the right file. *

    * If a backup file is found, and is bigger than the target file, then it will be used. * * @param file file on which to open an input stream. * @return a stream on the right file. * @exception IOException thrown if any IO related error occurs. */ private static InputStream getInputStream(File file) throws IOException { File backup = new File(file.getAbsolutePath() + BACKUP_SUFFIX); if (backup.exists() && (file.length() < backup.length())) { return new FileInputStream(backup); } // Opens a stream on the target file. return new FileInputStream(file); } } ================================================ FILE: src/main/java/com/mucommander/io/backup/BackupOutputStream.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.io.backup; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; import java.io.OutputStream; /** * Saves file in as crash-safe a manner as possible. *

    * In order to prevent system or muCommander failures to corrupt configuration files, * the BackupOutputStream implements the following algorithm: *

      *
    • Write its content to a backup file instead of the requested file
    • *
    • When close is called, copy the content of the backup file over the original file
    • *
    * This way, if a crash was to happen while configuration files are being saved, either of the * following will happen: *
      *
    • * The backup file is not properly saved, but the original configuration is left untouched. * We have lost some information (modifications since last save) but not all. *
    • *
    • * The original file is not properly saved, but the backup file is correct. This is easy to check, * as the backup and original file should always have the same size. If they don't, then the backup * file should be used rather than the original one. *
    • *
    *

    * Files that have been saved by this class should be read with {@link BackupInputStream} * in order to make sure that an uncorrupt version of them is loaded. *

    * The BackupOutputStream monitors all of its own I/O operations. If an error occurs, then the backup * operation will not be performed when {@link #close()} is called. It's possible to force the backup operation by * using the {@link #close(boolean)} method. * * @see BackupInputStream * @author Nicolas Rinaudo */ public class BackupOutputStream extends OutputStream implements BackupConstants { /** The underlying OutputStream */ private final OutputStream out; /** Path of the original file. */ private final AbstractFile target; /** Path to the backup file. */ private final AbstractFile backup; /** Whether, or not an error occurred while writing to the backup file. */ private boolean error; /** * Opens a backup output stream on the specified file. * @param file file on which to open a backup output stream. * @exception IOException thrown if any IO error occurs. */ public BackupOutputStream(File file) throws IOException { this(FileFactory.getFile(file.getAbsolutePath())); } /** * Opens a backup output stream on the specified file. * @param file file on which to open a backup output stream. * @exception IOException thrown if any IO error occurs. */ public BackupOutputStream(String file) throws IOException { this(FileFactory.getFile((new File(file)).getAbsolutePath())); } /** * Opens a backup output stream on the specified file. * @param file file on which to open a backup output stream. * @exception IOException thrown if any IO error occurs. */ public BackupOutputStream(AbstractFile file) throws IOException { this(file, FileFactory.getFile(file.getAbsolutePath() + BACKUP_SUFFIX)); } /** * Opens an output stream on the specified file using the specified backup file. * @param file file on which to open the backup output stream. * @param save file that will be used for backup. * @exception IOException thrown if any IO error occurs. */ private BackupOutputStream(AbstractFile file, AbstractFile save) throws IOException { out = save.getOutputStream(); target = file; backup = save; } // - Error catching --------------------------------------------------------- // -------------------------------------------------------------------------- /** * Flushes this output stream and forces any buffered output bytes to be written out to the stream. *

    * This method calls the flush() method of its underlying output stream. *

    * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be * forced through the {@link #close(boolean)} method. * * @throws IOException if an I/O error occurs. */ @Override public void flush() throws IOException { if(error) out.flush(); else { try {out.flush();} catch(IOException e) { error = true; throw e; } } } /** * Writes b.length bytes to this output stream. *

    * This method calls the write(byte[] b) method of its underlying output stream. *

    * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be * forced through the {@link #close(boolean)} method. * * @param b the data to be written. * @throws IOException if an I/O error occurs. */ @Override public void write(@NotNull byte[] b) throws IOException { if (error) { out.write(b); } else { try { out.write(b); } catch(IOException e) { error = true; throw e; } } } /** * Writes len bytes from the specified byte array starting at offset off to this output stream. *

    * This method calls the write(byte[] b, int off, int len) method of its underlying output stream. *

    * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be * forced through the {@link #close(boolean)} method. * * @param b the data to be written. * @param off the start offset in the data. * @param len the number of bytes to write. * @throws IOException if an I/O error occurs. */ @Override public void write(@NotNull byte[] b, int off, int len) throws IOException { if (error) { out.write(b, off, len); } else { try { out.write(b, off, len); } catch(IOException e) { error = true; throw e; } } } /** * Writes the specified byte to this output stream. *

    * This method calls the write(byte b) method of its underlying output stream. *

    * If an error occurs at this point, the {@link #close()} method will not overwrite the target file. This can be * forced through the {@link #close(boolean)} method. * * @param b the data to be written. * @throws IOException if an I/O error occurs. */ @Override public void write(int b) throws IOException { if (error) { out.write(b); } else { try { out.write(b); } catch(IOException e) { error = true; throw e; } } } // - Backup ----------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Overwrites the target file with the backup one. * @exception IOException thrown if any IO related error occurs. */ private void backup() throws IOException { // Deletes the destination file (AbstractFile.copyTo now fails when the destination exists). if (target.exists()) { target.delete(); } // We're not using backup.moveTo(target) because we want to make absolutely sure // that if an error occurs in the middle of the operation, at least one of the two files // is complete. backup.copyTo(target); backup.delete(); } /** * Finishes the backup operation. * @exception IOException thrown if any IO related error occurs. */ @Override public void close() throws IOException {close(!error);} /** * Closes the output stream. *

    * The backup parameter is meant for those cases when an error happened * while writing to the stream: if it did, we don't want to propagate to the target * file, and thus should prevent the backup operation from being performed. * * @param backup whether to overwrite the target file by the backup one. * @exception IOException thrown if any IO related error occurs. */ public void close(boolean backup) throws IOException { // Closes the underlying output stream. out.flush(); out.close(); if (backup) { backup(); } } } ================================================ FILE: src/main/java/com/mucommander/io/package.html ================================================ This package contains I/O classes that are application-specific, and therefore not in com.mucommander.commons.io. ================================================ FILE: src/main/java/com/mucommander/job/AbstractCopyJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.AbstractRWArchiveFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.FileCollisionRenameDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import java.io.IOException; /** * This class is the parent class of {@link com.mucommander.job.CopyJob} and {@link com.mucommander.job.MoveJob} and * allows them to share methods and fields. * * @author Maxence Bernard, Mariusz Jakubowski * @see com.mucommander.job.CopyJob * @see com.mucommander.job.MoveJob */ public abstract class AbstractCopyJob extends TransferFileJob { /** Base destination folder */ AbstractFile baseDestFolder; /** New filename in destination */ protected String newName; /** Default choice when encountering an existing file */ private int defaultFileExistsAction;// /** Title used for error dialogs */ protected String errorDialogTitle; protected boolean append; /** The archive that contains the destination files (maybe null) */ AbstractRWArchiveFile archiveToOptimize; /** True when an archive is being optimized */ boolean isOptimizingArchive; /** * Creates a new AbstractCopyJob. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be copied * @param destFolder destination folder where the files will be copied * @param newName the new filename in the destination folder, can be null in which case the original filename will be used. * @param fileExistsAction default action to be performed when a file already exists in the destination, see {@link com.mucommander.ui.dialog.file.FileCollisionDialog} for allowed values */ AbstractCopyJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFolder, String newName, int fileExistsAction) { super(progressDialog, mainFrame, files); this.baseDestFolder = destFolder; this.newName = newName; this.defaultFileExistsAction = fileExistsAction; } /** * Creates a destination file given a destination folder and a new file name. * @param destFolder a destination folder * @param destFileName a destination file name * @return the destination file or null if it cannot be created */ AbstractFile createDestinationFile(AbstractFile destFolder, String destFileName) { do { // Loop for retry try { return destFolder.getDirectChild(destFileName); } catch (IOException e) { // Destination file couldn't be instantiated int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_write_file", destFileName)); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel or close dialog return false return null; // Skip continues } } while(true); } /** * Checks if there is a file collision (file exists in the destination). * If there is no collision this method returns destFile. * If there is a collision this method returns:

      *
    • null if a user canceled the transfer *
    • null if a user skipped the file *
    • destFile if a user resumed the transfer (and sets append flag) *
    • destFile if a user has chosen to overwrite the file *
    • new file if a user renamed the file *
    * @param file a source file * @param destFolder a destination folder * @param destFile a destination file * @param allowCaseVariation if true, * @return destFile the new destination file */ protected AbstractFile checkForCollision(AbstractFile file, AbstractFile destFolder, AbstractFile destFile, boolean allowCaseVariation) { append = false; while (true) { // Check for file collisions (file exists in the destination, destination subfolder of source, ...) // if a default action hasn't been specified int collision = FileCollisionChecker.checkForCollision(file, destFile); // If allowCaseVariation is true and both files are equal, test if the destination filename is a variation // of the original filename with a different case. If that is the case, do not warn about the source and // destination being the same. if (allowCaseVariation && collision==FileCollisionChecker.SAME_SOURCE_AND_DESTINATION) { String sourceFileName = file.getName(); String destFileName = destFile.getName(); if(sourceFileName.equalsIgnoreCase(destFileName) && !sourceFileName.equals(destFileName)) break; } // Handle collision, asking the user what to do or using a default action to resolve the collision if (collision != FileCollisionChecker.NO_COLLISION) { int choice; // Use default action if one has been set, if not show up a dialog if (defaultFileExistsAction==FileCollisionDialog.ASK_ACTION) { FileCollisionDialog dialog = new FileCollisionDialog(getProgressDialog(), getMainFrame().getJFrame(), collision, file, destFile, true, true); choice = waitForUserResponse(dialog); // If 'apply to all' was selected, this choice will be used for any other files (user will not be asked again) if (dialog.applyToAllSelected()) defaultFileExistsAction = choice; } else { choice = defaultFileExistsAction; } // Cancel, skip or close dialog if (choice == -1 || choice == FileCollisionDialog.CANCEL_ACTION) { interrupt(); return null; } // Skip file else if (choice == FileCollisionDialog.SKIP_ACTION) { return null; } // Append to file (resume file copy) else if (choice == FileCollisionDialog.RESUME_ACTION) { append = true; break; } // Overwrite file else if (choice== FileCollisionDialog.OVERWRITE_ACTION) { // Do nothing, simply continue break; } // Overwrite file if destination is older else if (choice == FileCollisionDialog.OVERWRITE_IF_OLDER_ACTION) { // Overwrite if file is newer (strictly) if (file.getLastModifiedDate() <= destFile.getLastModifiedDate()) return null; break; } else if (choice == FileCollisionDialog.RENAME_ACTION) { setPaused(true); FileCollisionRenameDialog dlg = new FileCollisionRenameDialog(getMainFrame(), destFile); String destFileName = (String) waitForUserResponseObject(dlg); setPaused(false); if (destFileName != null) { destFile = createDestinationFile(destFolder, destFileName); } else { // turn on FileCollisionDialog, so we don't loop indefinitely defaultFileExistsAction = FileCollisionDialog.ASK_ACTION; } // continue with collision checking continue; } } break; // no collision } return destFile; } /** * Optimizes the given writable archive file and notifies the user in case of an error. * * @param rwArchiveFile the writable archive file to optimize */ void optimizeArchive(AbstractRWArchiveFile rwArchiveFile) { isOptimizingArchive = true; while (true) { try { archiveToOptimize = rwArchiveFile; archiveToOptimize.optimizeArchive(); break; } catch (IOException e) { if(showErrorDialog(errorDialogTitle, Translator.get("error_while_optimizing_archive", rwArchiveFile.getName()))==RETRY_ACTION) continue; break; } } isOptimizingArchive = false; } } ================================================ FILE: src/main/java/com/mucommander/job/ArchiveJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import java.io.InputStream; import com.mucommander.commons.file.archiver.ArchiveFormat; import com.mucommander.job.utils.ScanDirectoryThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.archiver.Archiver; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.io.StreamUtils; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This FileJob is responsible for compressing a set of files into an archive file. * * @author Maxence Bernard */ public class ArchiveJob extends TransferFileJob { private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveJob.class); /** Destination archive file */ private final AbstractFile destFile; /** Base destination folder's path */ private final String baseFolderPath; /** Archiver instance that does the actual archiving */ private Archiver archiver; /** Archive format */ private final ArchiveFormat archiveFormat; /** Optional archive comment */ private final String archiveComment; /** Lock to avoid Archiver.close() to be called while data is being written */ private final Object ioLock = new Object(); private final ScanDirectoryThread scanDirectoryThread; /** Processed files counter */ private long processedFilesCount; public ArchiveJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFile, ArchiveFormat archiveFormat, String archiveComment) { super(progressDialog, mainFrame, files); this.destFile = destFile; this.archiveFormat = archiveFormat; this.archiveComment = archiveComment; this.baseFolderPath = getBaseSourceFolder().getAbsolutePath(false); scanDirectoryThread = new ScanDirectoryThread(files); scanDirectoryThread.start(); } @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if (getState() == State.INTERRUPTED) { return false; } String filePath = file.getAbsolutePath(false); String entryRelativePath = filePath.substring(baseFolderPath.length()+1); // Process current file do { // Loop for retry try { if (file.isDirectory() && !file.isSymlink()) { // create new directory entry in archive file archiver.createEntry(entryRelativePath, file); // Recurse on files AbstractFile[] subFiles = file.ls(); boolean folderComplete = true; for (int i=0; i 5 ? 5 : result; } float progressBySize = 1.0f*(getTotalByteCounter().getByteCount() + getTotalSkippedByteCounter().getByteCount()) / scanDirectoryThread.getTotalBytes(); float progressByCount = 1.0f*(processedFilesCount-1) / scanDirectoryThread.getFilesCount(); float result = (progressBySize * 8 + progressByCount * 2) / 10; if (result < 0) { result = 0; } else if (result > 1) { result = 1; } return result; } } ================================================ FILE: src/main/java/com/mucommander/job/BatchRenameJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import java.util.List; /** * This job renames a group of files to new names defined by Batch-Rename Dialog. * @author Mariusz Jakubowski */ public class BatchRenameJob extends MoveJob { private List newNames; public BatchRenameJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, List newNames) { super(progressDialog, mainFrame, files, files.getBaseFolder(), null, FileCollisionDialog.ASK_ACTION, true); this.newNames = newNames; } //////////////////////////// // FileJob implementation // //////////////////////////// @Override protected boolean processFile(AbstractFile file, Object recurseParams) { this.newName = newNames.get(getCurrentFileIndex()); return super.processFile(file, recurseParams); } } ================================================ FILE: src/main/java/com/mucommander/job/CalculateChecksumJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.viewer.ViewerRegistrar; /** * This job calculates a checksum for a list of files and stores the results in a checksum file. * *

    The format of this file is a de facto standard ; a line is created for each file and goes like this: *

     * e7e9576b9e55940b4b8522a65902d4cd  readme.txt
     * 119abda7c941135d5bf382c386bca2ca  i386/debian-40r1-i386-DVD-1.iso
     * 3c0d332902b9b8dfec43ba02d1618c6e  ppc/debian-40r1-ppc-DVD-1.iso
     * ...
     * 
    * The path of each file is relative to the checksum file's path. In the above example, readme.txt and * the checksum file are located in the same folder. Note that 2 space characters (and not just one as anyone in his * right mind would think) separate the hexadecimal checksum from the file path. * *

    The above file format is used for all checksum algorithms but one: CRC32, which uses the special SFV format where * the checksum for each file is written as follow: *

     * wne-ebai.r00 697115b2
     * wne-ebai.r01 f80a8443
     * ...
     * 
    * * @author Maxence Bernard */ public class CalculateChecksumJob extends TransferFileJob { private static final Logger LOGGER = LoggerFactory.getLogger(CalculateChecksumJob.class); /** The checksum file where the checksum of each file is written */ private AbstractFile checksumFile; /** The OutputStream of the checksum file */ private OutputStream checksumFileOut; /** The path to the base source folder, i.e. the folder which contains all the files this job operates on */ private String baseSourcePath; /** True if the SFV format is used rather than the default 'SUMS' format */ private boolean useSfvFormat; /** The MessageDigest that serves to calculate the checksum */ private MessageDigest digest; public CalculateChecksumJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile checksumFile, MessageDigest digest) { super(progressDialog, mainFrame, files); this.checksumFile = checksumFile; this.digest = digest; this.useSfvFormat = digest.getAlgorithm().equalsIgnoreCase("CRC32"); this.baseSourcePath = getBaseSourceFolder().getAbsolutePath(true); } //////////////////////////////////// // TransferFileJob implementation // //////////////////////////////////// @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Skip directories if(file.isDirectory()) { do { // Loop for retry try { // for each file in folder... AbstractFile children[] = file.ls(); for (int i=0; i. */ package com.mucommander.job; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.util.FileSet; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * @author Maxence Bernard */ public class ChangeFileAttributesJob extends FileJob { private static final Logger LOGGER = LoggerFactory.getLogger(ChangeFileAttributesJob.class); private final boolean recurseOnDirectories; private int permissions = -1; private long date = -1; private short replication = -1; public ChangeFileAttributesJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, int permissions, boolean recurseOnDirectories) { super(progressDialog, mainFrame, files); this.permissions = permissions; this.recurseOnDirectories = recurseOnDirectories; } public ChangeFileAttributesJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, long date, boolean recurseOnDirectories) { super(progressDialog, mainFrame, files); this.date = date; this.recurseOnDirectories = recurseOnDirectories; } public ChangeFileAttributesJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, short replication, boolean recurseOnDirectories) { super(progressDialog, mainFrame, files); this.replication = replication; this.recurseOnDirectories = recurseOnDirectories; } @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted if (getState() == State.INTERRUPTED) return false; if (recurseOnDirectories && file.isDirectory()) { do { // Loop for retries try { AbstractFile[] children = file.ls(); int nbChildren = children.length; for (int i=0; i. */ package com.mucommander.job; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.io.StreamUtils; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This job combines files into one file, optionally checking the CRC of the merged file. * @author Mariusz Jakubowski */ public class CombineFilesJob extends AbstractCopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(CombineFilesJob.class); AbstractFile destFile = null; private OutputStream out; private AbstractFile crcFile; public CombineFilesJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFile, int fileExistsAction) { super(progressDialog, mainFrame, files, destFile, null, fileExistsAction); this.errorDialogTitle = Translator.get("combine_files_dialog.error_title"); } @Override protected boolean hasFolderChanged(AbstractFile folder) { return baseDestFolder.isParentOf(folder); } @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if (destFile == null) { // executed only on first part createDestFile(file); findCRCFile(file); } if (getState() == State.INTERRUPTED) return false; try { InputStream in = file.getInputStream(); setCurrentInputStream(in); StreamUtils.copyStream(in, out); } catch (IOException e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", destFile.getName()), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); interrupt(); return false; } finally { closeCurrentInputStream(); } return true; } /** * Creates the destination (merged) file. * @param file first part */ protected void createDestFile(AbstractFile file) { destFile = baseDestFolder; baseDestFolder = baseDestFolder.getParent(); destFile = checkForCollision(file, baseDestFolder, destFile, false); if (destFile == null) { interrupt(); return; } try { out = destFile.getOutputStream(); } catch(IOException e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", destFile.getName()), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); interrupt(); } } /** * Checks if CRC file exists. * @param file firts part */ private void findCRCFile(AbstractFile file) { AbstractFile f = file.getParent(); if (f != null) { try { crcFile = f.getDirectChild(file.getNameWithoutExtension() + ".sfv"); } catch (IOException e) { LOGGER.debug("Caught exception", e); } } } @Override protected void jobStopped() { super.jobStopped(); closeOutputStream(); } @Override protected void jobCompleted() { super.jobCompleted(); closeOutputStream(); checkCRC(); } /** * Checks CRC of merged file (if CRC file exists). */ private void checkCRC() { if (crcFile==null || !crcFile.exists()) { showErrorDialog(errorDialogTitle, Translator.get("combine_files_job.no_crc_file"), new String[]{OK_TEXT}, new int[]{OK_ACTION} ); return; } InputStream crcIn = null; try { crcIn = crcFile.getInputStream(); BufferedReader crcReader = new BufferedReader(new InputStreamReader(crcIn)); String crcLine = crcReader.readLine(); crcLine = crcLine.substring(crcLine.lastIndexOf(' ')+1).trim(); String crcDest = destFile.calculateChecksum("CRC32"); if (!crcLine.equals(crcDest)) { showErrorDialog(errorDialogTitle, Translator.get("combine_files_job.crc_check_failed", crcDest, crcLine), new String[]{OK_TEXT}, new int[]{OK_ACTION} ); } else { showErrorDialog(Translator.get("combine_files_dialog.error_title"), Translator.get("combine_files_job.crc_ok"), new String[]{OK_TEXT}, new int[]{OK_ACTION} ); } } catch (Exception e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("combine_files_job.crc_read_error"), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); } finally { if (crcIn!=null) { try { crcIn.close(); } catch (IOException e) { LOGGER.debug("Caught exception", e); } } } } /** * Closes the output stream. */ private void closeOutputStream() { if (out != null) { try { out.close(); } catch (IOException e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", destFile.getName()), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); } } } } ================================================ FILE: src/main/java/com/mucommander/job/CopyJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.adb.AdbFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.utils.ScanDirectoryThread; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; /** * This job recursively copies a set of files. Directories are copied recursively. * * @author Maxence Bernard */ public class CopyJob extends AbstractCopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(CopyJob.class); /** Destination file that is being copied, this value is updated every time #processFile() is called. * The value can be used by subclasses that override processFile should they need to work on the destination file. */ AbstractFile currentDestFile; private final ScanDirectoryThread scanDirectoryThread; /** Processed files counter */ private long processedFilesCount; /** Operating mode : COPY or DOWNLOAD */ public enum Mode { COPY, DOWNLOAD } private final Mode mode; /** * Creates a new CopyJob without starting it. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be copied * @param destFolder destination folder where the files will be copied * @param newName the new filename in the destination folder, can be null in which case the original filename will be used. * @param mode mode in which CopyJob is to operate: {@link com.mucommander.job.CopyJob.Mode#COPY} or {@link com.mucommander.job.CopyJob.Mode#DOWNLOAD}. * @param fileExistsAction default action to be performed when a file already exists in the destination, see {@link com.mucommander.ui.dialog.file.FileCollisionDialog} for allowed values */ public CopyJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFolder, String newName, Mode mode, int fileExistsAction) { super(progressDialog, mainFrame, files, destFolder, newName, fileExistsAction); this.mode = mode; this.errorDialogTitle = Translator.get(mode==Mode.DOWNLOAD ? "download_dialog.error_title" : "copy_dialog.error_title"); scanDirectoryThread = new ScanDirectoryThread(files); scanDirectoryThread.start(); } /** * Copies recursively the given file or folder. * * @param file the file or folder to move * @param recurseParams destination folder where the given file will be copied (null for top level files) * * @return true if the file has been copied. */ @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted if (getState() == State.INTERRUPTED) { return false; } processedFilesCount++; //try { // delay for debug purposes // Thread.sleep(1000); //} catch (InterruptedException e) { // e.printStackTrace(); //} // Destination folder AbstractFile destFolder = recurseParams == null ? baseDestFolder : (AbstractFile)recurseParams; // Is current file in base folder ? boolean isFileInBaseFolder = files.contains(file); // Determine filename in destination String destFileName = (isFileInBaseFolder && newName != null) ? newName : file.getName(); //System.out.println("destFileName " + destFileName); // create destination AbstractFile instance AbstractFile destFile = createDestinationFile(destFolder, destFileName); if (destFile == null) { return false; } currentDestFile = destFile; AbstractFile sourceFile = file.getAncestor(); // Do nothing if file is a symlink (skip file and return) if (file.isSymlink() && file instanceof LocalFile) { tryCopySymlinkFile(file, destFolder); return true; } // ADB files if (sourceFile instanceof AdbFile && destFile instanceof LocalFile && !sourceFile.isDirectory()) { return copyAdbFile(destFile, (AdbFile) sourceFile); } destFile = checkForCollision(file, destFolder, destFile, false); if (destFile == null) { return false; } if (!file.isDirectory()) { return tryCopyFile(file, destFile, append, errorDialogTitle); } return copyDirectoryRecursively(file, destFileName, destFile); } private boolean copyDirectoryRecursively(AbstractFile file, String destFileName, AbstractFile destFile) { return createSubFolder(destFileName, destFile) && copyChildrenRecursively(file, destFile); } private boolean createSubFolder(String destFileName, AbstractFile destFile) { // create the folder in the destination folder if it doesn't exist if (!(destFile.exists() && destFile.isDirectory())) { // Loop for retry do { try { destFile.mkdir(); } catch (IOException e) { // Unable to create folder int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_create_folder", destFileName)); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel or close dialog return false return false; // Skip continues } break; } while(true); } return true; } private boolean copyChildrenRecursively(AbstractFile file, AbstractFile destFile) { // and copy each file in this folder recursively do { // Loop for retry try { // for each file in folder... processChildernFiles(file, destFile); // Set currentDestFile back to the enclosing folder in case an overridden processFile method // needs to work with the folder after calling super.processFile. currentDestFile = destFile; // Only when finished with folder, set destination folder's date to match the original folder one changeFolderModifiedDate(file, destFile); return true; } catch (IOException e) { // file.ls() failed int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_read_folder", file.getName())); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel, skip or close dialog returns false return false; } } while(true); } private void changeFolderModifiedDate(AbstractFile file, AbstractFile destFile) { if (destFile.isFileOperationSupported(FileOperation.CHANGE_DATE)) { try { destFile.setLastModifiedDate(file.getLastModifiedDate()); } catch (IOException e) { LOGGER.debug("failed to change the date of "+ destFile, e); } } } private void processChildernFiles(AbstractFile file, AbstractFile destFile) throws IOException { AbstractFile[] subFiles = file.ls(); for (int i = 0; i < subFiles.length && getState() != State.INTERRUPTED; i++) { // Notify job that we're starting to process this file (needed for recursive calls to processFile) nextFile(subFiles[i]); processFile(subFiles[i], destFile); } } private boolean copyAdbFile(AbstractFile destFile, AdbFile sourceFile) { try { sourceFile.pushTo(destFile); } catch (IOException e) { e.printStackTrace(); return false; } return true; } // This job modifies baseDestFolder and its subfolders @Override protected boolean hasFolderChanged(AbstractFile folder) { return baseDestFolder.isParentOf(folder); } @Override protected void jobCompleted() { super.jobCompleted(); // If the destination files are located inside an archive, optimize the archive file AbstractArchiveFile archiveFile = baseDestFolder.getParentArchive(); if (archiveFile != null && archiveFile.isArchive() && archiveFile.isWritable()) { optimizeArchive((AbstractRWArchiveFile)archiveFile); } // If this job corresponds to a 'local copy' of a single file and in the same directory, // select the copied file in the active table after this job has finished (and hasn't been cancelled) if (files.size() == 1 && newName != null && baseDestFolder.equalsCanonical(files.elementAt(0).getParent())) { // Resolve new file instance now that it exists: some remote files do not immediately update file attributes // after creation, we need to get an instance that reflects the newly created file attributes selectFileWhenFinished(FileFactory.getFile(baseDestFolder.getAbsolutePath(true)+newName)); } } @Override public String getStatusString() { if (isCheckingIntegrity()) { return super.getStatusString(); } if (isOptimizingArchive) { return Translator.get("optimizing_archive", archiveToOptimize.getName()); } return Translator.get(mode == Mode.DOWNLOAD ? "download_dialog.downloading_file" : "copy_dialog.copying_file", getCurrentFilename()); } @Override public void interrupt() { if (scanDirectoryThread != null) { scanDirectoryThread.interrupt(); } super.interrupt(); } @Override public float getTotalPercentDone() { if (scanDirectoryThread == null || !scanDirectoryThread.isCompleted()) { float result = super.getTotalPercentDone(); return result > 5 ? 5 : result; } float progressBySize = 1.0f*(getTotalByteCounter().getByteCount() + getTotalSkippedByteCounter().getByteCount()) / scanDirectoryThread.getTotalBytes(); float progressByCount = 1.0f*(processedFilesCount-1) / scanDirectoryThread.getFilesCount(); float result = (progressBySize * 8 + progressByCount * 2) / 10; if (result < 0) { return 0; } else if (result > 1) { return 1; } return result; } // private void copySymLink(AbstractFile file, AbstractFile destFile) { // String targetPath = SymLinkUtils.getTargetPath(file); // SymLinkUtils.createSymlink(destFile, targetPath); // } } ================================================ FILE: src/main/java/com/mucommander/job/DeleteJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import com.mucommander.job.utils.ScanDirectoryThread; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.AbstractRWArchiveFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.DesktopManager; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This class is responsible for deleting a set of files. This job can operate in two modes, depending on the boolean * value specified in the constructor: *
      *
    • moveToTrash enabled: files are moved to the trash returned by {@link DesktopManager#getTrash()}. *
    • moveToTrash disabled: files are permanently deleted, i.e. deleted files cannot be recovered. In this mode, * folders are deleted recursively *
    * * @author Maxence Bernard */ public class DeleteJob extends FileJob { private static final Logger LOGGER = LoggerFactory.getLogger(DeleteJob.class); /** Title used for error dialogs */ private final String errorDialogTitle; /** If true, files will be moved to the trash instead of being deleted */ private final boolean moveToTrash; /** Trash instance, null if moveToTrash is false */ private AbstractTrash trash; /** The archive that contains the deleted files (maybe null) */ private AbstractRWArchiveFile archiveToOptimize; /** True when an archive is being optimized */ private boolean isOptimizingArchive; protected ScanDirectoryThread scanDirectoryThread; /** Processed files counter */ protected long processedFilesCount; /** * Creates a new DeleteJob without starting it. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be deleted * @param moveToTrash if true, files will be moved to the trash, if false they will be permanently deleted. * Should be true only if a trash is available on the current platform. */ public DeleteJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, boolean moveToTrash) { super(progressDialog, mainFrame, files); this.errorDialogTitle = Translator.get("delete_dialog.error_title"); this.moveToTrash = moveToTrash; if (moveToTrash) { trash = DesktopManager.getTrash(); } scanDirectoryThread = new ScanDirectoryThread(files, false); scanDirectoryThread.start(); } /** * Deletes the given file, either by moving it to the trash (if {@link #moveToTrash} is true) or by deleting the * file directly. * * @param file the file to delete * @throws IOException if an error occurred while deleting the file */ private void deleteFile(AbstractFile file) throws IOException { if (moveToTrash) { trash.moveToTrash(file); } else { file.delete(); } } //////////////////////////// // FileJob implementation // //////////////////////////// /** * Deletes recursively the given file or folder. * * @param file the file or folder to delete * @param recurseParams not used * * @return true if the file has been completely deleted. */ @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if (getState() == State.INTERRUPTED) { return false; } processedFilesCount++; // Delete files recursively, only if trash is not used. int ret; if(!moveToTrash && file.isDirectory()) { String filePath = file.getAbsolutePath(); filePath = filePath.substring(getBaseSourceFolder().getAbsolutePath(false).length()+1); // Important: symlinks must *not* be followed -- following symlinks could have disastrous effects. if(!file.isSymlink()) { do { // Loop for retry // Delete each file in this folder try { AbstractFile[] subFiles = file.ls(); for(int i=0; i 15 ? 15 : result; } float result = 1.0f*(processedFilesCount-1) / scanDirectoryThread.getFilesCount(); if (result < 0) { result = 0; } else if (result > 1) { result = 1; } return result; } } ================================================ FILE: src/main/java/com/mucommander/job/FileCollisionChecker.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; /** * The purpose of this class is to check for collisions between a source and destination file used in a file transfer. * *

    Currently, 3 collision types are detected: *

      *
    • {@link #DESTINATION_FILE_ALREADY_EXISTS}: the destination file already exists *
    • {@link #SAME_SOURCE_AND_DESTINATION}: source and destination files are the same, according to {@link AbstractFile#equalsCanonical(Object)} *
    • {@link #SOURCE_PARENT_OF_DESTINATION}: source is a folder (as returned by {@link com.mucommander.commons.file.AbstractFile#isBrowsable()} * and a parent of destination. *
    * *

    The value returned by {@link #checkForCollision(com.mucommander.commons.file.AbstractFile, com.mucommander.commons.file.AbstractFile)} * can be used to create a {@link com.mucommander.ui.dialog.file.FileCollisionDialog} in order to inform the user of the collision * and ask him how to resolve it. * * @see com.mucommander.ui.dialog.file.FileCollisionDialog * @author Maxence Bernard */ public class FileCollisionChecker { /** No collision detected */ public static final int NO_COLLISION = 0; /** The destination file already exists and is not a directory */ public static final int DESTINATION_FILE_ALREADY_EXISTS = 1; /** Source and destination files are the same */ public static final int SAME_SOURCE_AND_DESTINATION = 2; /** Source and destination are both folders and destination is a subfolder of source */ public static final int SOURCE_PARENT_OF_DESTINATION = 3; /** * * @param sourceFile source file, can be null in which case the only collision checked against is {@link #DESTINATION_FILE_ALREADY_EXISTS}. * @param destFile destination file, cannot be null * @return an int describing the collision type, or {@link #NO_COLLISION} if no collision was detected (see constants) */ public static int checkForCollision(AbstractFile sourceFile, AbstractFile destFile) { if(sourceFile!=null) { // Source and destination are equal if(destFile.equalsCanonical(sourceFile)) return SAME_SOURCE_AND_DESTINATION; // Both source and destination are folders and destination is a subfolder of source if(sourceFile.isParentOf(destFile)) return SOURCE_PARENT_OF_DESTINATION; } // File exists in destination if(destFile.exists() && !destFile.isDirectory()) return DESTINATION_FILE_ALREADY_EXISTS; return NO_COLLISION; } } ================================================ FILE: src/main/java/com/mucommander/job/FileJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.util.WeakHashMap; import com.mucommander.ui.dialog.PasswordDialog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.CachedFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.progress.JobProgress; import com.mucommander.job.ui.DialogResult; import com.mucommander.job.ui.UserInputHelper; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.notifier.AbstractNotifier; import com.mucommander.ui.notifier.NotificationType; /** * FileJob is a container for a 'file getTask' : basically an operation that involves files and bytes. * The class extending FileJob is required to give some information about the status of the job that * will be used to display visual indications of the job's progress. *

    * The actual processing is performed in a separate thread. A FileJob needs to be started explicitly using * {@link #start()}. The lifecycle of a FileJob is as follows:
    *
    *

     * {@link State#NOT_STARTED} -> {@link State#RUNNING} -> {@link State#FINISHED}
     *                         ^                |
     *                         |                -> {@link State#INTERRUPTED}
     *                         |                |                      
     *                         |                -> {@link State#PAUSED} -|
     *                         |                                    |
     *                         -------------------------------------|
     * 
    * * @author Maxence Bernard */ public abstract class FileJob implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(FileJob.class); /** Thread in which the file job is performed */ private Thread jobThread; /** Lock used when job is being paused */ private final Object pauseLock = new Object(); /** Timestamp in milliseconds when job started */ private long startDate; /** Timestamp in milliseconds when job has finished */ private long endDate; /** Number of milliseconds during which this job has been paused (been waiting for some user response). * Used to compute stats like average speed. */ private long pausedTime; /** Contains the timestamp when this job has been put in pause (if in pause) */ private long pauseStartDate; /** Associated dialog showing job progression */ private ProgressDialog progressDialog; /** Main frame on which the job is to be performed */ private final MainFrame mainFrame; /** Base source folder */ private AbstractFile baseSourceFolder; /** Files which are going to be processed */ protected FileSet files; /** Number of files that this job contains */ private int nbFiles; /** Index of file currently being processed, see {@link #getCurrentFileIndex()} */ private int currentFileIndex = -1; /** File currently being processed */ private AbstractFile currentFile; /** Name of the file currently being processed */ private String currentFilename = ""; /** If set to true, processed files will be unmarked from current table */ private boolean autoUnmark = true; /** File to be selected after job has finished (can be null if not set) */ private AbstractFile fileToSelect; public enum State { /** * Indicates that this job has not started yet, this is a temporary state */ NOT_STARTED, /** * Indicates that this job is currently processing files, this is a temporary state */ RUNNING, /** * Indicates that this job is currently paused, waiting for user response, this is a temporary state */ PAUSED, /** * Indicates that this job has been interrupted by the end user, this is a permanent state */ INTERRUPTED, /** Indicates that this job has naturally finished (i.e. without being interrupted), this is a permanent state */ FINISHED } /** Current state of this job */ private State jobState = State.NOT_STARTED; /** List of registered FileJobListener stored as weak references */ private final WeakHashMap listeners = new WeakHashMap<>(); /** Information about this job progress */ private final JobProgress jobProgress; /** True if the user asked to automatically skip errors */ private boolean autoSkipErrors; // private int nbFilesProcessed; // private int nbFilesDiscovered; protected final static int SKIP_ACTION = 0; protected final static int SKIP_ALL_ACTION = 1; protected final static int RETRY_ACTION = 2; protected final static int CANCEL_ACTION = 3; protected final static int APPEND_ACTION = 4; protected final static int OK_ACTION = 5; protected final static int OVERWRITE_READONLY_ACTION = 6; protected final static int OVERWRITE_READONLY_ALL_ACTION = 7; protected final static int RETRY_AS_ROOT_ACTION = 8; protected final static int RETRY_AS_ROOT_ALWAYS_ACTION = 9; protected final static String SKIP_TEXT = Translator.get("skip"); protected final static String SKIP_ALL_TEXT = Translator.get("skip_all"); protected final static String RETRY_TEXT = Translator.get("retry"); protected final static String RETRY_AS_ROOT_TEXT = Translator.get("retry_as_root"); protected final static String RETRY_AS_ROOT_ALWAYS_TEXT = Translator.get("retry_as_root_always"); protected final static String CANCEL_TEXT = Translator.get("cancel"); protected final static String APPEND_TEXT = Translator.get("resume"); protected final static String OK_TEXT = Translator.get("ok"); protected final static String OVERWRITE_READONLY_TEXT = Translator.get("overwrite"); protected final static String OVERWRITE_READONLY_ALL_TEXT = Translator.get("overwrite_all"); /** * Creates a new FileJob without starting it. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be processed */ public FileJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files) { this(mainFrame, files); this.progressDialog = progressDialog; } /** * Creates a new FileJob without starting it, and with no associated ProgressDialog. * * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be processed */ public FileJob(MainFrame mainFrame, FileSet files) { this.mainFrame = mainFrame; setFiles(files); this.jobProgress = new JobProgress(this); } public FileJob(MainFrame mainFrame) { this.mainFrame = mainFrame; this.jobProgress = new JobProgress(this); } /** * Specifies whether files that have been processed should be unmarked from current table (enabled by default). * * @param autoUnmark true to automatically unmark files after they have been processed. */ public void setAutoUnmark(boolean autoUnmark) { this.autoUnmark = autoUnmark; } /** * Sets whether this file job should automatically skip errors when encountered (disabled by default). * * @param autoSkipErrors true to automatically skip errors, false to show an error dialog. */ public void setAutoSkipErrors(boolean autoSkipErrors) { this.autoSkipErrors = autoSkipErrors; } /** * Sets the given file to be selected in the active table after this job has finished. * The file will only be selected if it exists in the active table's folder and if this job hasn't * been canceled. The selection will occur after the tables have been refreshed (if they are refreshed). * * @param file the file to be selected in the active table after this job has finished */ protected void selectFileWhenFinished(AbstractFile file) { this.fileToSelect = file; } /** * Starts file job in a separate thread. */ public void start() { // Return if job has already been started if (getState() != State.NOT_STARTED) { return; } // Pause auto-refresh during file job as it potentially modifies the current folders contents // and would potentially cause folder panel to auto-refresh getMainFrame().getLeftPanel().getFolderChangeMonitor().setPaused(true); getMainFrame().getRightPanel().getFolderChangeMonitor().setPaused(true); setState(State.RUNNING); startDate = System.currentTimeMillis(); jobThread = new Thread(this, getClass().getName()); jobThread.start(); } /** * Returns the dialog showing progress of this job. * @return the progressDialog */ protected ProgressDialog getProgressDialog() { return progressDialog; } /** * Returns the main frame. * @return the mainFrame */ protected MainFrame getMainFrame() { return mainFrame; } /** * Returns the current state of this FileJob. See constant fields for possible return values. * * @return the current state of this FileJob. See constant fields for possible return values. */ public State getState() { return jobState; } /** * Sets a new state for this FileJob and notifies registered FileJobListener instances of the change. * * @param jobState the new state */ protected void setState(State jobState) { State oldState = this.jobState; this.jobState = jobState; for (FileJobListener listener : listeners.keySet()) listener.jobStateChanged(this, oldState, jobState); } /** * Registers a FileJobListener to receive notifications whenever state of this FileJob changes. * *

    Listeners are stored as weak references so {@link #removeFileJobListener(FileJobListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the FileJobListener to register */ public void addFileJobListener(FileJobListener listener) { listeners.put(listener, null); } /** * Removes the given FileJobListener from the list of listeners that receive notifications when the state of * this FileJob has changed. * * @param listener the FileJobListener to remove */ public void removeFileJobListener(FileJobListener listener) { listeners.remove(listener); } /** * Returns the timestamp in milliseconds when this job started. * * @return the timestamp in milliseconds when this job started */ public long getStartDate() { return startDate; } /** * Returns the timestamp in milliseconds when this job ended, 0 if this job hasn't finished yet. * * @return the timestamp in milliseconds when this job ended */ public long getEndDate() { return endDate; } /** * Returns the timestamp in milliseconds when this job was last paused, 0 if this job has not been * paused yet. * * @return the timestamp in milliseconds when this job was last paused */ public long getPauseStartDate() { return pauseStartDate; } /** * Sets the timestamp in milliseconds when this job is paused. */ private void setPauseStartDate() { this.pauseStartDate = System.currentTimeMillis(); } /** * Returns the number of milliseconds during which this job has been paused (been waiting for some user response). * If this job has been paused several times, the total is returned. If this job has not been paused yet, * 0 is returned. * * @return the number of milliseconds during which this job has been paused */ public long getPausedTime() { return pausedTime; } /** * Adds a time of last pause to this job pause time counter. */ private void calcPausedTime() { this.pausedTime += System.currentTimeMillis() - this.getPauseStartDate(); } /** * Returns the number of milliseconds this job effectively spent processing files, excluding any pause time. * * @return the number of milliseconds this job effectively spent processing files, excluding any pause time */ public long getEffectiveJobTime() { // If job hasn't start yet, return 0 if (getStartDate() == 0) { return 0; } return (getEndDate() == 0 ? System.currentTimeMillis() : getEndDate())-getStartDate()-getPausedTime(); } /** * Interrupts this job, changes the job state to {@link State#INTERRUPTED} and notifies listeners. */ public void interrupt() { switch (getState()) { case INTERRUPTED: case FINISHED: return; case PAUSED: setPaused(false); break; } // Set state before calling stop() so that state is INTERRUPTED when jobStopped() is called // (some FileJob rely on that) setState(State.INTERRUPTED); stop(); } /** * Release reference to thread and store job's end date. */ private void stop() { // Return if job has already been stopped if (jobThread == null) { return; } // // Start by calling interrupt to have the thread return from any blocking I/O occurring in an interruptible // // channel or selector. // jobThread.interrupt(); jobThread = null; endDate = System.currentTimeMillis(); // Notify that the job has been stopped jobStopped(); } /** * Sets or unsets this job in paused mode. * @param paused true to set in pause mode */ public void setPaused(boolean paused) { // Lock the pause lock while updating paused status synchronized (pauseLock) { // Resume job if it was paused if (!paused && getState() == State.PAUSED) { // Calculate pause time calcPausedTime(); // Call the jobResumed method to notify of the new job's state jobResumed(); // Wake up the job's thread that is potentially waiting for pause to be over pauseLock.notify(); // Switch to RUNNING state and notify listeners setState(State.RUNNING); } // Pause job if it not paused already else if(paused && getState() != State.PAUSED && getState() != State.INTERRUPTED && getState() != State.FINISHED) { // Memorize pause time in order to calculate pause time when the job is resumed setPauseStartDate(); // Call the jobPaused method to notify of the new job's state jobPaused(); // Switch to PAUSED state and notify listeners setState(State.PAUSED); } } } /** * Changes the current file. This method should be called by subclasses whenever the job * starts processing a new file other than a top-level file, i.e. one that was passed * as an argument to {@link #processFile(AbstractFile, Object) processFile()}. * ({#nextFile(AbstractFile) nextFile()} is automatically called for files in base folder). * @param file file to process */ protected void nextFile(AbstractFile file) { this.setCurrentFile(file); // // Notify ProgressDialog (if any) that a new file is being processed // if(progressDialog!=null) // progressDialog.notifyCurrentFileChanged(); // Lock the pause lock synchronized(pauseLock) { // Loop while job is paused, there shouldn't normally be more than one loop while (getState() == State.PAUSED) { try { // Wait for a call to notify() pauseLock.wait(); } catch(InterruptedException e) { // No more problem, loop one more time } } } // if(this.currentFile!=null) // this.nbFilesProcessed++; } // protected void fileDiscovered(AbstractFile file) { // this.nbFilesDiscovered++; // } // // protected void filesDiscovered(AbstractFile files[]) { // this.nbFilesDiscovered += files.length; // } // // protected int getNbFilesDiscovered() { // return this.nbFilesDiscovered; // } // // protected int getNbFilesProcessed() { // return this.nbFilesProcessed; // } // /** * Returns the name of the file currently being processed surrounded by simple quotes (e.g. 'test.zip'), or an empty * string if no file is currently being processed. * * @return the name of the file currently being processed surrounded by simple quotes, or an empty string if no file * is currently being processed */ String getCurrentFilename() { return currentFilename; } /** * This method is called when this job starts, before the first call to {@link #processFile(AbstractFile,Object)} is made. * This method implementation does nothing but it can be overriden by subclasses to perform some first-time initializations. */ protected void jobStarted() { LOGGER.debug("called"); } /** * This method is called when this job has completed normal execution : all files have been processed without any interruption * (without any call to {@link #interrupt()}). * *

    The call happens after the last call to {@link #processFile(AbstractFile,Object)} is made. * This method implementation does nothing but it can be overridden by subclasses to properly complete the job. *

    Note that this method will NOT be called if a call to {@link #interrupt()} was made before all files were processed. */ protected void jobCompleted() { LOGGER.debug("called"); // Send a system notification if a notifier is available and enabled if (AbstractNotifier.isAvailable() && AbstractNotifier.getNotifier().isEnabled()) { AbstractNotifier.getNotifier().displayBackgroundNotification(NotificationType.JOB_COMPLETED, getProgressDialog() == null ? "" : getProgressDialog().getTitle(), Translator.get("progress_dialog.job_finished")); } } /** * This method is called when this job has been paused, either by the user, or by the job when asking for user input. * *

    This method implementation does nothing but it can be overridden by subclasses to do whatever is needed * when the job has been paused. */ protected void jobPaused() { LOGGER.debug("called"); } /** * This method is called when this job has been resumed after being paused. * *

    This method implementation does nothing but it can be overridden by subclasses to do whatever is needed * when the job has returned from pause. */ protected void jobResumed() { LOGGER.debug("called"); } /** * This method is called when this job has been stopped. The call happens after all calls to {@link #processFile(AbstractFile,Object)} and * {@link #jobCompleted()}. * This method implementation does nothing but it can be overridden by subclasses to properly terminate the job. * This is where you want to close any opened connections. * *

    Note that unlike {@link #jobCompleted()} this method is always called, whether the job has been completed (all * files were processed) or has been interrupted in the middle. */ protected void jobStopped() { LOGGER.debug("called"); } /** * Displays an error dialog with the specified title and message, * offers to skip the file, retry or cancel and waits for user choice. * The job is stopped if 'cancel' or 'close' was chosen, and the result * is returned. * * @param title error dialog title * @param message error dialog message */ protected int showErrorDialog(String title, String message) { String[] actionTexts = new String[]{SKIP_TEXT, SKIP_ALL_TEXT, RETRY_TEXT, CANCEL_TEXT}; int[] actionValues = new int[]{SKIP_ACTION, SKIP_ALL_ACTION, RETRY_ACTION, CANCEL_ACTION}; return showErrorDialog(title, message, actionTexts, actionValues); } /** * Displays an error dialog with the specified title and message and returns the selection action's value. * * @param title error dialog title * @param message error dialog message * @param actionTexts actions text to display * @param actionValues actions return values */ protected int showErrorDialog(String title, String message, String[] actionTexts, int[] actionValues) { // Return SKIP_ACTION if 'skip all' has previously been selected and 'skip' is in the list of actions. if (autoSkipErrors) { for (int actionValue : actionValues) if (actionValue == SKIP_ACTION) { return SKIP_ACTION; } } // Send a system notification if a notifier is available and enabled if (AbstractNotifier.isAvailable() && AbstractNotifier.getNotifier().isEnabled()) { AbstractNotifier.getNotifier().displayBackgroundNotification(NotificationType.JOB_ERROR, title, message); } QuestionDialog dialog; var mainFrame = getMainFrame().getJFrame(); if (getProgressDialog() == null) { dialog = new QuestionDialog(mainFrame, title, message, mainFrame, actionTexts, actionValues, 0); } else { dialog = new QuestionDialog(getProgressDialog(), title, message, mainFrame, actionTexts, actionValues, 0); } // Cancel or close dialog stops this job int userChoice = waitForUserResponse(dialog); if (userChoice == -1 || userChoice == CANCEL_ACTION) { interrupt(); // Keep 'skip all' choice for further error and return SKIP_ACTION } else if (userChoice == SKIP_ALL_ACTION) { autoSkipErrors = true; return SKIP_ACTION; } return userChoice; } String enterRootPasswordDialog() { return new PasswordDialog(Translator.get("enter_root_password")).getPassword(); } /** * Waits for the user's answer to the given question dialog, putting this * job in pause mode while waiting for the user. * * @param dialog object */ int waitForUserResponse(DialogResult dialog) { Object userInput = waitForUserResponseObject(dialog); return (Integer) userInput; } Object waitForUserResponseObject(DialogResult dialog) { // Put this job in pause mode while waiting for user response setPaused(true); UserInputHelper jobUserInput = new UserInputHelper(this, dialog); Object userInput = jobUserInput.getUserInput(); // Back to work setPaused(false); return userInput; } /** * Check and if needed, refreshes both file tables's current folders, based on the job's refresh policy. */ private void refreshTables() { FolderPanel activePanel = getMainFrame().getActivePanel(); FolderPanel inactivePanel = getMainFrame().getInactivePanel(); if (hasFolderChanged(inactivePanel.getCurrentFolder())) { inactivePanel.tryRefreshCurrentFolder(); } if (hasFolderChanged(activePanel.getCurrentFolder())) { // Select file specified by selectFileWhenFinished (if any) only if the file exists in the active table's folder if (fileToSelect != null && activePanel.getCurrentFolder().equalsCanonical(fileToSelect.getParent()) && fileToSelect.exists()) { activePanel.tryRefreshCurrentFolder(fileToSelect); } else { activePanel.tryRefreshCurrentFolder(); } } // Repaint the status bar as marked files have changed mainFrame.getStatusBar().updateSelectedFilesInfo(); // Resume current folders auto-refresh getMainFrame().getLeftPanel().getFolderChangeMonitor().setPaused(false); getMainFrame().getRightPanel().getFolderChangeMonitor().setPaused(false); } /** * Returns this job's percentage of completion, as a float comprised between 0 and 1. * * @return this job's percentage of completion, as a float comprised between 0 and 1 */ public float getTotalPercentDone() { return getCurrentFileIndex()/(float)getNbFiles(); } /** * Returns the index of the file currently being processed, {@link #getNbFiles()} if all files have been processed. * * @return the index of the file currently being processed, {@link #getNbFiles()} if all files have been processed */ public int getCurrentFileIndex() { return currentFileIndex==-1?0:currentFileIndex; } /** * Returns the file currently being processed. * @return the file currently being processed. */ public AbstractFile getCurrentFile() { return currentFile; } /** * Sets the file currently being processed. * @param file the file currently being processed. */ private void setCurrentFile(AbstractFile file) { this.currentFile = file; // Update current file information returned by getCurrentFilename() this.currentFilename = "'" + file.getName() + "'"; } /** * Returns the number of file that this job contains. * * @return the number of file that this job contains */ public int getNbFiles() { return nbFiles; } /** * Sets the number of files that this job contains. * * @param nbFiles the number of files that this job contains. */ protected void setNbFiles(int nbFiles) { this.nbFiles = nbFiles; } public void setFiles(FileSet files) { this.files = files; this.nbFiles = files.size(); this.baseSourceFolder = files.getBaseFolder(); // create CachedFile instances around the source files in order to cache the return value of frequently accessed // methods. This eliminates some I/O, at the (small) cost of a bit more CPU and memory. Recursion is enabled // so that children and parents of the files are also cached. // Note: When cached methods are called, they no longer reflect changes in the underlying files. In particular, // changes of size or date could potentially not be reflected when files are being processed but this should // not really present a risk. for (int i = 0; i < nbFiles; i++) { AbstractFile tempFile = files.elementAt(i); files.setElementAt((tempFile instanceof CachedFile)?tempFile:new CachedFile(tempFile, true), i); } if (this.baseSourceFolder != null) { this.baseSourceFolder = (getBaseSourceFolder() instanceof CachedFile)?getBaseSourceFolder():new CachedFile(getBaseSourceFolder(), true); } } /** * Returns a String describing what the job is currently doing. This default implementation returns * Processing CURRENT_FILE where CURRENT_FILE is the name of the file currently being processed. * This method should be overridden to provide a more accurate description. * * @return a String describing what the job is currently doing */ public String getStatusString() { return Translator.get("progress_dialog.processing_file", getCurrentFilename()); } /** * Returns information about the job progress. * @return the job progress */ public JobProgress getJobProgress() { return jobProgress; } /** * Returns the base source folder. * @return the baseSourceFolder */ AbstractFile getBaseSourceFolder() { return baseSourceFolder; } /** * This method is public as a side-effect of this class implementing Runnable. */ @Override public final void run() { FileTable activeTable = getMainFrame().getActiveTable(); // Notify that this job has started jobStarted(); //this.nbFilesDiscovered += nbFiles; // Loop on all source files, checking that job has not been interrupted for (currentFileIndex = 0; currentFileIndex < nbFiles; currentFileIndex++) { AbstractFile currentFile = files.elementAt(currentFileIndex); // Change current file and advance file index nextFile(currentFile); // Process current file boolean success = processFile(currentFile, null); // Stop if job was interrupted if (getState() == State.INTERRUPTED) { break; } // Unmark file in active table if 'auto unmark' is enabled // and file was processed successfully if (autoUnmark && success) { // Do not repaint rows individually as it would be too expensive activeTable.setFileMarked(currentFile, false, false); } // If last file was reached without any user interruption, all files have been processed with or // without errors, switch to FINISHED state and notify listeners if (currentFileIndex >= nbFiles-1 && getState() != FileJob.State.INTERRUPTED) { currentFileIndex++; stop(); jobCompleted(); setState(State.FINISHED); } } // Refresh tables's current folders, based on the job's refresh policy. refreshTables(); } /** * Returns true if the given folder has or may have been modified by this job. * This method is called after this job has finished processing files, to determine if the current MainFrame's * file tables need to be refreshed to reveal the modified contents. * * @param folder the folder to test * @return true if the given folder has or may have been modified by this job */ protected abstract boolean hasFolderChanged(AbstractFile folder); /** * Automatically called by {@link #run()} for each file that needs to be processed. * * @param file the file or folder to process * @param recurseParams array of parameters which can be used when calling this method recursively, contains null when called by {@link #run()} * * @return true if the operation was successful */ protected abstract boolean processFile(AbstractFile file, Object recurseParams); } ================================================ FILE: src/main/java/com/mucommander/job/FileJobException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; /** * FileJobException are exceptions that can be thrown by certain * FileJob methods. * * @author Maxence Bernard */ public class FileJobException extends Exception { /** Source cannot be opened */ public final static int CANNOT_OPEN_SOURCE = 1; /** Destination cannot be opened */ public final static int CANNOT_OPEN_DESTINATION = 2; /** An error occurred during the file transfer */ public final static int ERROR_WHILE_TRANSFERRING = 3; protected int reason; public FileJobException(int reason) { this.reason = reason; } public int getReason() { return reason; } } ================================================ FILE: src/main/java/com/mucommander/job/FileJobListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; /** * Interface to be implemented by classes that wish to be notified of state changes on a particular * {@link FileJob}. Those classes need to be registered to receive those events, this can be done by calling * {@link FileJob#addFileJobListener(FileJobListener)}. * * @author Maxence Bernard */ public interface FileJobListener { /** * Called when the state of the specified FileJob has changed. * * @param source the FileJob which state has changed * @param oldState the FileJob's state prior to the change, see FileJob's constant fields for possible values * @param newState the new FileJob's state, see FileJob's constant fields for possible values */ void jobStateChanged(FileJob source, FileJob.State oldState, FileJob.State newState); } ================================================ FILE: src/main/java/com/mucommander/job/FindFileJob.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.main.MainFrame; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.*; import org.jetbrains.annotations.NotNull; import ru.trolsoft.utils.search.*; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; /** * Job for directory scanning */ public class FindFileJob extends FileJob { private AbstractFile startDirectory; private String fileContent; private boolean searchSubdirectories; private boolean searchArchives; private boolean ignoreHidden; private SearchPattern searchPattern; private AbstractFileFilter fileFilter; private final List list = new ArrayList<>(); public FindFileJob(MainFrame mainFrame) { super(mainFrame); setAutoUnmark(false); } @Override protected boolean hasFolderChanged(AbstractFile folder) { return false; } @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted if (getState() == State.INTERRUPTED) { return false; } // If file is a directory, recurs if (file.isDirectory() && (!file.isSymlink() || file.equals(startDirectory))) { searchInFile(file); if (!searchSubdirectories && !file.equals(startDirectory)) { return true; } try { AbstractFile[] subFiles = file.ls(); for (int i = 0; i < subFiles.length && getState() != State.INTERRUPTED; i++) { if (ignoreHidden && file.isHidden()) { continue; } // Notify job that we're starting to process this file (needed for recursive calls to processFile) nextFile(subFiles[i]); processFile(subFiles[i], null); } } catch(Throwable e) { // Should we tell the user? } } else { // If not, increase file counter and bytes total if (!ignoreHidden || !file.isHidden()) { searchInFile(file); } } if (file.isArchive() && searchArchives) { try { AbstractFile[] subFiles = file.ls(); for (int i = 0; i < subFiles.length && getState() != State.INTERRUPTED; i++) { if (ignoreHidden && file.isHidden()) { continue; } // Notify job that we're starting to process this file (needed for recursive calls to processFile) nextFile(subFiles[i]); processFile(subFiles[i], null); } } catch(Throwable e) { // Should we tell the user? } } return true; } private void searchInFile(AbstractFile file) { File f = new File(file.toString()); if (fileFilter.accept(f) && fileContainsString(file)) { synchronized (this) { list.add(file); } } } private boolean fileContainsString(AbstractFile f) { //Profiler.start("check_new"); if (fileContent == null || fileContent.isEmpty()) { return true; } if (f.isDirectory()) { return false; } try (SearchSourceStream source = new InputStreamSource(f.getInputStream())) { long pos = SearchUtils.indexOf(source, searchPattern); //Profiler.stop("check_new"); return pos >= 0; } catch (IOException | SearchException e) { e.printStackTrace(); return false; } } public List getResults() { return list; } public void setStartDirectory(AbstractFile startDirectory) { this.startDirectory = startDirectory; FileSet fs = new FileSet(); fs.add(startDirectory); setFiles(fs); } public void setup(String fileMask, String fileContent, boolean searchSubdirs, boolean searchArchives, boolean caseSensitive, boolean ignoreHidden, String encoding, boolean hexMode, byte[] bytes) { fileMask = fileMask.trim(); fileMask = fileMask.isEmpty() ? "*" : fileMask; this.fileContent = fileContent; this.searchSubdirectories = searchSubdirs; this.searchArchives = searchArchives; this.ignoreHidden = ignoreHidden; IOCase filterCase = OsFamily.MAC_OS_X.isCurrent() || OsFamily.WINDOWS.isCurrent() ? IOCase.INSENSITIVE : IOCase.SENSITIVE; fileFilter = buildFileFilter(fileMask, filterCase); if (hexMode) { searchPattern = new BytesSearchPattern(bytes); } else { try { searchPattern = caseSensitive ? new StringCaseSensitiveSearchPattern(fileContent, encoding) : new StringCaseInsensitiveSearchPattern(fileContent, encoding); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } @NotNull private static AbstractFileFilter buildFileFilter(String fileMask, IOCase filterCase) { if (!fileMask.contains(",")) { String mask = fileMask.startsWith("!") ? fileMask.substring(1) : fileMask; return WildcardFileFilter.builder().setWildcards(mask.trim()).setIoCase(filterCase).get(); } return buildMultipleFileFilters(fileMask, filterCase); } @NotNull private static AbstractFileFilter buildMultipleFileFilters(String fileMask, IOCase filterCase) { String[] masks = fileMask.split(","); List fileFilters = new ArrayList<>(); boolean hasNot = false; for (String mask : masks) { String trimMask = mask.trim(); if (trimMask.isEmpty()) { continue; } if (trimMask.startsWith("!")) { var notMask = trimMask.substring(1).trim(); if (notMask.isEmpty()) { continue; } fileFilters.add(WildcardFileFilter.builder().setWildcards(notMask).setIoCase(filterCase).get()); hasNot = true; } else { fileFilters.add(WildcardFileFilter.builder().setWildcards(trimMask).setIoCase(filterCase).get()); } } return hasNot ? new AndFileFilter(fileFilters) : new OrFileFilter(fileFilters); } } ================================================ FILE: src/main/java/com/mucommander/job/MakeDirectoryFileJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import java.io.OutputStream; import com.mucommander.commons.file.*; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.macosx.AppleScript; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This FileJob creates a new file or directory. * * @author Maxence Bernard */ public class MakeDirectoryFileJob extends FileJob { private static final Logger LOGGER = LoggerFactory.getLogger(MakeDirectoryFileJob.class); private final AbstractFile destFolder; private final boolean mkfileMode; private long allocateSpace; private boolean executable; /** * Creates a new MakeDirectoryFileJob which operates in 'mkdir' mode. * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param fileSet files which are going to be processed */ public MakeDirectoryFileJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet fileSet) { super(progressDialog, mainFrame, fileSet); this.destFolder = fileSet.getBaseFolder(); this.mkfileMode = false; setAutoUnmark(false); } /** * Creates a new MakeDirectoryFileJob which operates in 'mkfile' mode. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param fileSet files which are going to be processed * @param allocateSpace number of bytes to allocate to the file, -1 for none (use AbstractFile#mkfile()) * @param executable set 'executable' attribute on unix-systems */ public MakeDirectoryFileJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet fileSet, long allocateSpace, boolean executable) { super(progressDialog, mainFrame, fileSet); this.destFolder = fileSet.getBaseFolder(); this.mkfileMode = true; this.allocateSpace = allocateSpace; this.executable = executable; setAutoUnmark(false); } //////////////////////////// // FileJob implementation // //////////////////////////// /** * Creates the new directory in the destination folder. */ @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted (although there is no way to stop the job at this time) if (getState() == State.INTERRUPTED) { return false; } do { try { LOGGER.debug("Creating {}", file); // Check for file collisions, i.e. if the file already exists in the destination int collision = FileCollisionChecker.checkForCollision(null, file); if (collision != FileCollisionChecker.NO_COLLISION) { // File already exists in destination, ask the user what to do (cancel, overwrite,...) but // do not offer the multiple files mode options such as 'skip' and 'apply to all'. var mainFrame = getMainFrame().getJFrame(); int choice = waitForUserResponse(new FileCollisionDialog(mainFrame, mainFrame, collision, null, file, false, false)); // Overwrite file if (choice == FileCollisionDialog.OVERWRITE_ACTION) { // Delete the file file.delete(); } // Cancel or dialog close (return) // else if (choice==-1 || choice==FileCollisionDialog.CANCEL_ACTION) { else { interrupt(); return false; } } if (mkfileMode) { // create file mkFile(file); } else { // create directory mkDir(file); } // Resolve new file instance now that it exists: remote files do not update file attributes after // creation, we need to get an instance that reflects the newly created file attributes file = FileFactory.getFile(file.getURL()); // Select newly created file when job is finished selectFileWhenFinished(file); return true; // Return Success } catch (IOException e) { // In mkfile mode, interrupting the job will close the OutputStream and cause an IOException to be // thrown, this is normal behavior if (mkfileMode && getState() == State.INTERRUPTED) { return false; } LOGGER.debug("IOException caught", e); boolean needAdminPermissions = e instanceof FileAccessDeniedException; int action; if (needAdminPermissions && !mkfileMode) { if (OsFamily.MAC_OS_X.isCurrent()) { tryMkDirAsAdministrator(file.getAbsolutePath(), null); return true; } else { action = showErrorDialog( Translator.get("error"), Translator.get(mkfileMode ? "cannot_write_file" : "cannot_create_folder", file.getAbsolutePath()), new String[]{RETRY_TEXT, RETRY_AS_ROOT_TEXT, RETRY_AS_ROOT_ALWAYS_TEXT, CANCEL_TEXT}, new int[]{RETRY_ACTION, RETRY_AS_ROOT_ACTION, RETRY_AS_ROOT_ALWAYS_ACTION, CANCEL_ACTION} ); } } else { action = showErrorDialog( Translator.get("error"), Translator.get(mkfileMode ? "cannot_write_file" : "cannot_create_folder", file.getAbsolutePath()), new String[] {RETRY_TEXT, CANCEL_TEXT}, new int[] {RETRY_ACTION, CANCEL_ACTION} ); } // Retry (loop) switch (action) { case RETRY_AS_ROOT_ACTION: String password = enterRootPasswordDialog(); tryMkDirAsAdministrator(file.getAbsolutePath(), password); continue; // case RETRY_AS_ROOT_ALWAYS_ACTION: // String password = enterRootPasswordDialog(); case RETRY_ACTION: continue; } // Cancel action return false; // Return Failure } } while(true); } private void tryMkDirAsAdministrator(String path, String password) { if (OsFamily.MAC_OS_X.isCurrent()) { AppleScript.execute("do shell script \"mkdir -p + '" + path + "' \" with administrator privileges", new StringBuilder()); } } private void mkFile(AbstractFile file) throws IOException { // Use mkfile if (allocateSpace == -1) { file.mkfile(); } // Allocate the requested number of bytes else { OutputStream mkfileOut = null; try { // using RandomAccessOutputStream if we can have one if (file.isFileOperationSupported(FileOperation.RANDOM_WRITE_FILE)) { mkfileOut = file.getRandomAccessOutputStream(); ((RandomAccessOutputStream)mkfileOut).setLength(allocateSpace); } // manually otherwise else { mkfileOut = file.getOutputStream(); // Use BufferPool to avoid excessive memory allocation and garbage collection byte[] buffer = BufferPool.getByteArray(); int bufferSize = buffer.length; try { long remaining = allocateSpace; while (remaining > 0 && getState() != State.INTERRUPTED) { int nbWrite = (int)(remaining > bufferSize ? bufferSize : remaining); mkfileOut.write(buffer, 0, nbWrite); remaining -= nbWrite; } } finally { BufferPool.releaseByteArray(buffer); } } } finally { if (mkfileOut != null) try { mkfileOut.close(); } catch (IOException ignore) { } } } // set 'executable' attribute if (executable && file.isFileOperationSupported(FileOperation.CHANGE_PERMISSION)) { try { file.changePermissions(FilePermissions.DEFAULT_EXECUTABLE_PERMISSIONS); } catch (IOException ignore) {} } } private void mkDir(AbstractFile file) throws IOException { file.mkdirs(); } /** * Folders only needs to be refreshed if it is the destination folder */ @Override protected boolean hasFolderChanged(AbstractFile folder) { return destFolder.equalsCanonical(folder); } //////////////////////// // Overridden methods // //////////////////////// @Override public String getStatusString() { return Translator.get("creating_file", getCurrentFilename()); } } ================================================ FILE: src/main/java/com/mucommander/job/MoveJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.AbstractRWArchiveFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.util.FileSet; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This job recursively moves a group of files. * * @author Maxence Bernard */ public class MoveJob extends AbstractCopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(MoveJob.class); /** True if this job corresponds to a single file renaming */ private final boolean renameMode; /** * Creates a new MoveJob without starting it. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be moved * @param destFolder destination folder where the files will be moved * @param newName the new filename in the destination folder, can be null in which case the original filename will be used. * @param fileExistsAction default action to be performed when a file already exists in the destination, see {@link com.mucommander.ui.dialog.file.FileCollisionDialog} for allowed values * @param renameMode true if this job corresponds to a single file renaming */ public MoveJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFolder, String newName, int fileExistsAction, boolean renameMode) { super(progressDialog, mainFrame, files, destFolder, newName, fileExistsAction); this.errorDialogTitle = Translator.get("move_dialog.error_title"); this.renameMode = renameMode; } //////////////////////////////////// // TransferFileJob implementation // //////////////////////////////////// /** * Moves recursively the given file or folder. * * @param file the file or folder to move * @param recurseParams destination folder where the given file will be moved (null for top level files) * * @return true if the file has been moved completly (copied + deleted). */ @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted if (getState() == State.INTERRUPTED) { return false; } // Destination folder AbstractFile destFolder = recurseParams == null ? baseDestFolder : (AbstractFile)recurseParams; // Is current file at the base folder level ? boolean isFileInBaseFolder = files.contains(file); // Determine filename in destination String originalName = file.getName(); String destFileName = isFileInBaseFolder && newName != null ? newName : originalName; // create destination AbstractFile instance AbstractFile destFile = createDestinationFile(destFolder, destFileName); if (destFile == null) { return false; } // Do not follow symlink, simply delete it and return // if (file.isSymlink()) { // do { // Loop for retry // try { // file.delete(); // return true; // } catch(IOException e) { // LOGGER.debug("IOException caught", e); // // int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_delete_file", file.getAbsolutePath())); // // Retry loops // if (ret == RETRY_ACTION) { // continue; // } // // Cancel, skip or close dialog returns false // return false; // } // } while(true); // } destFile = checkForCollision(file, destFolder, destFile, renameMode); if (destFile == null) { return false; } // Let's try to rename the file using AbstractFile#renameTo() whenever possible, as it is more efficient // than moving the file manually. // // Do not attempt to rename the file in the following cases: // - destination has to be appended // - file schemes do not match (at the time of writing, no filesystem supports mixed scheme renaming) // - if the 'rename' operation is not supported // Note: we want to avoid calling AbstractFile#renameTo when we know it will fail, as it performs some costly // I/O bound checks and ends up throwing an exception which also comes at a cost. if (!append && file.getURL().schemeEquals(destFile.getURL()) && file.isFileOperationSupported(FileOperation.RENAME)) { try { file.renameTo(destFile); return true; } catch(IOException e) { // Fail silently: renameTo might fail under normal conditions, for instance for local files which are // not located on the same volume. LOGGER.debug("Failed to rename "+file+" into "+destFile+" (not necessarily an error)", e); } } // Rename couldn't be used or didn't succeed: move the file manually // Move the directory and all its children recursively, by copying files to the destination and then deleting them. if (file.isDirectory()) { // create the destination folder if it doesn't exist if (!(destFile.exists() && destFile.isDirectory())) { do { // Loop for retry try { destFile.mkdir(); } catch(IOException e) { int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_create_folder", destFile.getAbsolutePath())); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel, skip or close dialog returns false return false; } break; } while(true); } // move each file in this folder recursively do { // Loop for retry try { AbstractFile[] subFiles = file.ls(); boolean isFolderEmpty = true; for (AbstractFile subFile : subFiles) { // Return now if the job was interrupted, so that we do not attempt to delete this folder if (getState() == State.INTERRUPTED) return false; // Notify job that we're starting to process this file (needed for recursive calls to processFile) nextFile(subFile); if (!processFile(subFile, destFile)) isFolderEmpty = false; } // Only when finished with folder, set destination folder's date to match the original folder one if (destFile.isFileOperationSupported(FileOperation.CHANGE_DATE)) { try { destFile.setLastModifiedDate(file.getLastModifiedDate()); } catch (IOException e) { LOGGER.debug("failed to change the date of "+destFile, e); // Fail silently } } // If one file failed to be moved, return false (failure) since this folder could not be moved totally if (!isFolderEmpty) { return false; } } catch (IOException e) { // file.ls() failed int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_read_folder", file.getName())); // Retry loops if (ret == RETRY_ACTION) continue; // Cancel, skip or close dialog returns false return false; } break; } while(true); // Return now if the job was interrupted, so that we do not attempt to delete this folder if (getState() == State.INTERRUPTED) { return false; } // finally, delete the empty folder return deleteEmptyFolder(file); } // File is a regular file, move it by copying it to the destination and then deleting it else { // if renameTo() was not supported or failed, or if it wasn't possible because of 'append', // try the hard way by copying the file first, and then deleting the source file. if (tryCopyFile(file, destFile, append, errorDialogTitle) && getState() != State.INTERRUPTED) { // Delete the source file do { // Loop for retry try { file.delete(); // All OK return true; } catch(IOException e) { LOGGER.debug("IOException caught", e); int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_delete_file", file.getAbsolutePath())); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel, skip or close dialog returns false return false; } } while(true); } return false; } } private boolean deleteEmptyFolder(AbstractFile file) { do { // Loop for retry try { file.delete(); return true; } catch(IOException e) { int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_delete_folder", file.getAbsolutePath())); // Retry loops if (ret == RETRY_ACTION) continue; // Cancel, skip or close dialog returns false return false; } } while(true); } // This job modifies baseDestFolder and its subfolders @Override protected boolean hasFolderChanged(AbstractFile folder) { return (getBaseSourceFolder()!=null && getBaseSourceFolder().isParentOf(folder)) || baseDestFolder.isParentOf(folder); } //////////////////////// // Overridden methods // //////////////////////// @Override protected void jobCompleted() { super.jobCompleted(); // If the source files are located inside an archive, optimize the archive file AbstractArchiveFile sourceArchiveFile = getBaseSourceFolder()==null?null:getBaseSourceFolder().getParentArchive(); if (sourceArchiveFile != null && sourceArchiveFile.isArchive() && sourceArchiveFile.isWritable()) { optimizeArchive((AbstractRWArchiveFile) sourceArchiveFile); } // If the destination files are located inside an archive, optimize the archive file, only if the destination // archive is different from the source one AbstractArchiveFile destArchiveFile = baseDestFolder.getParentArchive(); if (destArchiveFile != null && destArchiveFile.isArchive() && destArchiveFile.isWritable() && !(sourceArchiveFile != null && destArchiveFile.equalsCanonical(sourceArchiveFile))) { optimizeArchive((AbstractRWArchiveFile) destArchiveFile); } // If this job corresponds to a file renaming in the same directory, select the renamed file // in the active table after this job has finished (and hasn't been cancelled) if (files.size() == 1 && newName != null && baseDestFolder.equalsCanonical(files.elementAt(0).getParent())) { // Resolve new file instance now that it exists: some remote files do not immediately update file attributes // after creation, we need to get an instance that reflects the newly created file attributes selectFileWhenFinished(FileFactory.getFile(baseDestFolder.getAbsolutePath(true)+newName)); } } @Override public String getStatusString() { if (isCheckingIntegrity()) { return super.getStatusString(); } if (isOptimizingArchive) { return Translator.get("optimizing_archive", archiveToOptimize.getName()); } return Translator.get("move_dialog.moving_file", getCurrentFilename()); } } ================================================ FILE: src/main/java/com/mucommander/job/PropertiesJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.main.MainFrame; import java.io.IOException; /** * This FileJob calculates the number of files contained in a list of file and folders and * computes their size. * * @author Maxence Bernard */ public class PropertiesJob extends FileJob { /** Number of folders encountered so far */ private int nbFolders; /** Number of regular files (not folders) encountered so far */ private int nbFilesRecurse; /** Combined size of all files encountered so far */ private long totalBytes; public PropertiesJob(FileSet files, MainFrame mainFrame) { super(mainFrame, files); setAutoUnmark(false); } /** * Returns the size in bytes of all the files seen so far. */ public long getTotalBytes() { return totalBytes; } /** * Returns the number of folders counted so far. */ public int getNbFolders() { return nbFolders; } /** * Returns the number of files (folders excluded) counted so far. */ public int getNbFilesRecurse() { return nbFilesRecurse; } //////////////////////////// // FileJob implementation // //////////////////////////// /** * Adds the given file to the total of files or folders and the total size, * and recurses if it is a folder. */ @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted if (getState() == State.INTERRUPTED) return false; // If file is a directory, increase folder counter and recurse if (file.isDirectory() && !file.isSymlink()) { nbFolders++; try { AbstractFile[] subFiles = file.ls(); for (int i = 0; i < subFiles.length && getState() != State.INTERRUPTED; i++) { // Notify job that we're starting to process this file (needed for recursive calls to processFile) nextFile(subFiles[i]); processFile(subFiles[i], null); } } catch(IOException e) { // Should we tell the user? } } // If not, increase file counter and bytes total else { nbFilesRecurse++; long fileSize = file.getSize(); if (fileSize > 0) { // Can be equal to -1 if size not available totalBytes += fileSize; } } return true; } // This job does not modify anything @Override protected boolean hasFolderChanged(AbstractFile folder) { return false; } } ================================================ FILE: src/main/java/com/mucommander/job/SelfUpdateJob.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2021 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.updates.SelfUpdateUtils; import com.mucommander.utils.text.Translator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This job self-updates the trolCommander with a new JAR file that is fetched from a specified remote file. */ public class SelfUpdateJob extends CopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(SelfUpdateJob.class); private static final String TEMP_JAR_FILENAME = "trolcommander-new.jar"; public SelfUpdateJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractFile remoteJarFile) { super(progressDialog, mainFrame, new FileSet(remoteJarFile.getParent(), remoteJarFile), getTempJarFolder(), TEMP_JAR_FILENAME, CopyJob.Mode.DOWNLOAD, FileCollisionDialog.OVERWRITE_ACTION); } private static AbstractFile getApplicationJarFile() { return ResourceLoader.getRootPackageAsFile(SelfUpdateJob.class); } private static AbstractFile getTempJarFolder() { return getApplicationJarFile().getParent(); } // @Override // protected void jobStarted() { // super.jobStarted(); // try { //// // Loads all classes from the JAR file before the new JAR file is installed. //// // This will ensure that the shutdown sequence, which invokes so not-yet-loaded classes goes down smoothly. //// loadClassRecurse(destJar); //// loadingClasses = false; // } catch(Exception e) { // LOGGER.debug("Caught exception", e); // // TODO: display an error message // interrupt(); // } // } @Override protected void jobCompleted() { System.out.println("JOB COMPLETED"); System.out.println(SelfUpdateUtils.extractRestarter()); System.out.println(SelfUpdateUtils.checkRestarter()); System.out.println("SelfUpdateUtils.updateAndRestart()"); SelfUpdateUtils.updateAndRestart(); System.out.println("WindowManager.quit()"); WindowManager.quit(); System.out.println("System.exit(0)"); System.exit(0); //System.out.println(SelfUpdateUtils.getTcExecutionCommand()); // try { // // Mac OS X // if (OsFamily.MAC_OS_X.isCurrent() && executeOnMacOsX()) { // return; // } else if (executeDefault()) { // return; // } // // // No platform-specific launcher found, launch the Jar directly // ProcessRunner.execute(new String[]{"java", "-jar", destJar.getAbsolutePath()}); // } catch(IOException e) { // LOGGER.debug("Caught exception", e); // // TODO: we might want to do something about this // } finally { // WindowManager.quit(); // } } // @Override // protected boolean processFile(AbstractFile file, Object recurseParams) { // if (!super.processFile(file, recurseParams)) { // return false; // } // } @Override public String getStatusString() { return Translator.get("version_dialog.preparing_for_update"); } } ================================================ FILE: src/main/java/com/mucommander/job/SendMailJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.MimeTypes; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.io.StreamUtils; import com.mucommander.commons.io.base64.Base64OutputStream; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; /** * This job sends one or several files by email. * * @author Maxence Bernard */ public class SendMailJob extends TransferFileJob { /** True after connection to mail server has been established */ private boolean connectedToMailServer; /** Error dialog title */ private final String errorDialogTitle; ///////////////////// // Mail parameters // ///////////////////// /** Email recipient(s) */ private String recipientString; /** Email subject */ private final String mailSubject; /** Email body */ private final String mailBody; /** SMTP server */ private final String mailServer; /** From name */ private final String fromName; /** From address */ private final String fromAddress; /** Email boundary string, delimits the end of the body and attachments */ private final String boundary; /** Connection variable */ private BufferedReader in; /** OutputStream to the SMTP server */ private OutputStream out; /** Base64OutputStream to the SMTP server */ private Base64OutputStream out64; /** Socket connection to the SMTP server */ private Socket socket; private final static String CLOSE_TEXT = Translator.get("close"); private final static int CLOSE_ACTION = 11; public SendMailJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet filesToSend, String recipientString, String mailSubject, String mailBody) { super(progressDialog, mainFrame, filesToSend); this.boundary = "trolcommander" + System.currentTimeMillis(); this.recipientString = recipientString; this.mailSubject = mailSubject; this.mailBody = mailBody+"\n\n"+"Sent by trolCommander - http://www.trolsoft.ru\n"; this.mailServer = TcConfigurations.getPreferences().getVariable(TcPreference.SMTP_SERVER); this.fromName = TcConfigurations.getPreferences().getVariable(TcPreference.MAIL_SENDER_NAME); this.fromAddress = TcConfigurations.getPreferences().getVariable(TcPreference.MAIL_SENDER_ADDRESS); this.errorDialogTitle = Translator.get("email_dialog.error_title"); } /** * Returns true if mail preferences have been set. */ public static boolean mailPreferencesSet() { return TcConfigurations.getPreferences().isVariableSet(TcPreference.SMTP_SERVER) && TcConfigurations.getPreferences().isVariableSet(TcPreference.MAIL_SENDER_NAME) && TcConfigurations.getPreferences().isVariableSet(TcPreference.MAIL_SENDER_ADDRESS); } /** * Shows an error dialog with a single action : close, and stops the job. */ private void showErrorDialog(String message) { showErrorDialog(errorDialogTitle, message, new String[]{CLOSE_TEXT}, new int[]{CLOSE_ACTION}); interrupt(); } ///////////////////////////////////////////// // Methods taking care of sending the mail // ///////////////////////////////////////////// private void openConnection() throws IOException { this.socket = new Socket(mailServer, TcConfigurations.getPreferences().getVariable(TcPreference.SMTP_PORT, TcPreferences.DEFAULT_SMTP_PORT)); this.in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); this.out = socket.getOutputStream(); this.out64 = new Base64OutputStream(out, true); this.connectedToMailServer = true; } private void sendBody() throws IOException { // here you are supposed to send your username readWriteLine("HELO trolCommander"); // warning : some mail server validate the sender address and will fail is an invalid // address is provided readWriteLine("MAIL FROM: "+fromAddress); List recipients = new ArrayList<>(); recipientString = splitRecipientString(recipientString, recipients); for (String recipient : recipients) readWriteLine("RCPT TO: <" + recipient + ">"); readWriteLine("DATA"); writeLine("MIME-Version: 1.0"); writeLine("Subject: "+this.mailSubject); writeLine("From: "+this.fromName+" <"+this.fromAddress+">"); writeLine("To: "+recipientString); writeLine("Content-Type: multipart/mixed; boundary=\"" + boundary +"\""); writeLine("\r\n--" + boundary); // Send the body // writeLine( "Content-Type: text/plain; charset=\"us-ascii\"\r\n"); writeLine("Content-Type: text/plain; charset=\"utf-8\"\r\n"); writeLine(this.mailBody+"\r\n\r\n"); writeLine("\r\n--" + boundary ); } /** * Parses the specified string, replaces delimiter characters if needed and adds recipients (String instances) to the given Vector. * * @param recipientsStr String containing one or several recipients that need to be separated by ',' and/or ';' characters. */ private String splitRecipientString(String recipientsStr, List recipients) { // /!\ this piece of code is far from being bulletproof, but I'm too lazy now to rewrite it StringBuilder newRecipientsSb = new StringBuilder(); StringTokenizer st = new StringTokenizer(recipientsStr, ",;"); while(st.hasMoreTokens()) { String rec = st.nextToken().trim(); int pos1, pos2; if ((pos1 = rec.indexOf('<')) != -1 && (pos2 = rec.indexOf('>', pos1+1)) != -1) { recipients.add(rec.substring(pos1+1, pos2)); } else { recipients.add(rec); } newRecipientsSb.append(rec); if (st.hasMoreTokens()) { newRecipientsSb.append(", "); } } return newRecipientsSb.toString(); } /** * Send file as attachment encoded in Base64, and returns true if file was successfully * and completely transferred. */ private void sendAttachment(AbstractFile file) throws IOException { // Send MIME type of attachment file String mimeType = MimeTypes.getMimeType(file); // Default mime type if (mimeType == null) { mimeType = "application/octet-stream"; } writeLine("Content-Type:"+mimeType+"; name="+file.getName()); writeLine("Content-Disposition: attachment;filename=\""+file.getName()+"\""); writeLine("Content-transfer-encoding: base64\r\n"); try (InputStream fileIn = setCurrentInputStream(file.getInputStream())) { // Write file to socket StreamUtils.copyStream(fileIn, out64); // Writes padding bytes without closing the stream. out64.writePadding(); writeLine("\r\n--" + boundary); } } private void sayGoodBye() throws IOException { writeLine("\r\n\r\n--" + boundary + "--\r\n"); readWriteLine("."); readWriteLine("QUIT"); } private void closeConnection() { try { socket.close(); in.close(); out64.close(); } catch(Exception ignore) {} } private void readWriteLine(String s) throws IOException { out.write((s + "\r\n").getBytes(StandardCharsets.UTF_8)); in.readLine(); } private void writeLine(String s) throws IOException { out.write((s + "\r\n").getBytes(StandardCharsets.UTF_8)); } //////////////////////////////////// // TransferFileJob implementation // //////////////////////////////////// @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if (getState() == State.INTERRUPTED) return false; // Send file attachment try { sendAttachment(file); } catch(IOException e) { showErrorDialog(Translator.get("email.send_file_error", file.getName())); return false; } // If this was the last file, notify the mail server that the mail is over if(getCurrentFileIndex()==getNbFiles()-1) { try { // Say goodbye to the server sayGoodBye(); } catch(IOException e) { showErrorDialog(Translator.get("email.goodbye_failed")); return false; } } return true; } @Override protected boolean hasFolderChanged(AbstractFile folder) { // This job does not modify anything return false; } /////////////////////// // Overridden method // /////////////////////// /** * This method is called when this job starts, before the first call to {@link #processFile(AbstractFile,Object) processFile()} is made. * This method here does nothing, but it can be overridden by subclasses to perform some first-time initializations. */ @Override protected void jobStarted() { super.jobStarted(); // Open socket connection to the mail server, and say hello try { openConnection(); } catch(IOException e) { showErrorDialog(Translator.get("email.server_unavailable", mailServer)); } if (getState() == State.INTERRUPTED) return; // Send mail body try { sendBody(); } catch(IOException e) { showErrorDialog(Translator.get("email.connection_closed")); } } /** * Method overridden to close connection to the mail server. */ @Override protected void jobStopped() { super.jobStopped(); // Close the connection closeConnection(); } @Override public String getStatusString() { if (connectedToMailServer) { return Translator.get("email.sending_file", getCurrentFilename()); } else { return Translator.get("email.connecting_to_server", mailServer); } } } ================================================ FILE: src/main/java/com/mucommander/job/SplitFileJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.DummyFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.ChecksumInputStream; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.StreamUtils; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.SplitFileAction; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This job split the file into parts with given size. * @author Mariusz Jakubowski */ public class SplitFileJob extends AbstractCopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(SplitFileJob.class); private final long partSize; private final AbstractFile sourceFile; private InputStream origFileStream; private final AbstractFile destFolder; private long sizeLeft; private boolean recalculateCRC = false; /** * A class for holding file name and size of one part. * @author Mariusz Jakubowski * */ private static class DummyDestFile extends DummyFile { private final long size; public DummyDestFile(FileURL url, long size) { super(url); this.size = size; } @Override public long getSize() { return size; } } public SplitFileJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractFile file, AbstractFile destFolder, long partSize, int parts) { super(progressDialog, mainFrame, new FileSet(), destFolder, null, FileCollisionDialog.ASK_ACTION); this.partSize = partSize; this.setNbFiles(parts); this.sourceFile = file; this.destFolder = destFolder; this.errorDialogTitle = Translator.get("split_file_dialog.error_title"); createInputStream(); sizeLeft = sourceFile.getSize(); for (int i=1; i<=parts; i++) { addDummyFile(i, Math.min(partSize, sizeLeft)); sizeLeft -= partSize; } sizeLeft = sourceFile.getSize(); } /** * Adds a dummy output file (used in progress monitoring). * @param i index of a file * @param size size of a file */ private void addDummyFile(int i, long size) { String num; if (i < 10) { num = "00" + i; } else if (i < 100) { num = "0" + i; } else { num = Integer.toString(i); } FileURL childURL = (FileURL)destFolder.getURL().clone(); childURL.setPath(destFolder.addTrailingSeparator(childURL.getPath()) + sourceFile.getName() + "." + num); DummyDestFile fileHolder = new DummyDestFile(childURL, size); files.add(fileHolder); } @Override protected void jobStarted() { super.jobStarted(); createInputStream(); } /** * Creates an input stream from the file. */ private void createInputStream() { try { origFileStream = sourceFile.getInputStream(); } catch (IOException e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", sourceFile.getName()), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); setState(State.INTERRUPTED); return; } origFileStream = setCurrentInputStream(origFileStream); // init checksum calculation if (isIntegrityCheckEnabled()) { try { origFileStream = new ChecksumInputStream(origFileStream, MessageDigest.getInstance("CRC32")); } catch (NoSuchAlgorithmException e) { setIntegrityCheckEnabled(false); LOGGER.debug("Caught exception", e); } } } @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if (getState() == State.INTERRUPTED) return false; // create destination AbstractFile instance AbstractFile destFile = createDestinationFile(baseDestFolder, file.getName()); if (destFile == null) return false; destFile = checkForCollision(sourceFile, baseDestFolder, destFile, false); if (destFile == null) return false; OutputStream out = null; try { out = destFile.getOutputStream(); try { long written = StreamUtils.copyStream(origFileStream, out, BufferPool.getDefaultBufferSize(), partSize); sizeLeft -= written; } catch (FileTransferException e) { if (e.getReason() == FileTransferException.WRITING_DESTINATION) { // out of disk space - ask a user for a new disk recalculateCRC = true; // recalculate CRC (DigestInputStream doesn't support mark() and reset()) out.close(); out = null; sizeLeft -= e.getBytesWritten(); showErrorDialog(ActionProperties.getActionLabel(SplitFileAction.Descriptor.ACTION_ID), Translator.get("split_file_dialog.insert_new_media"), new String[]{OK_TEXT, CANCEL_TEXT}, new int[]{OK_ACTION, CANCEL_ACTION}); if (getState() == State.INTERRUPTED) { return false; } // create new output file if necessary if ((sizeLeft>0) && (getCurrentFileIndex() == getNbFiles()-1)) { setNbFiles(getNbFiles() + 1); addDummyFile(getNbFiles(), sizeLeft); } } else { throw e; } } // Preserve source file's date if(destFile.isFileOperationSupported(FileOperation.CHANGE_DATE)) { try { destFile.setLastModifiedDate(sourceFile.getLastModifiedDate()); } catch (IOException e) { LOGGER.debug("failed to change date of "+destFile, e); // Fail silently } } // Preserve source file's permissions: preserve only the permissions bits that are supported by the source // file and use default permissions for the rest of them. if(destFile.isFileOperationSupported(FileOperation.CHANGE_PERMISSION)) { try { // use #importPermissions(AbstractFile, int) to avoid isDirectory test destFile.importPermissions(sourceFile, FilePermissions.DEFAULT_FILE_PERMISSIONS); } catch (IOException e) { LOGGER.debug("failed to import "+sourceFile+" permissions into "+destFile, e); // Fail silently } } } catch (IOException e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", destFile.getName()), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); return false; } finally { try { if (out != null) out.close(); } catch(IOException ignore) { } } return true; } // This job modifies baseDestFolder and its subfolders @Override protected boolean hasFolderChanged(AbstractFile folder) { return baseDestFolder.isParentOf(folder); } @Override protected void jobCompleted() { // create checksum file if (isIntegrityCheckEnabled()) { if(origFileStream!=null && (origFileStream instanceof ChecksumInputStream)) { String crcFileName = sourceFile.getName() + ".sfv"; try { String sourceChecksum; if (recalculateCRC ) { origFileStream = sourceFile.getInputStream(); sourceChecksum = AbstractFile.calculateChecksum(origFileStream, MessageDigest.getInstance("CRC32")); origFileStream.close(); } else { sourceChecksum = ((ChecksumInputStream)origFileStream).getChecksumString(); } AbstractFile crcFile = baseDestFolder.getDirectChild(crcFileName); OutputStream crcStream = crcFile.getOutputStream(); String line = sourceFile.getName() + " " + sourceChecksum; crcStream.write(line.getBytes(StandardCharsets.UTF_8)); crcStream.close(); } catch (Exception e) { LOGGER.debug("Caught exception", e); showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", crcFileName), new String[]{CANCEL_TEXT}, new int[]{CANCEL_ACTION} ); } } } super.jobCompleted(); } } ================================================ FILE: src/main/java/com/mucommander/job/TempCopyJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This job copies a file or a set of files to a temporary folder and makes the temporary file(s) read-only. * The temporary files are deleted when the JVM terminates. * * @author Maxence Bernard */ public class TempCopyJob extends CopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(TempCopyJob.class); /** * Creates a new TempExecJob that operates on a single file. * * @param progressDialog the ProgressDialog that monitors this job * @param mainFrame the MainFrame this job is attached to * @param fileToCopy the file to copy to a temporary location */ public TempCopyJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractFile fileToCopy) { super(progressDialog, mainFrame, new FileSet(fileToCopy.getParent(), fileToCopy), FileFactory.getTemporaryFolder(), getTemporaryFileName(fileToCopy), Mode.COPY, FileCollisionDialog.OVERWRITE_ACTION); } /** * Creates a new TempExecJob that operates on a single file. * * @param progressDialog the ProgressDialog that monitors this job * @param mainFrame the MainFrame this job is attached to * @param filesToCopy the file to copy to a temporary location */ public TempCopyJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet filesToCopy) { super(progressDialog, mainFrame, filesToCopy, getTemporaryFolder(filesToCopy), null, Mode.COPY, FileCollisionDialog.OVERWRITE_ACTION); } protected static AbstractFile getTemporaryFolder(FileSet files) { AbstractFile tempFolder; try { tempFolder = FileFactory.getTemporaryFile(files.getBaseFolder().getName(), true); tempFolder.mkdir(); } catch(IOException e) { tempFolder = FileFactory.getTemporaryFolder(); } return tempFolder; } protected static String getTemporaryFileName(AbstractFile files) { try { return FileFactory.getTemporaryFile(files.getName(), true).getName(); } catch(IOException e) { // Should never happen under normal circumstances. LOGGER.warn("Caught exception instanciating temporary file, this should not happen!"); return files.getName(); } } } ================================================ FILE: src/main/java/com/mucommander/job/TempExecJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.PermissionAccesses; import com.mucommander.commons.file.PermissionTypes; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.quicklist.RecentExecutedFilesQL; /** * This job copies a file or a set of files to a temporary folder, makes the temporary file(s) read-only and * executes each of them with native file associations. The temporary files are deleted when the JVM terminates. * *

    It is important to understand that when this job operates on a set of files, a process is started for each file * to execute, so this operation should require confirmation by the user before being attempted. * * @author Maxence Bernard */ public class TempExecJob extends TempCopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(TempExecJob.class); /** Files to execute */ private final FileSet filesToExecute; /** * Creates a new TempExecJob that operates on a single file. * * @param progressDialog the ProgressDialog that monitors this job * @param mainFrame the MainFrame this job is attached to * @param fileToExecute the file to copy to a temporary location and execute */ public TempExecJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractFile fileToExecute) { this(progressDialog, mainFrame, new FileSet(fileToExecute.getParent(), fileToExecute)); } /** * Creates a new TempExecJob that operates on a set of files. Only a single command get executed, operating on * all files. * * @param progressDialog the ProgressDialog that monitors this job * @param mainFrame the MainFrame this job is attached to * @param filesToExecute the set of files to copy to a temporary location and execute */ public TempExecJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet filesToExecute) { super(progressDialog, mainFrame, filesToExecute); this.filesToExecute = filesToExecute; } //////////////////////// // Overridden methods // //////////////////////// @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if (!super.processFile(file, recurseParams)) { return false; } // TODO: temporary files seem to remain after the JVM quits under Mac OS X, even if the files permissions are unchanged // Execute the file, only if it is one of the top-level files if (!filesToExecute.contains(file)) { return true; } if (!currentDestFile.isDirectory()) { // Do not change directories' permissions try { // Make the temporary file read only if (currentDestFile.getChangeablePermissions().getBitValue(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION)) currentDestFile.changePermission(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION, false); } catch(IOException e) { LOGGER.debug("Caught exception while changing permissions of {}", currentDestFile, e); return false; } } return openFile(file); } private boolean openFile(AbstractFile file) { try { DesktopManager.open(currentDestFile); RecentExecutedFilesQL.addFile(file); return true; } catch(Exception e) { LOGGER.debug("Caught exception while opening {}", currentDestFile, e); return false; } } } ================================================ FILE: src/main/java/com/mucommander/job/TempOpenWithJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.command.Command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.PermissionAccesses; import com.mucommander.commons.file.PermissionTypes; import com.mucommander.commons.file.util.FileSet; import com.mucommander.process.ProcessRunner; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * This job copies a file or a set of files to a temporary folder, makes the temporary file(s) read-only and * executes them with a specific command. The temporary files are deleted when the JVM terminates. * *

    It is important to understand that when this job operates on a set of files, a process is started for each file * to execute, so this operation should require confirmation by the user before being attempted. * * @author Maxence Bernard, Nicolas Rinaudo */ public class TempOpenWithJob extends TempCopyJob { private static final Logger LOGGER = LoggerFactory.getLogger(TempOpenWithJob.class); /** The command to execute, appended with the temporary file path(s) */ private final Command command; /** Files to execute */ private final FileSet filesToOpen; /** This list is populated with temporary files, as they are created by processFile() */ private final FileSet tempFiles; /** * Creates a new TempOpenWithJob that operates on a single file. * * @param progressDialog the ProgressDialog that monitors this job * @param mainFrame the MainFrame this job is attached to * @param fileToOpen the file to copy to a temporary location and execute * @param command the command used to execute the temporary file */ public TempOpenWithJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractFile fileToOpen, Command command) { this(progressDialog, mainFrame, new FileSet(fileToOpen.getParent(), fileToOpen), command); } /** * Creates a new TempOpenWithJob that operates on a set of files. Only a single command get executed, operating on * all files. * * @param progressDialog the ProgressDialog that monitors this job * @param mainFrame the MainFrame this job is attached to * @param filesToOpen the set of files to copy to a temporary location and execute * @param command the command used to execute the temporary file */ public TempOpenWithJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet filesToOpen, Command command) { super(progressDialog, mainFrame, filesToOpen); this.command = command; this.filesToOpen = filesToOpen; tempFiles = new FileSet(baseDestFolder); } //////////////////////// // Overridden methods // //////////////////////// @Override protected boolean processFile(AbstractFile file, Object recurseParams) { if(!super.processFile(file, recurseParams)) return false; // TODO: temporary files seem to be left after the JVM quits under Mac OS X, even if the files permissions are unchanged // Add the file to the list of files to open, only if it is one of the top-level files if(filesToOpen.contains(file)) { if(!currentDestFile.isDirectory()) { // Do not change directories' permissions try { // Make the temporary file read only if(currentDestFile.getChangeablePermissions().getBitValue(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION)) currentDestFile.changePermission(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION, false); } catch(IOException e) { LOGGER.debug("Caught exception while changing permissions of {}", currentDestFile, e); return false; } } tempFiles.add(currentDestFile); } return true; } @Override protected void jobCompleted() { super.jobCompleted(); try { ProcessRunner.execute(command.getTokens(tempFiles), baseDestFolder); } catch(Exception e) { LOGGER.debug("Caught exception executing {} {}", command, tempFiles, e); } } } ================================================ FILE: src/main/java/com/mucommander/job/TransferFileJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.adb.AdbFile; import com.mucommander.desktop.DesktopManager; import lombok.Getter; import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.apple.eio.FileManager; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.io.ByteCounter; import com.mucommander.commons.io.ChecksumInputStream; import com.mucommander.commons.io.CounterInputStream; import com.mucommander.commons.io.FileTransferException; import com.mucommander.commons.io.ThroughputLimitInputStream; import com.mucommander.commons.io.security.MuProvider; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * TransferFileJob is a container for a file getTask : basically an operation that involves files and bytes.
    * *

    What makes TransferFileJob different from FileJob (and explains its very inspired name) is that a class * implementing TransferFileJob has to be able to give progress information about the file currently being processed. * * @author Maxence Bernard */ public abstract class TransferFileJob extends FileJob { private static final Logger LOGGER = LoggerFactory.getLogger(TransferFileJob.class); /** Contains the number of bytes processed in the current file so far, see {@link #getCurrentFileByteCounter()} ()} * -- GETTER -- * Returns the number of bytes that have been processed in the current file. */ @Getter private final ByteCounter currentFileByteCounter; /** Contains the number of bytes skipped in the current file so far, see {@link #getCurrentFileSkippedByteCounter()} ()} */ private final ByteCounter currentFileSkippedByteCounter; /** Contains the number of bytes processed so far, see {@link #getTotalByteCounter()} * -- GETTER -- * Returns a that holds the total number of bytes that have been processed by this job so far. */ @Getter private final ByteCounter totalByteCounter; /** Contains the number of bytes skipped so far (resumed files), see {@link #getTotalSkippedByteCounter()} * -- GETTER -- * Returns a * that holds the total number of bytes that have been skipped by this job so far. * Bytes are skipped when file transfers are resumed. */ @Getter private final ByteCounter totalSkippedByteCounter; /** InputStream currently being processed, may be null */ private ThroughputLimitInputStream tlin; /** ThroughputLimit in bytes per second, -1 initially (no limit) * -- GETTER -- * Returns the current transfer throughput limit, in bytes per second. 0 or -1 means that * there currently is no limit to the attainable transfer speed (full speed). */ @Getter private long throughputLimit = -1; /** Has the file currently being processed been skipped ? */ private boolean currentFileSkipped; /** If true, all transfers will be checked for integrity: the checksum of the source and destination file will * be calculated and compared to verify they match. * -- SETTER -- * Specifies if file transfers need to be checked for data integrity. If true is specified, the * checksum of the source and destination files will both be calculated and compared to verify they match. */ @Setter private boolean integrityCheckEnabled; /** True when the checksum of the source or destination file is being calculated. */ private boolean isCheckingIntegrity; /** The checksum algorithm used for checking the integrity of transferred files. The algorithm has to be the fastest * possible (to have the minimum impact on transfer speed) and does not need to have a good resitance to collision. */ private final static String CHECKSUM_VERIFICATION_ALGORITHM = "Adler32"; /** * If user changed "Overwrite all readonly" in the question dialog */ private boolean overwriteAllReadonly = false; static { // Register additional MessageDigest implementations provided by the muCommander API MuProvider.registerProvider(); } /** * Creates a new TransferFileJob. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be processed */ public TransferFileJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files) { super(progressDialog, mainFrame, files); this.currentFileByteCounter = new ByteCounter(); this.currentFileSkippedByteCounter = new ByteCounter(); // Account the current file's byte counter in the total byte counter this.totalByteCounter = new ByteCounter(currentFileByteCounter); this.totalSkippedByteCounter = new ByteCounter(currentFileSkippedByteCounter); } void copyToReadonlyFile(AbstractFile sourceFile, AbstractFile destFile, boolean append) throws FileTransferException { try { destFile.changePermission(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION, true); copyFile(sourceFile, destFile, append); destFile.changePermission(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION, false); } catch (IOException e) { LOGGER.error("Copy error", e); throw new FileTransferException(FileTransferException.OPENING_DESTINATION); } } /** * Copies the given source file to the specified destination file, optionally resuming the operation. * As much as the source and destination protocols allow, the source file's date and permissions will be preserved. * * @param sourceFile source file * @param destFile destination file * @param append append or overwrite * @throws FileTransferException on transfer error */ private void copyFile(AbstractFile sourceFile, AbstractFile destFile, boolean append) throws FileTransferException { // Reset this field in case it was set to true for the previous file isCheckingIntegrity = false; // Throw a specific FileTransferException if source and destination files are identical if (sourceFile.equalsCanonical(destFile)) { throw new FileTransferException(FileTransferException.SOURCE_AND_DESTINATION_IDENTICAL); } // Determine whether AbstractFile.copyRemotelyTo() should be used to copy the file. // Some file protocols do not provide a getOutputStream() method and require the use of copyRemotelyTo(). Some other // may also offer server to server copy which is more efficient than stream copy. boolean copied = false; if (sourceFile.isFileOperationSupported(FileOperation.COPY_REMOTELY)) { try { sourceFile.copyRemotelyTo(destFile); copied = true; } catch(IOException e) { // The file will be copied manually } } // If the file wasn't copied using copyRemotelyTo(), or if copyRemotelyTo() failed InputStream in = null; if (!copied) { // Copy source file stream to destination file try { long inLength = sourceFile.getSize(); // Try to open InputStream try { long destFileSize = destFile.getSize(); if (append && destFileSize > 0) { in = sourceFile.getInputStream(destFileSize); // Do not calculate checksum, as it needs to be calculated on the whole file inLength -= destFileSize; // Increase current file ByteCounter by the number of bytes skipped currentFileByteCounter.add(destFileSize); // Increase skipped ByteCounter by the number of bytes skipped currentFileSkippedByteCounter.add(destFileSize); } else { in = sourceFile.getInputStream(); if (integrityCheckEnabled) { in = new ChecksumInputStream(in, MessageDigest.getInstance(CHECKSUM_VERIFICATION_ALGORITHM)); } } setCurrentInputStream(in); } catch(Exception e) { LOGGER.debug("IOException caught, throwing FileTransferException", e); throw new FileTransferException(FileTransferException.OPENING_SOURCE); } if (destFile instanceof AdbFile adbFile) { try { adbFile.pullFrom(sourceFile); } catch (IOException e) { e.printStackTrace(); throw new FileTransferException(FileTransferException.WRITING_DESTINATION); } System.out.println(sourceFile.getClass().getName()); System.out.println(sourceFile + " -> " + destFile); return; } // Copy source stream to destination file destFile.copyStream(tlin, append, inLength); } finally { // This block will always be executed, even if an exception // was thrown in the catch block // Tries to close the streams no matter what happened before closeCurrentInputStream(); } } tryCopyFileDate(sourceFile, destFile); tryCopyFilePermissions(sourceFile, destFile); tryCopyFileTypeAndCreator(sourceFile, destFile); // This block is executed only if integrity check has been enabled (disabled by default) if (integrityCheckEnabled) { // Indicate that integrity is being checked, the value is reset when the next file starts isCheckingIntegrity = true; String sourceChecksum; if (in instanceof ChecksumInputStream) { // The file was copied with a ChecksumInputStream, the checksum is already calculated, simply // retrieve it sourceChecksum = ((ChecksumInputStream)in).getChecksumString(); } else { // The file was copied using AbstractFile#copyRemotelyTo(), or the transfer was resumed: // we have to calculate the source file's checksum from scratch. try { sourceChecksum = calculateChecksum(sourceFile); } catch (Exception e) { throw new FileTransferException(FileTransferException.READING_SOURCE); } } LOGGER.debug("Source checksum= "+sourceChecksum); // Calculate the destination file's checksum String destinationChecksum; try { destinationChecksum = calculateChecksum(destFile); } catch(Exception e) { throw new FileTransferException(FileTransferException.READING_DESTINATION); } LOGGER.debug("Destination checksum= "+destinationChecksum); // Compare both checksums and throw an exception if they don't match if (!sourceChecksum.equals(destinationChecksum)) { throw new FileTransferException(FileTransferException.CHECKSUM_MISMATCH); } } } private String calculateChecksum(AbstractFile file) throws IOException, NoSuchAlgorithmException { currentFileByteCounter.reset(); InputStream in = setCurrentInputStream(file.getInputStream()); try { return AbstractFile.calculateChecksum(in, MessageDigest.getInstance(CHECKSUM_VERIFICATION_ALGORITHM)); } finally { closeCurrentInputStream(); } } /** * Tries to copy the given source file to the specified destination file (see {@link #copyFile(AbstractFile,AbstractFile,boolean)} * displaying a generic error dialog {@link #showErrorDialog(String, String) #showErrorDialog()} if something went wrong, * and giving the user the choice to skip the file, retry or cancel. * * @return true if the file was properly copied, false if the transfer was interrupted / aborted by the user * */ boolean tryCopyFile(AbstractFile sourceFile, AbstractFile destFile, boolean append, String errorDialogTitle) { boolean overwriteReadonly = false; // Copy file to destination do { // Loop for retry try { if (overwriteReadonly) { copyToReadonlyFile(sourceFile, destFile, append); } else { copyFile(sourceFile, destFile, append); } return true; } catch(FileTransferException e) { // If the job was interrupted by the user at the time the exception occurred, it most likely means that // the IOException was caused by the stream being closed as a result of the user interruption. // If that is the case, the exception should not be interpreted as an error. // Same goes if the current file was skipped. if (getState() == State.INTERRUPTED || wasCurrentFileSkipped()) { return false; } // Print the exception's stack trace LOGGER.debug("Copy failed", e); int reason = e.getReason(); int choice; switch(reason) { // Could not open source file for read case FileTransferException.OPENING_SOURCE: choice = showErrorDialog(errorDialogTitle, Translator.get("cannot_read_file", sourceFile.getName())); break; // Could not open destination file for write case FileTransferException.UNSUPPORTED_OPERATION: choice = showErrorDialog(errorDialogTitle, Translator.get("error_unsupported_operation"), // from the perspective of users there is nothing to cancel but only to acknowledge new String[] { OK_TEXT }, // technically we're cancelling here new int[] { CANCEL_ACTION }); break; case FileTransferException.OPENING_DESTINATION: // if write to read-only file if (!destFile.getPermissions().getBitValue(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION)) { if (overwriteAllReadonly) { choice = OVERWRITE_READONLY_ACTION; } else { String[] actionTexts = new String[]{SKIP_TEXT, SKIP_ALL_TEXT, OVERWRITE_READONLY_TEXT, OVERWRITE_READONLY_ALL_TEXT, CANCEL_TEXT}; int[] actionValues = new int[]{SKIP_ACTION, SKIP_ALL_ACTION, OVERWRITE_READONLY_ACTION, OVERWRITE_READONLY_ALL_ACTION, CANCEL_ACTION}; choice = showErrorDialog(errorDialogTitle, Translator.get("overwrite_readonly_file", destFile.getName()), actionTexts, actionValues); } } else { choice = showErrorDialog(errorDialogTitle, Translator.get("cannot_write_file", destFile.getName())); } break; // Source and destination files are identical case FileTransferException.SOURCE_AND_DESTINATION_IDENTICAL: choice = showErrorDialog(errorDialogTitle, Translator.get("same_source_destination")); break; // Checksum of source and destination files don't match case FileTransferException.CHECKSUM_MISMATCH: choice = showErrorDialog(errorDialogTitle, Translator.get("integrity_check_error")); break; default: choice = showErrorDialog(errorDialogTitle, Translator.get("error_while_transferring", sourceFile.getName()), new String[]{SKIP_TEXT, SKIP_ALL_TEXT, APPEND_TEXT, RETRY_TEXT, CANCEL_TEXT}, new int[]{SKIP_ACTION, SKIP_ALL_ACTION, APPEND_ACTION, RETRY_ACTION, CANCEL_ACTION} ); break; } // Retry action (append or retry) if (choice == RETRY_ACTION || choice == APPEND_ACTION || choice == OVERWRITE_READONLY_ACTION || choice == OVERWRITE_READONLY_ALL_ACTION) { // Reset current file byte counters currentFileByteCounter.reset(); currentFileSkippedByteCounter.reset(); // Append resumes transfer append = choice == APPEND_ACTION; overwriteReadonly = choice == OVERWRITE_READONLY_ACTION || choice == OVERWRITE_READONLY_ALL_ACTION; if (choice == OVERWRITE_READONLY_ALL_ACTION) { overwriteAllReadonly = true; } continue; } // Skip or Cancel action (stop() is already called by showErrorDialog) return false; } } while(true); } /** * Registers the given InputStream as currently in use, in order to: *

      *
    • count the number of bytes that have been read from it (see {@link #getCurrentFileByteCounter()}) *
    • block read methods calls when the job is paused *
    • limit the throughput if a limit has been specified (see {@link #setThroughputLimit(long)}) *
    • close the InputStream when the job is stopped *
    * *

    This method should be called by subclasses when creating a new InputStream, before the InputStream is used. * * @param in the InputStream to be used * @return the 'augmented' InputStream using the given stream as the underlying InputStream */ synchronized InputStream setCurrentInputStream(InputStream in) { if (tlin == null) { tlin = new ThroughputLimitInputStream(new CounterInputStream(in, currentFileByteCounter), throughputLimit); } else { tlin.setUnderlyingInputStream(new CounterInputStream(in, currentFileByteCounter)); } return tlin; } /** * Closes the currently registered source InputStream. */ synchronized void closeCurrentInputStream() { if (tlin != null) { try { tlin.close(); } catch(IOException e) { LOGGER.error("Close error", e); } } } /** * Returns true if file transfers need to be checked for data integrity. In this case, the checksum of * the source and destination files are both calculated and compared to verify they match. * * @return true if file transfers need to be checked for data integrity */ boolean isIntegrityCheckEnabled() { return integrityCheckEnabled; } /** * Returns true if the integrity of the current file is being verified. * * @return true if the integrity of the current file is being verified */ boolean isCheckingIntegrity() { return isCheckingIntegrity; } /** * Interrupts the current file transfer and advance to the next one. */ public synchronized void skipCurrentFile() { if (tlin != null) { LOGGER.debug("skipping current file, closing "+ tlin); // Prevents an error from being reported when the current InputStream is closed currentFileSkipped = true; // Close the current input stream to interrupt the transfer closeCurrentInputStream(); } // Resume job if currently paused if (getState() == State.PAUSED) { setPaused(false); } } /** * Return true if the file that is currently being processed has been skipped. * * @return true if the file that is currently being processed has been skipped */ synchronized boolean wasCurrentFileSkipped() { return currentFileSkipped; } /** * Returns the percentage of the current file that has been processed, 0 if the current file's size * is not available (in this case getNbCurrentFileBytesProcessed() returns -1). * * @return the percentage of the current file that has been processed */ public float getFilePercentDone() { long currentFileSize = getCurrentFileSize(); return currentFileSize <= 0 ? 0 : getCurrentFileByteCounter().getByteCount()/(float)currentFileSize; } /** * Returns the number of bytes that have been skipped in the current file. Bytes are skipped when file transfers * are resumed. * * @return the number of bytes that have been skipped in the current file */ private ByteCounter getCurrentFileSkippedByteCounter() { return currentFileSkippedByteCounter; } /** * Returns the size of the file currently being processed, -1 if this information is not available. * * @return the size of the file currently being processed, -1 if this information is not available. */ public long getCurrentFileSize() { return getCurrentFile() == null ? -1 : getCurrentFile().getSize(); } /** * Sets a transfer throughput limit in bytes per seconds, replacing any previous limit. * This limit corresponds to the number of bytes that can be read from a registered InputStream. * *

    Specifying 0 or -1 disables any throughput limit, the transfer will be carried out at full speed. * *

    If this job is paused, the new limit will be effective after the job has been resumed. * If not, it will be effective immediately. * * @param bytesPerSecond new throughput limit in bytes per second, 0 or -1 to disable the limit */ public void setThroughputLimit(long bytesPerSecond) { // Note: ThroughputInputStream interprets 0 as a complete pause (blocks reads) which is different // from what a user would expect when specifying 0 as a limit this.throughputLimit = bytesPerSecond <= 0 ? -1 : bytesPerSecond; synchronized(this) { if (getState() != State.PAUSED && tlin != null) { tlin.setThroughputLimit(throughputLimit); } } } /** * Overrides {@link FileJob#jobStopped()} to stop any file processing by closing the source InputStream. */ @Override protected void jobStopped() { super.jobStopped(); synchronized(this) { if (tlin != null) { LOGGER.debug("closing current InputStream "+ tlin); closeCurrentInputStream(); } } } /** * Overrides {@link FileJob#jobPaused()} to pause any file processing * by having the source InputStream's read methods lock. */ @Override protected void jobPaused() { super.jobPaused(); synchronized(this) { if (tlin != null) { tlin.setThroughputLimit(0); } } } /** * Overrides {@link FileJob#jobResumed()} to resume any file processing by releasing * the lock on the source InputStream's read methods. */ @Override protected void jobResumed() { super.jobResumed(); synchronized(this) { // Restore previous throughput limit (if any, -1 by default) if (tlin != null) { tlin.setThroughputLimit(throughputLimit); } } } /** * Advances file index and resets current file's byte counters. This method should be called by subclasses * whenever the job starts processing a new file. */ @Override protected void nextFile(AbstractFile file) { totalByteCounter.add(currentFileByteCounter, true); totalSkippedByteCounter.add(currentFileSkippedByteCounter, true); // Reset some fields that need it currentFileSkipped = false; super.nextFile(file); } /** * Method overridden to return a more accurate percentage of job processed so far by taking into account the current * file's percentage of completion. */ @Override public float getTotalPercentDone() { float nbFilesProcessed = getCurrentFileIndex(); int nbFiles = getNbFiles(); // If file is in base folder and is not a directory... if (getCurrentFile() != null && nbFilesProcessed != nbFiles && files.contains(getCurrentFile()) && !getCurrentFile().isDirectory()) { // Add current file's progress long currentFileSize = getCurrentFile().getSize(); if (currentFileSize > 0) { nbFilesProcessed += getCurrentFileByteCounter().getByteCount()/(float)currentFileSize; } } return nbFilesProcessed/(float)nbFiles; } /** * This method is overridden to return a custom string "Checking integrity of CURRENT_FILE" when the current file * is being checked for integrity. */ @Override public String getStatusString() { if (isCheckingIntegrity()) { return Translator.get("progress_dialog.verifying_file", getCurrentFilename()); } return super.getStatusString(); } protected boolean tryCopySymlinkFile(AbstractFile sourceFile, AbstractFile destFile) { Path sourcePath = ((File) sourceFile.getUnderlyingFileObject()).toPath(); Path destPath = ((File) destFile.getUnderlyingFileObject()).toPath(); try { Files.createSymbolicLink(destPath, Files.readSymbolicLink(sourcePath)); } catch (IOException e) { LOGGER.debug("failed to create symbolic link "+destFile, e); return false; } // Preserve source file's date tryCopyFileDate(sourceFile, destFile); // Preserve source file's permissions: preserve only the permissions bits that are supported by the source file // and use default permissions for the rest of them. tryCopyFilePermissions(sourceFile, destFile); // Under Mac OS X only, preserving the file type and creator DesktopManager.postCopy(sourceFile, destFile); // Under Mac OS X only, preserving the file type and creator tryCopyFileTypeAndCreator(sourceFile, destFile); return true; } private void tryCopyFileDate(AbstractFile sourceFile, AbstractFile destFile) { if (destFile.isFileOperationSupported(FileOperation.CHANGE_DATE)) { try { destFile.setLastModifiedDate(sourceFile.getLastModifiedDate()); } catch (IOException e) { LOGGER.debug("failed to change the date of "+destFile, e); } } } private void tryCopyFilePermissions(AbstractFile sourceFile, AbstractFile destFile) { if (destFile.isFileOperationSupported(FileOperation.CHANGE_PERMISSION)) { try { destFile.importPermissions(sourceFile, FilePermissions.DEFAULT_FILE_PERMISSIONS); // use #importPermissions(AbstractFile, int) to avoid isDirectory test } catch(IOException e) { LOGGER.debug("failed to import "+sourceFile+" permissions into "+destFile, e); } } } private void tryCopyFileTypeAndCreator(AbstractFile sourceFile, AbstractFile destFile) { if (OsFamily.MAC_OS_X.isCurrent() && sourceFile.hasAncestor(LocalFile.class) && destFile.hasAncestor(LocalFile.class)) { String sourcePath = sourceFile.getAbsolutePath(); try { FileManager.setFileTypeAndCreator(destFile.getAbsolutePath(), FileManager.getFileType(sourcePath), FileManager.getFileCreator(sourcePath)); } catch(IOException e) { // Swallow the exception and do not interrupt the transfer LOGGER.debug("Error while setting Mac OS X file type and creator on destination", e); } } } // /** // * Method overridden to return a more accurate percentage of job processed so far by taking // * into account the current file's processed percentage. // */ // public float getTotalPercentDone() { // float nbFilesProcessed = getNbFilesProcessed(); // // // If file is in base folder and is not a directory // if(currentFile!=null && files.indexOf(currentFile)!=-1 && !currentFile.isDirectory()) { // // Take into account current file's progress // long currentFileSize = currentFile.getSize(); // if(currentFileSize>0) // nbFilesProcessed += getCurrentFileByteCounter().getByteCount()/(float)currentFileSize; // } // ////AppLogger.finest("nbFilesProcessed="+(int)nbFilesProcessed+" nbFilesDiscovered="+getNbFilesDiscovered()+" %="+((int)100*nbFilesProcessed/getNbFilesDiscovered())); // // return nbFilesProcessed/getNbFilesDiscovered(); // } } ================================================ FILE: src/main/java/com/mucommander/job/UnpackJob.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job; import com.mucommander.commons.file.*; import com.mucommander.commons.file.impl.ProxyFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.impl.UnmarkAllAction; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * This job unpacks a set of archive files to a base destination folder. Archive entries are extracted in their natural * order using {@link com.mucommander.commons.file.AbstractArchiveFile#getEntryIterator()}, to traverse the archive only once * and achieve optimal performance. * * @author Maxence Bernard */ public class UnpackJob extends AbstractCopyJob { /** Archive entries to be unpacked */ private List selectedEntries; /** Depth of the folder in which the top entries are located. 0 is the highest depth (archive's root folder) */ private final int baseArchiveDepth; private long totalFilesSize; private int totalFilesCount; private int processedFilesCount; private long processedFilesSize; private boolean preparingFinished; /** * Creates a new UnpackJob without starting it. *

    * The base destination folder will be created if it doesn't exist. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param files files which are going to be unpacked * @param destFolder destination folder where the files will be copied * @param fileExistsAction default action to be performed when a file already exists in the destination, see {@link com.mucommander.ui.dialog.file.FileCollisionDialog} for allowed values */ public UnpackJob(ProgressDialog progressDialog, MainFrame mainFrame, FileSet files, AbstractFile destFolder, int fileExistsAction) { super(progressDialog, mainFrame, files, destFolder, null, fileExistsAction); this.errorDialogTitle = Translator.get("unpack_dialog.error_title"); this.baseArchiveDepth = 0; } /** * Creates a new UnpackJob without starting it. * * @param progressDialog dialog which shows this job's progress * @param mainFrame mainFrame this job has been triggered by * @param archiveFile the archive file which is going to be unpacked * @param destFolder destination folder where the files will be copied * @param newName the new filename in the destination folder, if null the original filename will be used * @param fileExistsAction default action to be performed when a file already exists in the destination, see {@link com.mucommander.ui.dialog.file.FileCollisionDialog} for allowed values * @param selectedEntries entries to be unpacked * @param baseArchiveDepth depth of the folder in which the top entries are located. 0 is the highest depth (archive's root folder) */ public UnpackJob(ProgressDialog progressDialog, MainFrame mainFrame, AbstractArchiveFile archiveFile, int baseArchiveDepth, AbstractFile destFolder, String newName, int fileExistsAction, List selectedEntries) { super(progressDialog, mainFrame, new FileSet(archiveFile.getParent(), archiveFile), destFolder, newName, fileExistsAction); this.errorDialogTitle = Translator.get("unpack_dialog.error_title"); this.baseArchiveDepth = baseArchiveDepth; this.selectedEntries = selectedEntries; } //////////////////////////////////// // TransferFileJob implementation // //////////////////////////////////// @Override protected void jobStarted() { super.jobStarted(); // create the base destination folder if it doesn't exist yet if (!baseDestFolder.exists()) { // Loop for retry do { try { baseDestFolder.mkdir(); } catch(IOException e) { // Unable to create folder int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_create_folder", baseDestFolder.getName())); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel or close dialog interrupts the job interrupt(); // Skip continues } break; } while(true); } } /** * Unpacks the given archive file. If the file is a directory, its children will be processed recursively. * If the file is not an archive file nor a directory, it is not processed and false is returned. * * @param file the file to unpack * @param recurseParams unused * @return true if the file has been processed successfully */ @Override protected boolean processFile(AbstractFile file, Object recurseParams) { // Stop if interrupted if (getState() == State.INTERRUPTED) { return false; } // Destination folder AbstractFile destFolder = baseDestFolder; // If the file is a directory, process its children recursively if (file.isDirectory()) { do { // Loop for retries try { // List files inside archive file (can throw an IOException) AbstractFile[] archiveFiles = getCurrentFile().ls(); // Recurse on zip's contents for(int j=0; j " + file.getCanonicalPath()); return true; } // Check if the file does not already exist in the destination destFile = checkForCollision(entryFile, destFolder, destFile, false); if (destFile == null) { // A collision occurred and either the file was skipped, or the user cancelled the job continue; } // It is noteworthy that the iterator returns entries in no particular order (consider it random). // For that reason, we cannot assume that the parent directory of an entry will be processed // before the entry itself. // If the entry is a directory ... if (entryFile.isDirectory()) { // create the directory in the destination, if it doesn't already exist if (!(destFile.exists() && destFile.isDirectory())) { // Loop for retry do { try { // Use mkdirs() instead of mkdir() to create any parent folder that doesn't exist yet destFile.mkdirs(); } catch(IOException e) { // Unable to create folder int ret = showErrorDialog(errorDialogTitle, Translator.get("cannot_create_folder", entryFile.getName())); // Retry loops if (ret == RETRY_ACTION) { continue; } // Cancel or close dialog return false return false; // Skip continues } break; } while(true); } } // The entry is a regular file, copy it else { // create the file's parent directory(s) if it doesn't already exist AbstractFile destParentFile = destFile.getParent(); if (!destParentFile.exists()) { // Use mkdirs() instead of mkdir() to create any parent folder that doesn't exist yet destParentFile.mkdirs(); } // The entry is wrapped in a ProxyFile to override #getInputStream() and delegate it to // ArchiveFile#getEntryInputStream in order to take advantage of the ArchiveEntryIterator, which for // some archive file implementations (such as TAR) can speed things by an order of magnitude. if (!tryCopyFile(new ProxiedEntryFile(entryFile, entry, archiveFile, iterator), destFile, append, errorDialogTitle)) { // !!! we don't need to break the process in this case // return false; } } } return true; } catch (IOException e) { showErrorDialog(errorDialogTitle, Translator.get("cannot_read_file", archiveFile.getName())); } finally { // The ArchiveEntryIterator must be closed when finished if (iterator != null) { try { iterator.close(); } catch(IOException e) { // Not much we can do about it } } } return false; } // This job modifies the base destination folder and its subfolders @Override protected boolean hasFolderChanged(AbstractFile folder) { return baseDestFolder.isParentOf(folder); } @Override protected void jobCompleted() { super.jobCompleted(); // If the destination files are located inside an archive, optimize the archive file AbstractArchiveFile archiveFile = baseDestFolder.getParentArchive(); if(archiveFile!=null && archiveFile.isArchive() && archiveFile.isWritable()) optimizeArchive((AbstractRWArchiveFile)archiveFile); // Unselect all files in the active table upon successful completion if (selectedEntries != null) { ActionManager.performAction(UnmarkAllAction.Descriptor.ACTION_ID, getMainFrame()); } } @Override public String getStatusString() { if (isCheckingIntegrity() || !preparingFinished) { return super.getStatusString(); } if (isOptimizingArchive) { return Translator.get("optimizing_archive", archiveToOptimize.getName()); } return Translator.get("unpack_dialog.unpacking_file", getCurrentFilename()); } private void calculateTotalSize(AbstractArchiveFile archiveFile) { totalFilesSize = 0; totalFilesCount = 0; // get all directories List selectedDirectories = new ArrayList<>(); List fileEntries = new ArrayList<>(); if (selectedEntries != null) { for (ArchiveEntry entry : selectedEntries) { if (entry.isDirectory()) { selectedDirectories.add(entry.getPath()); } else { fileEntries.add(entry); } } } try { ArchiveEntryIterator iterator = archiveFile.getEntryIterator(); ArchiveEntry entry; while ((entry = iterator.nextEntry()) != null && getState() != State.INTERRUPTED) { // check in directories boolean addThisEntry = false; if (selectedEntries != null) { if (!selectedDirectories.isEmpty()) { String path = entry.getPath(); for (String dir : selectedDirectories) { if (path.startsWith(dir)) { addThisEntry = true; break; } } } // directories if (!addThisEntry && !selectedEntries.isEmpty()) { for (ArchiveEntry selEntry : selectedEntries) { if (entry == selEntry) { addThisEntry = true; break; } } } // entries } else { addThisEntry = true; } if (addThisEntry) { totalFilesSize += entry.getSize(); totalFilesCount++; } } // while } catch (IOException e) { e.printStackTrace(); } preparingFinished = true; } @Override public float getTotalPercentDone() { if (totalFilesSize == 0) { float result = super.getTotalPercentDone(); return result > 5 ? 5 : result; } float progressBySize = 1.0f*processedFilesSize / totalFilesSize; float progressByCount = 1.0f*(processedFilesCount-1) / totalFilesCount; float result = (progressBySize * 8 + progressByCount * 2) / 10; if (result < 0) { result = 0; } else if (result > 1) { result = 1; } return result; } private static class ProxiedEntryFile extends ProxyFile { private final ArchiveEntry entry; private final AbstractArchiveFile archiveFile; private final ArchiveEntryIterator iterator; ProxiedEntryFile(AbstractFile entryFile, ArchiveEntry entry, AbstractArchiveFile archiveFile, ArchiveEntryIterator iterator) { super(entryFile); this.entry = entry; this.archiveFile = archiveFile; this.iterator = iterator; } @Override public InputStream getInputStream() throws IOException { return archiveFile.getEntryInputStream(entry, iterator); } } } ================================================ FILE: src/main/java/com/mucommander/job/progress/JobProgress.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job.progress; import com.mucommander.job.FileJob; import com.mucommander.job.TransferFileJob; import com.mucommander.utils.text.DurationFormat; import com.mucommander.utils.text.Translator; /** * Contains information about job progress. * */ public class JobProgress { private final FileJob job; private TransferFileJob transferFileJob; private long effectiveJobTime; private long lastTime; private int totalPercentInt; private String totalProgressText; private int filePercentInt; private String fileProgressText; private long currentBps; private long bytesTotal; private long totalBps; private long lastBytesTotal; private String jobStatusString; private long jobPauseStartDate; public JobProgress(FileJob job) { this.job = job; if (job instanceof TransferFileJob) { this.transferFileJob = (TransferFileJob) job; } lastBytesTotal = 0; lastTime = System.currentTimeMillis(); } /** * Calculates the job progress status. This method calculates variables used * to show job progress information. It can update information only on a * processed file (when labelOnly is true). If * labelOnly is false it will try to update full information on * a job progress (e.g. percent completed, bytes per second, etc.). * * @param fullUpdate * true update all information about processed file.
    * false update only label of a processed file.
    * Note that if a job has just finished this flag is ignored * and all variables are recalculated. * @return true if full job progress has been updated, * false if only label has been updated. */ boolean calcJobProgress(boolean fullUpdate) { FileJob.State jobState = job.getState(); jobPauseStartDate = job.getPauseStartDate(); if (jobState == FileJob.State.FINISHED || jobState == FileJob.State.INTERRUPTED) { jobStatusString = Translator.get("progress_dialog.job_finished"); // Job just finished, let's loop one more time to ensure that // components (progress bar in particular) // reflect job completion fullUpdate = true; } else { jobStatusString = job.getStatusString(); } if (!fullUpdate) { return false; } // Do not refresh progress information is job is paused, simply sleep if (jobState == FileJob.State.PAUSED) { return false; } // Now is updated with current time, or job end date if job has finished // already. long now = job.getEndDate(); if (now == 0) { // job hasn't finished yet now = System.currentTimeMillis(); } long currentFileRemainingTime = 0; long totalRemainingTime; effectiveJobTime = job.getEffectiveJobTime(); if (effectiveJobTime == 0) { effectiveJobTime = 1; // To avoid potential zero divisions } if (transferFileJob != null) { bytesTotal = transferFileJob.getTotalByteCounter().getByteCount() - transferFileJob.getTotalSkippedByteCounter().getByteCount(); totalBps = (long) (bytesTotal * 1000d / effectiveJobTime); if (now - lastTime > 0) { // To avoid divisions by zero currentBps = (long) ((bytesTotal - lastBytesTotal) * 1000d / (now - lastTime)); } else { currentBps = 0; } // Update current file progress bar float filePercentFloat = transferFileJob.getFilePercentDone(); filePercentInt = (int) (100 * filePercentFloat); fileProgressText = filePercentInt + "%"; // Append estimated remaining time (ETA) if current file transfer is // not already finished (100%) if (filePercentFloat < 1) { fileProgressText += " - "; long currentFileSize = transferFileJob.getCurrentFileSize(); // If current file size is not available, ETA cannot be // calculated if (currentFileSize == -1) { fileProgressText += "?"; } // Avoid potential divisions by zero else if (totalBps == 0) { currentFileRemainingTime = -1; fileProgressText += DurationFormat.getInfiniteSymbol(); } else { currentFileRemainingTime = (long) ((1000 * (currentFileSize - transferFileJob.getCurrentFileByteCounter().getByteCount())) / (float) totalBps); fileProgressText += DurationFormat.format(currentFileRemainingTime); } } lastBytesTotal = bytesTotal; lastTime = now; } // Update total progress bar // Total job percent is based on the *number* of files remaining, not // their actual size. // So this is very approximate. float totalPercentFloat = job.getTotalPercentDone(); totalPercentInt = (int) (100 * totalPercentFloat); totalProgressText = totalPercentInt + "%"; // Add a rough estimate of the total remaining time (ETA): // total remaining time is based on the total job percent completed // which itself is based on the *number* // of files remaining, not their actual size. So this is very // approximate. // Do not add ETA if job is already finished (100%) if (totalPercentFloat < 1) { totalProgressText += " - "; // Avoid potential divisions by zero if (totalPercentFloat == 0) { totalProgressText += "?"; } else { // Make sure that total ETA is never smaller than current file // ETA totalRemainingTime = (long) ((1 - totalPercentFloat) * (effectiveJobTime / totalPercentFloat)); totalRemainingTime = Math.max(totalRemainingTime, currentFileRemainingTime); totalProgressText += DurationFormat.format(totalRemainingTime); } } return true; } public String getJobStatusString() { return jobStatusString; } public boolean isTransferFileJob() { return transferFileJob != null; } public int getFilePercentInt() { return filePercentInt; } public String getFileProgressText() { return fileProgressText; } public long getBytesTotal() { return bytesTotal; } public long getTotalBps() { return totalBps; } public long getLastTime() { return lastTime; } public long getCurrentBps() { return currentBps; } public int getTotalPercentInt() { return totalPercentInt; } public String getTotalProgressText() { return totalProgressText; } public long getEffectiveJobTime() { return effectiveJobTime; } public long getJobPauseStartDate() { return jobPauseStartDate; } } ================================================ FILE: src/main/java/com/mucommander/job/progress/JobProgressListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job.progress; import com.mucommander.job.FileJob; import java.util.EventListener; /** * Interface to be implemented by classes that wish to be notified of progress changes on a particular * {@link FileJob}. Those classes need to be registered to receive those events, this can be done by calling * {@link JobProgressMonitor#addJobProgressListener(JobProgressListener)}. * * @author Mariusz Jakubowski */ public interface JobProgressListener extends EventListener { /** * Called when a new job has been initiated. * * @param source a job added * @param idx index of a job in a job queue */ void jobAdded(FileJob source, int idx); /** * Called when a new job has finished and has been removed from the queue. * * @param source a job removed * @param idx index of a job in a job queue */ void jobRemoved(FileJob source, int idx); /** * Called when the progress of the specified FileJob has been updated. * * @param source the FileJob which progress has been updated * @param idx index of a job in a job queue * @param fullUpdate if false indicates that only file label has been updated * @see JobProgress#calcJobProgress */ void jobProgress(FileJob source, int idx, boolean fullUpdate); } ================================================ FILE: src/main/java/com/mucommander/job/progress/JobProgressMonitor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job.progress; import com.mucommander.job.FileJob; import com.mucommander.job.FileJobListener; import javax.swing.*; import javax.swing.event.EventListenerList; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; /** * A class that monitors jobs progress. * @author Mariusz Jakubowski * */ public class JobProgressMonitor implements FileJobListener { /** Controls how often should current file label be refreshed (in ms) */ private final static int CURRENT_FILE_LABEL_REFRESH_RATE = 100; /** Controls how often should progress information be refreshed */ private final static int MAIN_REFRESH_RATE = 10; /** Time after which remove finished job from a monitor */ private final static int FINISHED_JOB_REMOVE_TIME = 1500; /** Timer used to monitor jobs progress */ private Timer progressTimer; /** List of listeners */ private EventListenerList listenerList = new EventListenerList(); /** A list of monitored jobs. */ private List jobs = new ArrayList<>(); /** An instance of this class */ private static final JobProgressMonitor instance = new JobProgressMonitor(); /** * Creates a new JobProgressMonitor instance. */ private JobProgressMonitor() { JobProgressTimer timerListener = new JobProgressTimer(); progressTimer = new Timer(CURRENT_FILE_LABEL_REFRESH_RATE, timerListener); } /** * Returns the instance of JobProgressMonitor. * @return the instance of JobProgressMonitor. */ public static JobProgressMonitor getInstance() { return instance; } /** * Adds a listener to the list that's notified each time a job * progress is updated. * * @param l the JobProgressListener */ public void addJobProgressListener(JobProgressListener l) { listenerList.add(JobProgressListener.class, l); } /** * Removes a listener from the list that's notified each time job * progress is updated. * * @param l the JobProgressListener */ public void removeJobProgressListener(JobProgressListener l) { listenerList.remove(JobProgressListener.class, l); } /** * Forwards the progress notification event to all * JobProgressListeners that registered * themselves as listeners. * @param source a job for which the progress has been updated * @param fullUpdate if false only file label has been updated * * @see #addJobProgressListener * @see JobProgressListener#jobProgress */ private void fireJobProgress(FileJob source, boolean fullUpdate) { int idx = jobs.indexOf(source); Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { ((JobProgressListener)listeners[i+1]).jobProgress(source, idx, fullUpdate); } } /** * Forwards the job added notification event to all * JobProgressListeners that registered * themselves as listeners. * @param source an added job * @param idx index of a job in a list * * @see #addJobProgressListener * @see JobProgressListener#jobAdded(FileJob, int) */ private void fireJobAdded(FileJob source, int idx) { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { ((JobProgressListener)listeners[i+1]).jobAdded(source, idx); } } /** * Forwards the job removed notification event to all * JobProgressListeners that registered * themselves as listeners. * @param source a removed job * @param idx index of a job in a list * * @see #addJobProgressListener * @see JobProgressListener#jobRemoved(FileJob, int) */ private void fireJobRemoved(FileJob source, int idx) { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length-2; i>=0; i-=2) { ((JobProgressListener)listeners[i+1]).jobRemoved(source, idx); } } /** * Adds a new job to the list of monitored jobs. * This method is executed in Swing Thread (EDT). * After adding a new job a {@link JobProgressListener#jobAdded(FileJob, int)} * event is fired. * @param job a job to be added */ public void addJob(final FileJob job) { // ensure that this method is called in EDT if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(() -> addJob(job)); } jobs.add(job); int idx = jobs.size() - 1; fireJobAdded(job, idx); if (!progressTimer.isRunning()) { progressTimer.start(); } job.addFileJobListener(this); } /** * Removes a job from a list of monitored jobs. * This method is executed in Swing Thread (EDT). * After adding a new job a {@link JobProgressListener#jobRemoved(FileJob, int)} * event is fired. * @param job a job to be removed */ public void removeJob(final FileJob job) { // ensure that this method is called in EDT if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(() -> removeJob(job)); } int idx = jobs.indexOf(job); if (idx != -1) { jobs.remove(idx); } if (jobs.isEmpty()) { progressTimer.stop(); } fireJobRemoved(job, idx); job.removeFileJobListener(this); } /** * Returns number of monitored jobs. * @return number of monitored jobs. */ public int getJobCount() { return jobs.size(); } /** * Returns a progress of a job with specified index. * @param rowIndex an index of a job * @return a progress information or null if job doesn't exists */ public JobProgress getJobProgres(int rowIndex) { if (rowIndex < jobs.size()) { FileJob job = jobs.get(rowIndex); return job.getJobProgress(); } return null; } /** * A {@link FileJobListener} implementation. * Removes a finished job after a small delay. */ public void jobStateChanged(final FileJob source, FileJob.State oldState, FileJob.State newState) { if (newState==FileJob.State.FINISHED || newState==FileJob.State.INTERRUPTED) { Timer timer = new Timer(FINISHED_JOB_REMOVE_TIME, (e) -> removeJob(source)); timer.setRepeats(false); timer.start(); } } /** * * This class implements a listener for a job progress timer. * */ private class JobProgressTimer implements ActionListener { /** a loop index indicating if this refresh is partial (label only) or full */ private int loopCount = 0; public void actionPerformed(ActionEvent e) { loopCount++; boolean fullUpdate; if (loopCount >= MAIN_REFRESH_RATE) { fullUpdate = true; loopCount = 0; } else { fullUpdate = false; } // for each job calculate new progress and notify listeners for(FileJob job : jobs) { boolean updateFullUI; JobProgress jobProgress = job.getJobProgress(); updateFullUI = jobProgress.calcJobProgress(fullUpdate); fireJobProgress(job, updateFullUI); } } } } ================================================ FILE: src/main/java/com/mucommander/job/ui/DialogResult.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job.ui; /** * An interface marking a dialog which can return a value. * @author Mariusz Jakubowski * */ public interface DialogResult { Object getUserInput(); } ================================================ FILE: src/main/java/com/mucommander/job/ui/UserInputHelper.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job.ui; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.job.FileJob; /** * This class is used to show a dialog for user and get a response from * this dialog. It is used by {@link FileJob} class. * * @author Mariusz Jakubowski */ public class UserInputHelper { private static final Logger LOGGER = LoggerFactory.getLogger(UserInputHelper.class); private Object userInput; private final DialogResult dialog; public UserInputHelper(FileJob job, DialogResult dialog) { this.dialog = dialog; } public Object getUserInput() { try { SwingUtilities.invokeAndWait(() -> userInput = dialog.getUserInput()); } catch (Exception e) { LOGGER.debug("Caught exception", e); } return userInput; } } ================================================ FILE: src/main/java/com/mucommander/job/utils/ScanDirectoryThread.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.job.utils; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import java.io.IOException; /** * Thread to calculating the total size of files */ public class ScanDirectoryThread extends Thread { private final FileSet files; private long totalBytes; private boolean completed; private long executionTime; private long filesCount; private boolean interrupted; private final boolean calcSize; public ScanDirectoryThread(FileSet files) { this.files = files; this.calcSize = true; setName("ScanDirectoryThread " + files.getBaseFolder()); } public ScanDirectoryThread(FileSet files, boolean calcSize) { this.files = files; this.calcSize = calcSize; setName("ScanDirectoryThread " + files.getBaseFolder()); } @Override public void run() { executionTime = System.currentTimeMillis(); for (AbstractFile file : files) { if (interrupted) { break; } try { processFile(file); } catch (Throwable ignore) {} } completed = true; executionTime = System.currentTimeMillis() - executionTime; //System.out.println("finished " + totalBytes + " " + filesCount + " time " + executionTime); } private void processFile(AbstractFile file) { if (interrupted) { return; } filesCount++; if (file.isSymlink()) { return; // ignore symlinks } if (file.isDirectory()) { try { AbstractFile[] subfiles = file.ls(); for (AbstractFile subfile : subfiles ) { if (interrupted) { return; } processFile(subfile); } } catch (IOException e) { e.printStackTrace(); } } else { if (calcSize) { totalBytes += file.getSize(); } } } public long getTotalBytes() { return totalBytes; } public boolean isCompleted() { return completed; } public long getFilesCount() { return filesCount; } public void interrupt() { interrupted = true; } } ================================================ FILE: src/main/java/com/mucommander/launcher/LauncherCmdHelper.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.launcher; import com.mucommander.PlatformManager; import com.mucommander.RuntimeConstants; import com.mucommander.TrolCommander; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.conf.TcConfigurations; import com.mucommander.extension.ExtensionManager; import com.mucommander.shell.ShellHistoryManager; import com.mucommander.ui.main.commandbar.CommandBarIO; import com.mucommander.ui.main.toolbar.ToolBarIO; /** * Helper class for launcher command line * * @author Maxence Bernard, Nicolas Rinaudo, Oleg Trifonov */ public class LauncherCmdHelper { private static Logger logger; /** * Whether to display verbose error messages. */ private boolean verbose; /** * Whether to ignore warnings when booting. */ private boolean fatalWarnings; /** * Index in the command line arguments. */ private int index; private final String[] args; public LauncherCmdHelper(String[] args, boolean verbose, boolean fatalWarnings) { this.args = args; this.verbose = verbose; this.fatalWarnings = fatalWarnings; } // - Commandline handling methods ------------------------------------------- // -------------------------------------------------------------------------- /** * Prints muCommander's command line usage and exits. */ private static void printUsage() { System.out.println("Usage: trolcommander [options] [folders]"); System.out.println("Options:"); // Allows users to tweak how file associations are loaded / saved. System.out.println(" -a FILE, --assoc FILE Load associations from FILE."); // Allows users to tweak how bookmarks are loaded / saved. System.out.println(" -b FILE, --bookmarks FILE Load bookmarks from FILE."); // Allows users to tweak how configuration is loaded / saved. System.out.println(" -c FILE, --configuration FILE Load configuration from FILE"); // Allows users to tweak how command bar configuration is loaded / saved. System.out.println(" -C FILE, --commandbar FILE Load command bar from FILE."); // Allows users to change the extension's folder. System.out.println(" -e FOLDER, --extensions FOLDER Load extensions from FOLDER."); // Allows users to tweak how custom commands are loaded / saved. System.out.println(" -f FILE, --commands FILE Load custom commands from FILE."); // Ignore warnings. System.out.println(" -index, --ignore-warnings Do not fail on warnings (default)."); // Allows users to tweak how keymaps are loaded. System.out.println(" -k FILE, --keymap FILE Load keymap from FILE"); // Allows users to change the preference's folder. System.out.println(" -p FOLDER, --preferences FOLDER Store configuration files in FOLDER"); // muCommander will not print verbose error messages. System.out.println(" -S, --silent Do not print verbose error messages"); // Allows users to tweak how shell history is loaded / saved. System.out.println(" -s FILE, --shell-history FILE Load shell history from FILE"); // Allows users to tweak how toolbar configuration are loaded. System.out.println(" -t FILE, --toolbar FILE Load toolbar from FILE"); // Allows users to tweak how credentials are loaded. System.out.println(" -u FILE, --credentials FILE Load credentials from FILE"); // Text commands. System.out.println(" -h, --help Print the help text and exit"); System.out.println(" -v, --version Print the version and exit"); // muCommander will print verbose boot error messages. System.out.println(" -V, --verbose Print verbose error messages (default)"); // Pedantic mode. System.out.println(" -w, --fail-on-warnings Quits when a warning is encountered during"); System.out.println(" the boot process."); System.exit(0); } /** * Prints muCommander's version to stdout and exits. */ private static void printVersion() { System.out.println(RuntimeConstants.APP_STRING); System.out.print("Copyright (C) "); System.out.print(RuntimeConstants.COPYRIGHT); System.out.println(" Maxence Bernard"); System.out.println("This is free software, distributed under the terms of the GNU General Public License."); System.exit(0); } public void parseArgs() { label: for (index = 0; index < args.length; index++) { // Print version. switch (args[index]) { case "-v": case "--version": printVersion(); break; // Print help. case "-h": case "--help": printUsage(); break; // Associations handling. case "-a": case "--assoc": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { com.mucommander.command.CommandManager.setAssociationFile(args[++index]); } catch (Exception e) { printError("Could not set association files", e, fatalWarnings); } break; // Custom commands handling. case "-f": case "--commands": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { com.mucommander.command.CommandManager.setCommandFile(args[++index]); } catch (Exception e) { printError("Could not set commands file", e, fatalWarnings); } break; // Bookmarks handling. case "-b": case "--bookmarks": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { com.mucommander.bookmark.BookmarkManager.setBookmarksFile(args[++index]); } catch (Exception e) { printError("Could not set bookmarks file", e, fatalWarnings); } break; // Configuration handling. case "-c": case "--configuration": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { TcConfigurations.setPreferencesFile(args[++index]); } catch (Exception e) { printError("Could not set configuration file", e, fatalWarnings); } break; // Shell history. case "-s": case "--shell-history": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { ShellHistoryManager.setHistoryFile(args[++index]); } catch (Exception e) { printError("Could not set shell history file", e, fatalWarnings); } break; // Keymap file. case "-k": case "--keymap": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { com.mucommander.ui.action.ActionKeymapIO.setActionsFile(args[++index]); } catch (Exception e) { printError("Could not set keymap file", e, fatalWarnings); } break; // Toolbar file. case "-t": case "--toolbar": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { ToolBarIO.setDescriptionFile(args[++index]); } catch (Exception e) { printError("Could not set keymap file", e, fatalWarnings); } break; // Commandbar file. case "-C": case "--commandbar": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { CommandBarIO.setDescriptionFile(args[++index]); } catch (Exception e) { printError("Could not set commandbar description file", e, fatalWarnings); } break; // Credentials file. case "-U": case "--credentials": if (index >= args.length - 1) printError("Missing FILE parameter to " + args[index], null, true); try { com.mucommander.auth.CredentialsManager.setCredentialsFile(args[++index]); } catch (Exception e) { printError("Could not set credentials file", e, fatalWarnings); } break; // Preference folder. case "-p": case "--preferences": if (index >= args.length - 1) printError("Missing FOLDER parameter to " + args[index], null, true); try { PlatformManager.setPreferencesFolder(args[++index]); } catch (Exception e) { printError("Could not set preferences folder", e, fatalWarnings); } break; // Extensions folder. case "-e": case "--extensions": if (index >= args.length - 1) printError("Missing FOLDER parameter to " + args[index], null, true); try { ExtensionManager.setExtensionsFolder(args[++index]); } catch (Exception e) { printError("Could not set extensions folder", e, fatalWarnings); } break; // Ignore warnings. case "-index": case "--ignore-warnings": fatalWarnings = false; break; // Fail on warnings. case "-w": case "--fail-on-warnings": fatalWarnings = true; break; // Silent mode. case "-S": case "--silent": verbose = false; break; // Verbose mode. case "-V": case "--verbose": verbose = true; break; // Illegal argument. default: break label; } } } /** * Prints an error message. */ private static void printError(String msg, boolean quit) { if (quit) { getLogger().error(msg); System.err.println("See trolcommander --help for more information."); System.exit(1); } else{ getLogger().warn(msg); } } /** * Prints a configuration file specific error message. */ void printFileError(String msg, Throwable exception, boolean quit) { StringBuilder error; error = createErrorMessage(msg, exception, quit); if (!quit) { error.append(". Using default values."); } printError(error.toString(), quit); } public void printFileError(String msg, Throwable exception) { printFileError(msg, exception, fatalWarnings); } /** * Prints the specified error message to stderr. * @param msg error message to print to stderr. * @param quit whether to quit after printing the error message. * @param exception exception that triggered the error (for verbose output). */ public void printError(String msg, Exception exception, boolean quit) { printError(createErrorMessage(msg, exception, quit).toString(), quit); } /** * Creates an error message. */ private StringBuilder createErrorMessage(String msg, Throwable exception, boolean quit) { StringBuilder error = new StringBuilder(); if (quit) { error.append("Warning: "); } error.append(msg); if (verbose && exception != null) { error.append(": ").append(exception.getMessage()); } return error; } public String[] getFolders() { String[] folders = new String[args.length - index]; System.arraycopy(args, index, folders, 0, folders.length); return folders; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(TrolCommander.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/launcher/LauncherExecutor.kt ================================================ package com.mucommander.launcher import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit class LauncherExecutor( private val cores: Int ) : ThreadPoolExecutor( cores, cores, 0L, TimeUnit.MILLISECONDS, LinkedBlockingQueue() ) { private val runningTasks: MutableSet = HashSet() fun isFull(): Boolean { if (runningTasks.size < cores) { return false } runningTasks.removeIf { obj: LauncherTask? -> obj!!.isDone() } return runningTasks.size >= cores } fun execute(task: LauncherTask, force: Boolean): Boolean { if (force || (runningTasks.size < cores && task.isReadyForExecution())) { super.execute(task.task) runningTasks.add(task) return true } return false } fun executeFirst(tasks: MutableCollection): Boolean { val it = tasks.iterator() while (it.hasNext()) { val task = it.next() if (execute(task, false)) { it.remove() return true } } return false } } ================================================ FILE: src/main/java/com/mucommander/launcher/LauncherTask.kt ================================================ package com.mucommander.launcher import com.mucommander.profiler.Profiler import java.util.concurrent.Callable import java.util.concurrent.FutureTask class LauncherTask ( name: String, private val handler: Runnable, ) : Callable { private val name = "launcher.$name" private var depends: List? = null internal val task: FutureTask = FutureTask(this) fun depends(vararg tasks: LauncherTask): LauncherTask { depends = tasks.toList() return this } //@Throws(Exception::class) override fun call(): Void? { if (!depends.isNullOrEmpty()) { Profiler.start("$name.depends") for (t in depends) { t.task.get() } Profiler.stop("$name.depends") } Profiler.start(name) try { handler.run() } catch (e: Throwable) { printError("Launcher getTask error for $name: ", e) } Profiler.stop(name) onFinish() return null } fun isReadyForExecution(): Boolean { if (depends.isNullOrEmpty()) { return true } for (dt in depends) { if (!dt.isDone()) { return false } } return true } fun isDone(): Boolean = task.isDone() fun onFinish() { } override fun toString() = name fun printError(msg: String, e: Throwable) { println(msg) e.printStackTrace() } } ================================================ FILE: src/main/java/com/mucommander/launcher/ShutdownHook.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.launcher; import com.mucommander.auth.CredentialsManager; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.conf.TcConfigurations; import com.mucommander.shell.ShellHistoryManager; import com.mucommander.ui.action.ActionKeymapIO; import com.mucommander.ui.main.commandbar.CommandBarIO; import com.mucommander.ui.main.toolbar.ToolBarIO; import com.mucommander.ui.main.tree.TreeIOThreadManager; import com.mucommander.ui.notifier.AbstractNotifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The init method of this thread is called when the program shuts down, either because * the user chose to quit the program or because the program was interrupted by a logoff. * @author Maxence Bernard */ public class ShutdownHook extends Thread { private static Logger logger; /** Whether shutdown tasks have been performed already. */ private static boolean shutdownTasksPerformed; /** * Creates a new ShutdownHook. */ ShutdownHook() { super(ShutdownHook.class.getName()); } /** * Shuts down muCommander. */ public static void initiateShutdown() { getLogger().info("shutting down"); // // No need to call System.exit() under Java 1.4, application will naturally exit // // when no there is no more window showing and no non-daemon thread still running. // // However, natural application death will not trigger ShutdownHook so we need to explicitly // // perform shutdown tasks. // performShutdownTasks(); // System.exit() will trigger ShutdownHook and perform shutdown tasks System.exit(0); } /** * Called by the VM when the program shuts down, this method writes the configuration. */ @Override public void run() { performShutdownTasks(); } /** * Performs tasks before shut down, such as writing the configuration file. This method can only * be called once, any further call will be ignored (no-op). */ private synchronized static void performShutdownTasks() { // Return if shutdown tasks have already been performed if (shutdownTasksPerformed) { return; } TreeIOThreadManager.getInstance().interrupt(); // Save snapshot try { TcConfigurations.saveSnapshot(); } catch(Exception e) { getLogger().warn("Failed to save snapshot", e); } // Save preferences // Don't need to save preferences on shutdown because it saves in Preferences edit dialog on Ok pressed try { TcConfigurations.savePreferences(); } catch(Exception e) { getLogger().warn("Failed to save configuration", e); } // Save shell history try { ShellHistoryManager.writeHistory(); } catch(Exception e) { getLogger().warn("Failed to save shell history", e); } // Write credentials file to disk, only if changes were made try { CredentialsManager.writeCredentials(false); } catch(Exception e) { getLogger().warn("Failed to save credentials", e); } // Write bookmarks file to disk, only if changes were made try { BookmarkManager.writeBookmarks(false); } catch(Exception e) { getLogger().warn("Failed to save bookmarks", e); } // Saves the current theme. // try {ThemeManager.saveCurrentTheme();} // catch(Exception e) {LOGGER.warn("Failed to save user theme", e);} // Saves the file associations. // try {CommandManager.writeCommands();} // catch(Exception e) {LOGGER.warn("Failed to save commands", e);} // try {CommandManager.writeAssociations();} // catch(Exception e) {LOGGER.warn("Failed to save associations", e);} // Saves the action keymap. try { ActionKeymapIO.saveActionKeymap(); } catch(Exception e) { getLogger().warn("Failed to save action keymap", e); } // Saves the command bar. try { CommandBarIO.saveCommandBar(); } catch(Exception e) { getLogger().warn("Failed to save command bar", e); } // Saves the toolbar. try { ToolBarIO.saveToolBar(); } catch(Exception e) { getLogger().warn("Failed to save toolbar", e); } var notifier = AbstractNotifier.getNotifier(); if (notifier.isEnabled()) { notifier.setEnabled(false); } // Shutdown tasks should only be performed once shutdownTasksPerformed = true; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ShutdownHook.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/launcher/tasks.kt ================================================ package com.mucommander.launcher import com.formdev.flatlaf.FlatDarculaLaf import com.formdev.flatlaf.FlatDarkLaf import com.formdev.flatlaf.FlatIntelliJLaf import com.formdev.flatlaf.FlatLightLaf import com.formdev.flatlaf.themes.FlatMacDarkLaf import com.formdev.flatlaf.themes.FlatMacLightLaf import com.mucommander.RuntimeConstants import com.mucommander.TrolCommander import com.mucommander.auth.CredentialsManager import com.mucommander.bonjour.BonjourDirectory import com.mucommander.bookmark.BookmarkManager import com.mucommander.bookmark.file.BookmarkProtocolProvider import com.mucommander.command.CommandManager import com.mucommander.commons.file.FileFactory import com.mucommander.commons.file.icon.impl.SwingFileIconProvider import com.mucommander.commons.file.impl.ftp.FTPProtocolProvider import com.mucommander.commons.file.impl.smb.SMBProtocolProvider import com.mucommander.commons.runtime.OsFamily import com.mucommander.commons.runtime.OsVersion import com.mucommander.conf.TcConfigurations import com.mucommander.conf.TcPreference import com.mucommander.conf.TcPreferences import com.mucommander.desktop.DesktopManager import com.mucommander.extension.ExtensionManager import com.mucommander.ui.PreloadedJFrame import com.mucommander.profiler.Profiler import com.mucommander.shell.ShellHistoryManager import com.mucommander.ui.action.ActionKeymapIO import com.mucommander.ui.action.ActionManager import com.mucommander.ui.dialog.about.AboutDialog import com.mucommander.ui.dialog.pref.general.GeneralPreferencesDialog import com.mucommander.ui.dialog.startup.InitialSetupDialog import com.mucommander.ui.icon.FileIcons import com.mucommander.ui.main.SplashScreen import com.mucommander.ui.main.WindowManager import com.mucommander.ui.main.commandbar.CommandBarIO import com.mucommander.ui.main.frame.CommandLineMainFrameBuilder import com.mucommander.ui.main.frame.DefaultMainFramesBuilder import com.mucommander.ui.main.toolbar.ToolBarIO import com.mucommander.ui.notifier.AbstractNotifier import com.mucommander.ui.theme.ThemeManager import com.mucommander.ui.tools.ToolsEnvironment import com.mucommander.utils.TcLogging import com.mucommander.utils.text.CustomDateFormat import com.mucommander.utils.text.Translator import org.slf4j.LoggerFactory import java.awt.Desktop import java.awt.GraphicsEnvironment import java.awt.desktop.AboutEvent import java.awt.event.KeyEvent import java.lang.reflect.Constructor import java.util.* import javax.swing.KeyStroke import javax.swing.UIManager import kotlin.math.max import kotlin.system.exitProcess private var splashScreen: SplashScreen? = null fun prepareLauncherTasks(helper: LauncherCmdHelper): List { val prepareLoggerTask = LauncherTask("prepare_logger") { LoggerFactory.getLogger(TrolCommander::class.java) } val prepareGraphicsTask = LauncherTask("prepare_graphics") { GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames() } val loadPreferencesTask = LauncherTask("load_preferences") { TcConfigurations.getPreferences() } val installFlatLightLafTask = LauncherTask("install_flat_light_laf") { FlatLightLaf.installLafInfo() } val installFlatDarculaLafTask = LauncherTask("install_flat_dracula_laf") { FlatDarculaLaf.installLafInfo() } val installFlatDarkLafTask = LauncherTask("install_flat_dark_laf") { FlatDarkLaf.installLafInfo() } val installFlatIntelliJLafTask = LauncherTask("install_flat_intellij_laf") { FlatIntelliJLaf.installLafInfo() } val installFlatMacLightLafTask = LauncherTask("install_flat_maclight_laf") { FlatMacLightLaf.installLafInfo() } val installFlatMacDarkLafTask = LauncherTask("install_flat_macdark_laf") { FlatMacDarkLaf.installLafInfo() } val installVaquaLafTask = LauncherTask("install_aqua_lf") { if (OsFamily.getCurrent() == OsFamily.MAC_OS_X && OsVersion.MAC_OS_X_10_13.isCurrentLower()) { val aquaLaf = org.violetlib.aqua.AquaLookAndFeel() UIManager.installLookAndFeel(UIManager.LookAndFeelInfo(aquaLaf.getName(), aquaLaf.javaClass.getName())) } } val prepareKeystrokeClassTask = LauncherTask("prepare_keystroke") { KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK) } val loadConfigsTask = LauncherTask("config") { loadConfigs(helper) } val loadEnvTask = LauncherTask("load_env") { ToolsEnvironment.load() } val loadBookmarksTask = LauncherTask("load_bookmarks") { BookmarkManager.loadBookmarks() } val loadIconsTask = LauncherTask("load_file_icons") { // Initialize the SwingFileIconProvider from the main thread, see method Javadoc for an explanation on why we do this now SwingFileIconProvider.forceInit() } val registerArchiveProtocolsTask = LauncherTask("register_archive_protocols") { FileFactory.registerProtocolArchives() } val registerNetworkProtocolsTask = LauncherTask("register_network_protocols") { FileFactory.registerProtocolNetworks() } val registerOtherProtocolsTask = LauncherTask("register_other_protocols") { FileFactory.registerProtocolOthers() } val loadCredentialsTask = LauncherTask("load_credentials") { CredentialsManager.loadCredentials() } val loadCustomCommandsTask = LauncherTask("load_custom_commands") { loadCustomCommands(helper) } val loadShellHistoryTask = LauncherTask("load_shell_history") { ShellHistoryManager.loadHistory() } val configureFileSystemTask = LauncherTask("configure_files") { configureFileSystems() } val initMacOsSupportTask = LauncherTask("init_macos_support") { initMacOsSupport(helper) } val startBonjourTask = LauncherTask("start_bonjour") { BonjourDirectory.setActive(isBonjourEnabled()) } val preloadedFramesTask = LauncherTask("preloaded_frames") { PreloadedJFrame.init(); } val startTask = LauncherTask("start") { start(helper) }.depends(initMacOsSupportTask) val initCustomDateFormatTask = LauncherTask("init_custom_date_format") { CustomDateFormat.init() }.depends(loadConfigsTask) val loadDictionaryTask = LauncherTask("load_dictionary") { Translator.init() }.depends(loadConfigsTask) val showSplashTask = LauncherTask("show_splash") { if (isShowSplash()) { splashScreen = SplashScreen(RuntimeConstants.VERSION, "Loading preferences...") } }.depends(loadPreferencesTask, loadConfigsTask) val loadThemeTask = LauncherTask("load_theme") { ThemeManager.loadCurrentTheme() }.depends(showSplashTask, prepareGraphicsTask) val registerActionsTask = LauncherTask("register_actions") { ActionManager.registerActions() }.depends(prepareKeystrokeClassTask, loadDictionaryTask) val initDesktopTask = LauncherTask("init_desktop") { initDesktop() }.depends(loadConfigsTask) val initBarsTask = LauncherTask("init_bars") { initBarFiles() }.depends(registerActionsTask) val enableNotificationsTask = LauncherTask("enable_notifications") { enableNotifications() }.depends(registerActionsTask) val prepareWindowManagerTask = LauncherTask("prepared_window_manager") { WindowManager.setupAfterCreation() } val createMainWindowTask = LauncherTask("create_main_window") { createMainWindow(helper) }.depends(loadThemeTask, showSplashTask, initDesktopTask, registerActionsTask, loadCustomCommandsTask, prepareWindowManagerTask) val disposeSplashTask = LauncherTask("dispose_splash") { splashScreen?.dispose() splashScreen = null }.depends(showSplashTask, createMainWindowTask) val showSetupWindowTask = LauncherTask("show_setup_window") { val showSetup = TcConfigurations.getPreferences().getVariable(TcPreference.THEME_TYPE) == null if (showSetup) { InitialSetupDialog(WindowManager.getCurrentMainFrame().jFrame).showDialog() } }.depends(loadConfigsTask) return LinkedList( listOf( prepareLoggerTask, prepareGraphicsTask, loadPreferencesTask, installFlatLightLafTask, preloadedFramesTask, installFlatDarculaLafTask, installFlatDarkLafTask, installFlatIntelliJLafTask, installFlatMacLightLafTask, installFlatMacDarkLafTask, installVaquaLafTask, prepareKeystrokeClassTask, initMacOsSupportTask, registerActionsTask, loadConfigsTask, startTask, loadIconsTask, showSplashTask, configureFileSystemTask, loadThemeTask, loadDictionaryTask, loadCustomCommandsTask, loadBookmarksTask, loadCredentialsTask, loadShellHistoryTask, initCustomDateFormatTask, startBonjourTask, initBarsTask, prepareWindowManagerTask, createMainWindowTask, enableNotificationsTask, initDesktopTask, registerArchiveProtocolsTask, registerNetworkProtocolsTask, registerOtherProtocolsTask, disposeSplashTask, showSetupWindowTask, loadEnvTask, )) // tasks.add(taskDisposeSplash); // tasks.add(taskShowSetupWindow); // tasks.add(taskLoadEnvironment); } private fun initDesktop() { try { val install = !TcConfigurations.isPreferencesFileExists() DesktopManager.init(install) } catch (e: Exception) { System.err.println("Could not initialize desktop") e.printStackTrace() exitProcess(1) } } fun start(helper: LauncherCmdHelper) { // Checks whether a graphics environment is available and exit with an error otherwise. if (GraphicsEnvironment.isHeadless()) { System.err.println("Error: no graphical environment detected.") exitProcess(1) } try { TcLogging.configureLogging() } catch (e: Exception) { helper.printFileError("Configure logging error", e) } // Adds all extensions to the classpath. try { Profiler.start("init-extensions-manager") ExtensionManager.init() Profiler.stop("init-extensions-manager") ExtensionManager.addExtensionsToClasspath() } catch (e: Exception) { helper.printFileError("Failed to add extensions to the classpath", e) } // This the property is supposed to have the java.net package use the proxy defined in the system settings // to establish HTTP connections. This property is supported only under Java 1.5 and up. // Note that Mac OS X already uses the system HTTP proxy, with or without this property being set. System.setProperty("java.net.useSystemProxies", "true") //boolean showSetup = MuConfigurations.getPreferences().getVariable(MuPreference.THEME_TYPE) == null; // Traps VM shutdown Runtime.getRuntime().addShutdownHook(ShutdownHook()) } private fun createMainWindow(helper: LauncherCmdHelper) { println("Initializing window...") Profiler.start("launcher.create-window") WindowManager.createNewMainFrame(CommandLineMainFrameBuilder(helper.getFolders())) // If no initial path was specified, start a default main window. if (WindowManager.getCurrentMainFrame() == null) { val mainFrameBuilder = DefaultMainFramesBuilder() WindowManager.createNewMainFrame(mainFrameBuilder) } Profiler.stop("launcher.create-window") Profiler.stop("loading") Profiler.print() Profiler.hide("launcher.") } fun initMacOsSupport(helper: LauncherCmdHelper) { // If trolCommander is running under Mac OS X (how lucky!), add some glue for the main menu bar and other OS X specifics. if (OsFamily.MAC_OS_X.isCurrent) { // Use reflection to create an OSXIntegration instance so that ClassLoader // doesn't throw an NoClassDefFoundException under platforms other than Mac OS X try { val osxIntegrationClass = Class.forName("com.mucommander.ui.macosx.OSXIntegration") val constructor: Constructor<*> = osxIntegrationClass.getConstructor() constructor.newInstance() } catch (e: java.lang.Exception) { helper.printFileError("Exception thrown while initializing Mac OS X integration", e) } val desktop = Desktop.getDesktop() if (desktop.isSupported(Desktop.Action.APP_ABOUT)) { desktop.setAboutHandler { _: AboutEvent? -> AboutDialog((WindowManager.getCurrentMainFrame())).showDialog() } } if (desktop.isSupported(Desktop.Action.APP_PREFERENCES)) { desktop.setAboutHandler { _: AboutEvent? -> GeneralPreferencesDialog.getDialog().showDialog() } } } } private fun loadConfigs(helper: LauncherCmdHelper) { // Attempts to guess whether this is the first time trolCommander is booted or not. //boolean isFirstBoot; //try {isFirstBoot = !MuConfigurations.isPreferencesFileExists();} //catch(IOException e) {isFirstBoot = true;} // Load snapshot data before loading configuration as until version 0.9 the snapshot properties // were stored as preferences so when loading such preferences they could overload snapshot properties try { TcConfigurations.loadSnapshot() } catch (e: Exception) { helper.printFileError("Could not load snapshot", e) } // Configuration needs to be loaded before any sort of GUI creation is performed // under Mac OS X, if we're to use the metal look, we need to know about it right about now. try { TcConfigurations.loadPreferences() } catch (e: Exception) { helper.printFileError("Could not load configuration", e) } // The math.max(1.0f, ...) part is to workaround a bug which cause(d) this value to be set to 0.0 in the configuration file. FileIcons.setScaleFactor( max(1.0f, TcConfigurations.getPreferences().getVariable(TcPreference.TABLE_ICON_SCALE, TcPreferences.DEFAULT_TABLE_ICON_SCALE)) ) FileIcons.setSystemIconsPolicy( TcConfigurations.getPreferences().getVariable(TcPreference.USE_SYSTEM_FILE_ICONS, TcPreferences.DEFAULT_USE_SYSTEM_FILE_ICONS) ) } private fun loadCustomCommands(helper: LauncherCmdHelper) { try { CommandManager.loadCommands() } catch (e: java.lang.Exception) { helper.printFileError("Could not load custom commands", e) } // Migrates the custom editor and custom viewer if necessary. TrolCommander.migrateCommand("viewer.use_custom", "viewer.custom_command", CommandManager.VIEWER_ALIAS) TrolCommander.migrateCommand("editor.use_custom", "editor.custom_command", CommandManager.EDITOR_ALIAS) try { CommandManager.writeCommands() } catch (e: java.lang.Exception) { helper.printFileError("Caught exception", e) // There's really nothing we can do about this... } try { CommandManager.loadAssociations() } catch (e: java.lang.Exception) { helper.printFileError("Could not load custom associations", e) } ActionManager.registerCommandsActions() } private fun configureFileSystems() { // Configure the SMB subsystem (backed by jCIFS) to maintain compatibility with SMB servers that don't support // NTLM v2 authentication such as Samba 3.0.x, which still is widely used and comes pre-installed on // Mac OS X Leopard. // Since jCIFS 1.3.0, the default is to use NTLM v2 authentication and extended security. SMBProtocolProvider.setSmbLmCompatibility(isSmbLmCompatibilityEnabled()) SMBProtocolProvider.setExtendedSecurity(isSmbExtendedSecurityEnabled()) // Use the FTP configuration option that controls whether to force the display of hidden files, or leave it for // the servers to decide whether to show them. FTPProtocolProvider.setForceHiddenFilesListing(isListHiddenFiles()) // FileFactory.registerProtocolFile(); // Use CredentialsManager for file URL authentication FileFactory.setDefaultAuthenticator(CredentialsManager.getAuthenticator()) // Register the application-specific 'bookmark' protocol. FileFactory.registerProtocol(BookmarkProtocolProvider.BOOKMARK, BookmarkProtocolProvider()) } private fun initBarFiles() { println("Loading actions shortcuts...") try { ActionKeymapIO.loadActionKeymap() } catch (e: Exception) { printError("Could not load actions shortcuts", e) } println("Loading toolbar description...") try { ToolBarIO.loadDescriptionFile() } catch (e: Exception) { printError("Could not load toolbar description", e) } println("Loading command bar description...") try { CommandBarIO.loadCommandBar() } catch (e: Exception) { printError("Could not load commandbar description", e) } } private fun enableNotifications() { // Enable system notifications, only after MainFrame is created as SystemTrayNotifier needs to retrieve a MainFrame instance if (isNotificationsEnabled()) { println("Enabling system notifications...") if (AbstractNotifier.isAvailable()) { AbstractNotifier.getNotifier().setEnabled(true) } } } private fun printError(msg: String, e: Throwable) { println(msg) e.printStackTrace() } private fun isListHiddenFiles() = TcConfigurations.getPreferences().getVariable(TcPreference.LIST_HIDDEN_FILES, TcPreferences.DEFAULT_LIST_HIDDEN_FILES) private fun isSmbExtendedSecurityEnabled() = TcConfigurations.getPreferences().getVariable(TcPreference.SMB_USE_EXTENDED_SECURITY, TcPreferences.DEFAULT_SMB_USE_EXTENDED_SECURITY) private fun isSmbLmCompatibilityEnabled() = TcConfigurations.getPreferences().getVariable(TcPreference.SMB_LM_COMPATIBILITY, TcPreferences.DEFAULT_SMB_LM_COMPATIBILITY) private fun isBonjourEnabled() = TcConfigurations.getPreferences().getVariable(TcPreference.ENABLE_BONJOUR_DISCOVERY, TcPreferences.DEFAULT_ENABLE_BONJOUR_DISCOVERY) private fun isShowSplash() = TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_SPLASH_SCREEN, TcPreferences.DEFAULT_SHOW_SPLASH_SCREEN) private fun isNotificationsEnabled() = TcConfigurations.getPreferences().getVariable(TcPreference.ENABLE_SYSTEM_NOTIFICATIONS, TcPreferences.DEFAULT_ENABLE_SYSTEM_NOTIFICATIONS) ================================================ FILE: src/main/java/com/mucommander/package.html ================================================ muCommander specific wrappers for the various APIs. ================================================ FILE: src/main/java/com/mucommander/process/AbstractProcess.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * muCommander specific version of a process, allowing various types of processes to be executed. *

    * Unlike normal instances of java.lang.Process, abstract processes * will empty their own streams, preventing deadlocks from occurring on some systems. *

    * Note that abstract processes should not be created directly. They should be * instantiated through {@link com.mucommander.process.ProcessRunner#execute(String[], com.mucommander.commons.file.AbstractFile,ProcessListener)}. * * @author Nicolas Rinaudo */ public abstract class AbstractProcess { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractProcess.class); // - Instance fields ------------------------------------------------------- // ------------------------------------------------------------------------- /** Stdout monitor. */ private ProcessOutputMonitor stdoutMonitor; /** Stderr monitor. */ private ProcessOutputMonitor stderrMonitor; // - Process monitoring ---------------------------------------------------- // ------------------------------------------------------------------------- /** * Kills the process. */ public final void destroy() { // Process destruction occurs in a separate thread, as in some (rare) // cases, deadlocks will occur while trying to kill a native process. // An example of that is executing echo blah | ssh localhost ls -l // under MAC OS X. // Using a separate thread allows muCommander to continue working properly even // when that occurs. new Thread(() -> { // Closes the process' streams. LOGGER.debug("Destroying process..."); stdoutMonitor.stopMonitoring(); if (stderrMonitor != null) { stderrMonitor.stopMonitoring(); } // Destroys the process. try { destroyProcess(); } catch(IOException e) { LOGGER.debug("IOException caught", e); } }).start(); } public void waitMonitoring() throws InterruptedException { if (stdoutMonitor != null) { stdoutMonitor.join(); } if (stderrMonitor != null) { stderrMonitor.join(); } } /** * Starts monitoring the process. * @param listener if non null, listener will receive updates about the process' event. * @param encoding encoding that should be used by the process' stdout and stderr streams. */ final void startMonitoring(ProcessListener listener, String encoding) throws IOException { // Only monitors stdout if the process uses merged streams. if (usesMergedStreams()) { LOGGER.debug("Starting process merged output monitor..."); stdoutMonitor = new ProcessOutputMonitor(getInputStream(), encoding, listener, this); stdoutMonitor.setName("Process stdout/stderr monitor"); stdoutMonitor.start(); } // Monitors both stdout and stderr. else { LOGGER.debug("Starting process stdout and stderr monitors..."); stdoutMonitor = new ProcessOutputMonitor(getInputStream(), encoding, listener, this); stdoutMonitor.setName("Process stdout monitor"); stdoutMonitor.start(); stderrMonitor = new ProcessOutputMonitor(getErrorStream(), encoding, listener); stderrMonitor.setName("Process stderr monitor"); stderrMonitor.start(); } } // - Abstract methods ------------------------------------------------------ // ------------------------------------------------------------------------- /** * Returns true if this process only uses one output stream. *

    * Some processes will use a single stream for their standard error and standard output streams. Such * processes should return true here to prevent both streams from being monitored.
    * Note that if a process uses merged streams, {@link #getInputStream()} will be monitored. * * @return true if this process merges his output streams, false otherwise. */ public abstract boolean usesMergedStreams(); /** * Makes the current thread wait for the process to die. * @return the process' exit code. * @throws InterruptedException thrown if the current thread is interrupted while waiting on the process to die. * @throws IOException thrown if an error occurs while waiting for the process to die. */ public abstract int waitFor() throws InterruptedException, IOException; /** * Destroys the process. * @throws IOException thrown if an error occurs while destroying the process. */ protected abstract void destroyProcess() throws IOException; /** * Returns this process' exit value. * @return this process' exit value. */ public abstract int exitValue(); /** * Returns the stream used to send data to the process. * @return the stream used to send data to the process. * @throws IOException thrown if an error occurs while retrieving the process' output stream. */ public abstract OutputStream getOutputStream() throws IOException; /** * Returns the process' standard output stream. * @return the process' standard output stream. * @throws IOException thrown if an error occurs while retrieving the process' input stream. */ public abstract InputStream getInputStream() throws IOException; /** * Returns the process' standard error stream. * @return the process' standard error stream. * @throws IOException thrown if an error occurs while retrieving the process' error stream. */ public abstract InputStream getErrorStream() throws IOException; } ================================================ FILE: src/main/java/com/mucommander/process/DebugProcessListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Listener used while in debug mode. *

    * In debug mode, instances of this listener will automatically be registered to non-monitored processes. * Its only goal is to output information about the process' state. * * @author Nicolas Rinaudo */ class DebugProcessListener implements ProcessListener { private static final Logger LOGGER = LoggerFactory.getLogger(DebugProcessListener.class); // - Instance fields ----------------------------------------------------- // ----------------------------------------------------------------------- /** Command that this listener is monitoring. */ private String command; // - Initialisastion ----------------------------------------------------- // ----------------------------------------------------------------------- /** * Creates a new process listener monitoring the specified command. * @param tokens tokens that compose the command that is being ran. */ public DebugProcessListener(String[] tokens) { StringBuilder buffer; // Rebuilds the command. buffer = new StringBuilder(); for (String token : tokens) { buffer.append(token); buffer.append(' '); } command = buffer.toString(); } // - Process monitoring -------------------------------------------------- // ----------------------------------------------------------------------- /** * Prints out information about the way the process died. * @param returnValue process' return value. */ public void processDied(int returnValue) { LOGGER.debug(command + ": died with return code " + returnValue); } /** * Ignored. */ public void processOutput(byte[] buffer, int offset, int length) { LOGGER.trace(command + ": " + new String(buffer, offset, length)); } /** * Prints out the process output. */ public void processOutput(String output) { LOGGER.trace(command + ": " + output); } } ================================================ FILE: src/main/java/com/mucommander/process/ExecutionFinishListener.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; /** * @author Oleg Trifonov * Created on 22/04/16. */ public interface ExecutionFinishListener { /** * * @param exitCode the process' exit code. * @param output contains the encoded process output. */ void onFinish(int exitCode, String output); } ================================================ FILE: src/main/java/com/mucommander/process/ExecutorUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2015 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import com.mucommander.command.Command; import com.mucommander.commons.file.AbstractFile; import java.io.IOException; /** * @author Oleg Trifonov * Created on 22/04/16. */ public class ExecutorUtils { /** * Executes the specified command in the specified folder. * * @param command command to execute * @param currentFolder where to init the command from. * @param onFinish here to send information about the resulting process. * @param encoding output encoding (system default is used if null). * @return process exit code * @throws IOException * @throws InterruptedException */ public static int executeAndGetOutput(String command, AbstractFile currentFolder, ExecutionFinishListener onFinish, String encoding) throws IOException, InterruptedException { StringBuffer out = new StringBuffer(); AbstractProcess process = execute(command, currentFolder, new ProcessListener() { @Override public void processDied(int returnValue) {} @Override public void processOutput(String output) { out.append(output); } @Override public void processOutput(byte[] buffer, int offset, int length) {} }, encoding); int exitCode = process.waitFor(); process.waitMonitoring(); process.destroy(); if (onFinish != null) { onFinish.onFinish(exitCode, out.toString()); } return exitCode; } public static int executeAndGetOutput(String[] command, AbstractFile currentFolder, ExecutionFinishListener onFinish, String encoding) throws IOException, InterruptedException { StringBuffer out = new StringBuffer(); AbstractProcess process = execute(command, currentFolder, new ProcessListener() { @Override public void processDied(int returnValue) {} @Override public void processOutput(String output) { out.append(output); } @Override public void processOutput(byte[] buffer, int offset, int length) {} }, encoding); int exitCode = process.waitFor(); process.waitMonitoring(); process.destroy(); if (onFinish != null) { onFinish.onFinish(exitCode, out.toString()); } return exitCode; } /** * Executes the specified command in the specified folder, using system default encoding. * * @param command command to execute * @param currentFolder where to init the command from. * @param executionFinishListener here to send information about the resulting process. * @return exit code * @throws IOException * @throws InterruptedException */ public static int executeAndGetOutput(String command, AbstractFile currentFolder, ExecutionFinishListener executionFinishListener) throws IOException, InterruptedException { return executeAndGetOutput(command, currentFolder, executionFinishListener, null); } public static int executeAndGetOutput(String[] command, AbstractFile currentFolder, ExecutionFinishListener executionFinishListener) throws IOException, InterruptedException { return executeAndGetOutput(command, currentFolder, executionFinishListener, null); } public static int execute(String command, AbstractFile currentFolder) throws IOException, InterruptedException { return executeAndGetOutput(command, currentFolder, null); } public static int execute(String command) throws IOException, InterruptedException { return executeAndGetOutput(command, null, null); } public static int execute(String[] command) throws IOException, InterruptedException { return executeAndGetOutput(command, null, null); } private static AbstractProcess execute(String command, AbstractFile currentFolder, ProcessListener listener, String encoding) throws IOException { String[] tokens = Command.getTokens(command); return encoding == null ? ProcessRunner.execute(tokens, currentFolder, listener) : ProcessRunner.execute(tokens, currentFolder, listener, encoding); } private static AbstractProcess execute(String[] command, AbstractFile currentFolder, ProcessListener listener, String encoding) throws IOException { return encoding == null ? ProcessRunner.execute(command, currentFolder, listener) : ProcessRunner.execute(command, currentFolder, listener, encoding); } } ================================================ FILE: src/main/java/com/mucommander/process/LocalProcess.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Process running on the local computer. * @author Nicolas Rinaudo */ public class LocalProcess extends AbstractProcess { private static final Logger LOGGER = LoggerFactory.getLogger(LocalProcess.class); /** Underlying system process. */ private final Process process; /** * Creates a new local process running the specified command. * @param tokens command to init and its parameters. * @param dir directory in which to start the command. * @throws IOException if the process could not be created. */ public LocalProcess(String[] tokens, File dir) throws IOException { ProcessBuilder pb = new ProcessBuilder(removeOuterQuotationMarks(tokens)); // Set the process' working directory pb.directory(dir); // Merge the process' stdout and stderr pb.redirectErrorStream(true); process = pb.start(); } private boolean isQuotedWith(String string, String quotationMark) { return string.startsWith(quotationMark) && string.endsWith(quotationMark); } private String[] removeOuterQuotationMarks(String[] tokens) { String[] newTokens = new String[tokens.length]; for(int i = 0; i < tokens.length; ++i) { String token = tokens[i]; if (token.length() > 1 && isQuotedWith(token, "\"") || isQuotedWith(token, "'")) { token = token.substring(1, token.length() - 1); } newTokens[i]= token; } return newTokens; } /** * Returns true if the current JRE version supports merged java.lang.Process streams. * @return true if the current JRE version supports merged java.lang.Process streams, false otherwise. */ @Override public boolean usesMergedStreams() { // TODO remove it return true;//JavaVersion.JAVA_1_5.isCurrentOrHigher(); } /** * Waits for the process to die. * @return the process' exit code. * @throws InterruptedException if the thread was interrupted while waiting on the process to die. */ @Override public int waitFor() throws InterruptedException { return process.waitFor(); } /** * Destroys the process. */ @Override protected void destroyProcess() { process.destroy(); } /** * Returns the process' exit value. * @return the process' exit value. */ @Override public int exitValue() { return process.exitValue(); } /** * Returns the process' output stream. * @return the process' output stream. */ @Override public OutputStream getOutputStream() { return process.getOutputStream(); } /** * Returns the process' error stream. *

    * On Java 1.5 or higher, this will throw an java.io.IOException, as we're using * merged output streams. Developers should protect themselves against this by checking * {@link #usesMergedStreams()} before accessing streams. * * @return the process' error stream. * @throws IOException if this process is using merged streams. */ @Override public InputStream getErrorStream() throws IOException { if (usesMergedStreams()) { LOGGER.debug("Tried to access the error stream of a merged streams process."); throw new IOException(); } return process.getErrorStream(); } /** * Returns the process' input stream. * @return the process' input stream. */ @Override public InputStream getInputStream() { return process.getInputStream(); } } ================================================ FILE: src/main/java/com/mucommander/process/ProcessListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; /** * Implementations of this interface can listen to a process' state and streams. * @see com.mucommander.process.AbstractProcess * @author Maxence Bernard, Nicolas Rinaudo */ public interface ProcessListener { /** * This method is called when the process dies. No more calls to processOutput and * processError will be made past this call. * @param returnValue the value returned by the process (return code). */ void processDied(int returnValue); /** * This method is called whenever the process sends data to its output streams (stdout or stderr). *

    * The output passed to this method is encoded. Listener that need to work with raw bytes should * use {@link #processOutput(byte[],int,int)} instead. * * @param output contains the encoded process output. */ void processOutput(String output); /** * This method is called whenever the process sends data to its output streams (stdout or stderr). *

    * The output passed to this method is raw and doesn't take encoding into account. Listeners that * need to work with properly encoded output should use {@link #processOutput(String)} instead. * * @param buffer contains the process' output. * @param offset offset in buffer at which the process' output starts. * @param length length of the process' output in buffer. */ void processOutput(byte[] buffer, int offset, int length); } ================================================ FILE: src/main/java/com/mucommander/process/ProcessListenerList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import java.util.List; import java.util.Vector; /** * Convenience class used to have more than one listener on any given process. * @author Nicolas Rinaudo */ public class ProcessListenerList implements ProcessListener { // - Instance fields ----------------------------------------------------- // ----------------------------------------------------------------------- /** All registered listeners. */ private final List listeners = new Vector<>(); // - Listener registration ----------------------------------------------- // ----------------------------------------------------------------------- /** * Adds the specified listener to the list of listeners. * @param listener process listener to add. */ public void add(ProcessListener listener) {listeners.add(listener);} /** * Removes the specified listener from the list of listeners. * @param listener process listener to remove. */ public void remove(ProcessListener listener) {listeners.remove(listener);} // - Listener code ------------------------------------------------------- // ----------------------------------------------------------------------- /** * Propagates the process died event to all registered listeners. */ public void processDied(int returnValue) { for (ProcessListener listener : listeners) { listener.processDied(returnValue); } } /** * Propagates the process output event to all registered listeners. */ public void processOutput(byte[] buffer, int offset, int length) { for (ProcessListener listener : listeners) { listener.processOutput(buffer, offset, length); } } /** * Propagates the process output event to all registered listeners. */ public void processOutput(String output) { for (ProcessListener listener : listeners) { listener.processOutput(output); } } } ================================================ FILE: src/main/java/com/mucommander/process/ProcessOutputMonitor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import java.io.IOException; import java.io.InputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Used to monitor a process' stdout and stderr streams. *

    * This class is used by {@link com.mucommander.process.AbstractProcess} to make sure that * processes do not stall because their stdout and stderr streams are not emptied. *

    * This implementation is rather hackish, and should not be used directly: it works, but is not * meant to support anything but the very specific needs of {@link com.mucommander.process.AbstractProcess}. * * @author Nicolas Rinaudo */ class ProcessOutputMonitor extends Thread { private static final Logger LOGGER = LoggerFactory.getLogger(ProcessOutputMonitor.class); /** Stream to read from. */ private InputStream in; private final String encoding; /** Listener to notify of updates. */ private final ProcessListener listener; /** Process to wait on once the stream is closed. */ private AbstractProcess process; /** Whether the process is still being monitored. */ private boolean monitor; /** * Creates a news ProcessOutputMonitor that will read from in and notify listener. * @param in input stream to 'empty'. * @param listener where to send the content of the stream. */ public ProcessOutputMonitor(InputStream in, String encoding, ProcessListener listener) { this.listener = listener; this.in = in; this.monitor = true; this.encoding = encoding; } /** * Creates a news ProcessOutputMonitor that will read from in and notify listener. *

    * A process monitor created that way will also wait on the specified process before quiting, * and notify the listener when the process has actually died. * * @param in input stream to 'empty'. * @param listener where to send the content of the stream. * @param process process to wait on. */ public ProcessOutputMonitor(InputStream in, String encoding, ProcessListener listener, AbstractProcess process) { this(in, encoding, listener); this.process = process; } // - Main code ------------------------------------------------------------- // ------------------------------------------------------------------------- /** * Empties the content of the stream and notifies the listener. */ public void run() { int read; // Number of bytes read in the last read operation. byte[] buffer = new byte[512]; // Where to store the stream's output. // Reads the content of the stream. try { while (monitor && ((read = in.read(buffer, 0, buffer.length)) != -1)) { if (listener != null) { listener.processOutput(buffer, 0, read); if (encoding == null) { listener.processOutput(new String(buffer, 0, read)); } else { listener.processOutput(new String(buffer, 0, read, encoding)); } } } } // Ignore this exception: either there's nothing we can do about it anyway, // or it's 'normal' (the process has been killed). catch (IOException e) { LOGGER.debug("IOException thrown while monitoring process", e); } LOGGER.debug("Process output stream emptied, closing"); // Closes the stream. try { if (in != null) { in.close(); } } catch(IOException e) { LOGGER.debug("IOException thrown while closing process stream", e); } // If a process was set, perform 'cleanup' tasks. if (process != null) { // Waits for the process to die. try { process.waitFor(); } catch(Exception e) { LOGGER.debug("Caught Exception while waiting for process "+process, e); } // If this process is still being monitored, notifies its // listener that it has exited. if (monitor && (listener != null)) { listener.processDied(process.exitValue()); } } } /** * Notifies the monitor that it should stop reading from the stream it's been affected to. *

    * Calling this method will cause the process' stream to stop being read. For this reason, * it should only be called right before the process is killed, as it will otherwise stall. */ public void stopMonitoring() { // Closes the input stream. try { in.close(); } catch(Exception ignore) {} // Notifies the main thread that it should stop monitoring the stream. in = null; monitor = false; } } ================================================ FILE: src/main/java/com/mucommander/process/ProcessRunner.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.process; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.impl.local.LocalFile; import java.io.File; import java.io.IOException; import java.util.StringTokenizer; /** * Used to init process in as safe a manner as possible. *

    * The Java process API, while very simple, contains a lot of pitfalls and requires some work to use properly. * Typical errors are forgetting to monitor a process' output streams, which will make it deadlock more often than not. *

    * Using the ProcessRunner will take care of all these tasks, while still allowing most of the flexibility * of the standard API. * * @author Nicolas Rinaudo */ public class ProcessRunner { /** * Prevents instances of ProcessRunner from being created. */ private ProcessRunner() { } /** * Executes the specified command in the specified directory. *

    * Note that both currentDirectory and listener can be set to null.
    * If no current directory is specified, the VM's current directory will be used. Moreover, if the current directory * is not on residing on local file system, the user's home directory will be used instead. Finally, if the current * directory is on a local file system but is actually not a {@link AbstractFile#isDirectory() directory} * (an archive entry for instance), the first file's parent that is an actual directory will be used. *
    * If listener is set to null, nobody will be notified of the process' state. Its streams * will still be emptied to prevent deadlocks. * * @param tokens tokens that compose the command to execute. * @param currentDirectory directory in which to execute the process (user directory if null). * @param listener object that will be notified of modifications in the process' state (ignored if null). * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @throws IOException thrown if any error occurs while creating the process. */ public static AbstractProcess execute(String[] tokens, AbstractFile currentDirectory, ProcessListener listener, String encoding) throws IOException { // If currentDirectory is null, use the VM's current directory. if (currentDirectory == null) { currentDirectory = FileFactory.getFile(System.getProperty("user.dir"), true); } else { // If currentDirectory is not on a local filesystem, use the user's home. if (!currentDirectory.hasAncestor(LocalFile.class)) { currentDirectory = FileFactory.getFile(System.getProperty("user.home"), true); } // If currentDirectory is not a directory (e.g. an archive entry) else { while (currentDirectory != null && !currentDirectory.isDirectory()) { currentDirectory = currentDirectory.getParent(); } // This shouldn't normally happen if (currentDirectory == null) { currentDirectory = FileFactory.getFile(System.getProperty("user.dir"), true); } } } // // Register a debug process listener. // if(listener == null) // listener = new DebugProcessListener(tokens); // Starts the process. File dir = currentDirectory == null ? null : (File)currentDirectory.getUnderlyingFileObject(); AbstractProcess process = new LocalProcess(tokens, dir); process.startMonitoring(listener, encoding); return process; } // - Helper methods ------------------------------------------------------ // ----------------------------------------------------------------------- /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, currentDirectory, listener, null). * * @param command command to execute. * @param currentDirectory directory in which to execute the process (user directory if null). * @param listener object that will be notified of modifications in the process' state (ignored if null). * @return the generated process. * @throws IOException thrown if any error occurs while creating the process. */ public static AbstractProcess execute(String command, AbstractFile currentDirectory, ProcessListener listener) throws IOException { return execute(command, currentDirectory, listener, null); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, null, null, null). * * @param command command to execute. * @return the generated process. * @see #execute(String,AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command) throws IOException { return execute(command, null, null, null); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, null, null, encoding). * * @param command command to execute. * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @see #execute(String,AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command, String encoding) throws IOException { return execute(command, null, null, encoding); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, null, listener, null). * * @param command command to execute. * @param listener object that will be notified of any modification in the process' state (ignored if null). * @return the generated process. * @see #execute(String,AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command, ProcessListener listener) throws IOException { return execute(command, null, listener, null); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, null, listener, encoding). * * @param command command to execute. * @param listener object that will be notified of any modification in the process' state. * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @see #execute(String,AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command, ProcessListener listener, String encoding) throws IOException { return execute(command, null, listener, encoding); } /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, currentDirectory, null, null). * * @param command command to execute. * @param currentDirectory directory in which to init the command. * @return the generated process. * @see #execute(String,AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command, AbstractFile currentDirectory) throws IOException { return execute(command, currentDirectory, null, null); } /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(command, currentDirectory, null, encoding). * * @param command command to execute. * @param currentDirectory directory in which to init the command (uses the VM's current directory if null). * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @see #execute(String,AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command, AbstractFile currentDirectory, String encoding) throws IOException { return execute(command, currentDirectory, null, encoding); } /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, currentDirectory, null, encoding) where tokens * is an array contains all the tokens found in command. *

    * More precisely, the command string is broken into tokens using a StringTokenizer created by the call * new StringTokenizer(command) with no further modification of the character categories. The tokens produced by the * tokenizer are then placed in the new string array tokens, in the same order. * * @param command command to execute. * @param currentDirectory directory in which to init the command (uses the VM's current directory if null). * @param encoding encoding used to read from the process' stream (system default is used if null). * @param listener object that will be notified of modifications in the process' state (ignored if null). * @return the generated process. * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String command, AbstractFile currentDirectory, ProcessListener listener, String encoding) throws IOException { // Initialisation. StringTokenizer parser = new StringTokenizer(command); // Used to parse the command. String[] tokens = new String[parser.countTokens()]; // Tokens that make up the command. // Breaks command into tokens. for(int i = 0; i < tokens.length; i++) { tokens[i] = parser.nextToken(); } // Starts the process. return execute(tokens, currentDirectory, listener, encoding); } /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, currentDirectory, listener, null). * * @param tokens command to execute. * @param currentDirectory directory in which to init the command (uses the VM's current directory if null). * @param listener object that will be notified of any modification in the process' state. * @return the generated process. * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens, AbstractFile currentDirectory, ProcessListener listener) throws IOException { return execute(tokens, currentDirectory, listener, null); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, null, null, null). * * @param tokens command to execute. * @return the generated process. * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens) throws IOException { return execute(tokens, null, null, null); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, null, null, encoding). * * @param tokens command to execute. * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens, String encoding) throws IOException { return execute(tokens, null, null, encoding); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, null, listener, null). * * @param tokens command to execute. * @param listener object that will be notified of any modification in the process' state (ignored if null). * @return the generated process. * @see #execute(String[],AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens, ProcessListener listener) throws IOException { return execute(tokens, null, listener, null); } /** * Executes the specified command in the VM's current directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, null, listener, encoding). * * @param tokens command to execute. * @param listener object that will be notified of any modification in the process' state (ignored if null). * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @see #execute(String[],AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens, ProcessListener listener, String encoding) throws IOException { return execute(tokens, null, listener, encoding); } /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, currentDirectory, null, null). * * @param tokens command to execute. * @param currentDirectory directory in which to init the command. * @return the generated process. * @see #execute(String[],AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens, AbstractFile currentDirectory) throws IOException { return execute(tokens, currentDirectory, null, null); } /** * Executes the specified command in the specified directory. *

    * This is a convenience method and behaves exactly as a call to execute(tokens, currentDirectory, null, null). * * @param tokens command to execute. * @param currentDirectory directory in which to init the command. * @param encoding encoding used to read from the process' stream (system default is used if null). * @return the generated process. * @see #execute(String[],AbstractFile,ProcessListener,String) * @throws IOException thrown if an error happens while starting the process. */ public static AbstractProcess execute(String[] tokens, AbstractFile currentDirectory, String encoding) throws IOException { return execute(tokens, currentDirectory, null, encoding); } } ================================================ FILE: src/main/java/com/mucommander/process/package.html ================================================ Generic process management API. ================================================ FILE: src/main/java/com/mucommander/profiler/Profiler.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.profiler; import java.util.*; /** * Created on 01/01/14. */ public class Profiler { public static final boolean ENABLED = true; private static final Map timesStart = new HashMap<>(); private static final Map timesDuration = new HashMap<>(); private static final Map callCount = new HashMap<>(); private static final Set hiddenGroups = new HashSet<>(); private static String lastSectionName; private static final List initThreads = new ArrayList<>(); public static long getTime() { // return System.nanoTime(); return System.currentTimeMillis(); } public static void start(String name) { if (!ENABLED) { return; } lastSectionName = name; synchronized (timesStart) { timesStart.put(name, getTime()); } synchronized (callCount) { Integer cnt = callCount.get(name); if (cnt == null) { cnt = 1; } else { cnt++; } callCount.put(name, cnt); } } public static void stop(String name) { if (!ENABLED) { return; } long endTime = getTime(); long startTime; synchronized (timesStart) { startTime = timesStart.get(name); } long duration = endTime - startTime; synchronized (timesDuration) { Long sum = timesDuration.get(name); if (sum == null) { sum = duration; } else { sum += duration; } timesDuration.put(name, sum); } } public static void stop() { if (!ENABLED) { return; } stop(lastSectionName); } public static void print() { if (!ENABLED) { return; } synchronized (timesDuration) { TreeMap sortedMap = new TreeMap<>(new ValueComparator(timesDuration)); sortedMap.putAll(timesDuration); System.out.println(withSpaces("Name", 40) + "\t" + withSpaces("Total", 10) + "\t" + withSpaces("Count", 7) + "\t" + "Average"); System.out.println(withSpaces("-----------", 40) + "\t" + withSpaces("--------", 10) + "\t" + withSpaces("-------", 7) + "\t" + "----------"); for (String name : sortedMap.keySet()) { boolean isHidden = false; for (String hiddenName : hiddenGroups) { if (name.contains(hiddenName)) { isHidden = true; break; } } if (isHidden) { continue; } long duration = timesDuration.get(name); int count = callCount.get(name); String avgDuration = String.valueOf(duration/count); if (avgDuration.equals("0") && duration != 0) { avgDuration = String.valueOf(1.0*duration/count); } System.out.println(withSpaces(name, 40) + "\t" + withSpaces(Long.toString(duration), 10) + "\t" + withSpaces(Integer.toString(count), 7) + "\t" + avgDuration); } } } private static String withSpaces(String name, int len) { StringBuilder nameBuilder = new StringBuilder(name); while (nameBuilder.length() < len) { nameBuilder.append(" "); } name = nameBuilder.toString(); return name; } static class ValueComparator implements Comparator { Map base; public ValueComparator(Map base) { this.base = base; } // Note: this comparator imposes orderings that are inconsistent with equals. @Override public int compare(String a, String b) { if (base.get(a) >= base.get(b)) { return -1; } else { return 1; } // returning 0 would merge keys } } public static void hide(String name) { hiddenGroups.add(name); } public static void unhide(String name) { hiddenGroups.remove(name); } /* public static void printThreads() { System.out.println("---------------------------"); Set threads = Thread.getAllStackTraces().keySet(); for (Thread thread : threads) { String s = thread2str(thread); if (initThreads.contains(s)) { continue; } System.out.println(s); } System.out.println("---------------------------"); } public static void initThreads() { if (initThreads.size() > 0) { return; } Set threads = Thread.getAllStackTraces().keySet(); for (Thread thread : threads) { initThreads.add(thread2str(thread)); } } private static String thread2str(Thread thread) { return thread.getId() + ":" + thread.getName() + ":" + thread.getState(); } */ } ================================================ FILE: src/main/java/com/mucommander/shell/Shell.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; import java.io.IOException; import com.mucommander.process.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.command.Command; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; /** * Used to execute shell commands. * @author Maxence Bernard, Nicolas Rinaudo */ public class Shell implements ConfigurationListener { private static final Logger LOGGER = LoggerFactory.getLogger(Shell.class); // - Class variables ----------------------------------------------------- // ----------------------------------------------------------------------- /** Encoding used by the shell. */ private static String encoding; /** Whether encoding should be auto-detected or not. */ private static boolean autoDetectEncoding; /** Tokens that compose the shell command. */ private static String[] tokens; /** Tokens that compose remote shell commands. */ private static final String[] remoteTokens; /** Instance of configuration listener. */ private static final Shell confListener; static { TcConfigurations.addPreferencesListener(confListener = new Shell()); // This could in theory also be written without the confListener reference. // It turns out, however, that proGuard is a bit too keen when removing fields // he thinks are not used. This code is written that way to make sure // confListener is not taken out, and the ConfigurationListener instance removed // instantly as there is only a WeakReference on it. // The things we have to do... Shell.setShellCommand(); remoteTokens = new String[1]; } /** * Prevents instances of Shell from being created. */ private Shell() {} // - Shell interaction --------------------------------------------------- // ----------------------------------------------------------------------- /** * Executes the specified command in the specified folder. *

    * The currentFolder folder parameter will only be used if it's neither a * remote directory nor an archive. Otherwise, the command will init from the user's * home directory. * * @param command command to init. * @param currentFolder where to init the command from. * @return the resulting process. * @exception IOException thrown if any error occurs while trying to init the command. */ public static AbstractProcess execute(String command, AbstractFile currentFolder) throws IOException { return execute(command, currentFolder, null); } /** * Executes the specified command in the specified folder. *

    * The currentFolder folder parameter will only be used if it's neither a * remote directory nor an archive. Otherwise, the command will init from the user's * home directory. *

    * Information about the resulting process will be sent to the specified listener. * * @param command command to init. * @param currentFolder where to init the command from. * @param listener where to send information about the resulting process. * @return the resulting process. * @exception IOException thrown if any error occurs while trying to init the command. */ public static synchronized AbstractProcess execute(String command, AbstractFile currentFolder, ProcessListener listener) throws IOException { LOGGER.debug("Executing " + command); // Adds the command to history. ShellHistoryManager.add(command); // Builds the shell command. // Local files use the configuration defined shell. Remote files // will execute the command as-is. String[] commandTokens; if (currentFolder == null || currentFolder.hasAncestor(LocalFile.class)) { tokens[tokens.length - 1] = command; commandTokens = tokens; } else { remoteTokens[0] = command; commandTokens = remoteTokens; } // Starts the process. if (autoDetectEncoding) { if (listener == null) { listener = new ShellEncodingListener(); } else { ProcessListenerList listeners = new ProcessListenerList(); listeners.add(listener); listeners.add(new ShellEncodingListener()); listener = listeners; } } return (encoding == null) ? ProcessRunner.execute(commandTokens, currentFolder, listener) : ProcessRunner.execute(commandTokens, currentFolder, listener, encoding); } // - Configuration management -------------------------------------------- // ----------------------------------------------------------------------- /** * Extracts the shell command from configuration. */ private static synchronized void setShellCommand() { String shellCommand; // Retrieves the configuration defined shell command. if (TcConfigurations.getPreferences().getVariable(TcPreference.USE_CUSTOM_SHELL, TcPreferences.DEFAULT_USE_CUSTOM_SHELL)) shellCommand = TcConfigurations.getPreferences().getVariable(TcPreference.CUSTOM_SHELL, DesktopManager.getDefaultShell()); else shellCommand = DesktopManager.getDefaultShell(); // Splits the command into tokens, leaving room for the argument. String[] buffer = Command.getTokens(shellCommand); tokens = new String[buffer.length + 1]; System.arraycopy(buffer, 0, tokens, 0, buffer.length); // Retrieves encoding configuration. encoding = TcConfigurations.getPreferences().getVariable(TcPreference.SHELL_ENCODING); autoDetectEncoding = TcConfigurations.getPreferences().getVariable(TcPreference.AUTODETECT_SHELL_ENCODING, TcPreferences.DEFAULT_AUTODETECT_SHELL_ENCODING); } /** * Reacts to configuration changes. */ public void configurationChanged(ConfigurationEvent event) { if (event.getVariable().startsWith(TcPreferences.SHELL_SECTION)) { setShellCommand(); } } } ================================================ FILE: src/main/java/com/mucommander/shell/ShellEncodingListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; import java.io.ByteArrayOutputStream; import java.nio.charset.Charset; import com.mucommander.commons.io.EncodingDetector; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.process.ProcessListener; /** * Listens to shell output and tries to guess at its encoding. * @author Nicolas Rinaudo */ class ShellEncodingListener implements ProcessListener { private static ByteArrayOutputStream out = new ByteArrayOutputStream(); public synchronized void processDied(int returnValue) { // Abort if there is no need to identify the encoding anymore. if (out == null) { return; } // Attempts to guess at the encoding. If no guess can be made, ignore. String encoding = EncodingDetector.detectEncoding(out.toByteArray()); if (encoding == null) { return; } // Checks whether the detected charset is supported. if (Charset.isSupported(encoding)) { String oldEncoding = TcConfigurations.getPreferences().getVariable(TcPreference.SHELL_ENCODING); // If no encoding was previously set, or we have found a new encoding, change the current shell encoding. if(!encoding.equals(oldEncoding)) { TcConfigurations.getPreferences().setVariable(TcPreference.SHELL_ENCODING, encoding); } // Stop listening for new byte input if we have gathered a large enough sample set. if (out.size() >= EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE) { out = null; } } } /** * Ignored. */ public void processOutput(String output) {} public synchronized void processOutput(byte[] buff, int from, int len) { if (out != null && (len = Math.min(len, EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE - out.size())) > 0) { out.write(buff, from, len); } } } ================================================ FILE: src/main/java/com/mucommander/shell/ShellHistoryConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; /** * Defines the structure of the shell history XML file. * @author Nicolas Rinaudo */ interface ShellHistoryConstants { /** Name of the XML file's root element. */ String ROOT_ELEMENT = "history"; /** Name of a command element in the XML file. */ String COMMAND_ELEMENT = "command"; /** Name of the root element's attribute containing the muCommander version that was used to create the shell history file */ String ATTRIBUTE_VERSION = "version"; } ================================================ FILE: src/main/java/com/mucommander/shell/ShellHistoryListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; /** * Interface used to monitor changes to the shell history. * @author Nicolas Rinaudo */ public interface ShellHistoryListener { /** * Notifies the listener that a new element has been added to the history. * @param command command that was added to the history. */ void historyChanged(String command); /** * Notifies the listeners that the history has been cleared. */ void historyCleared(); } ================================================ FILE: src/main/java/com/mucommander/shell/ShellHistoryManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.WeakHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.io.backup.BackupOutputStream; /** * Used to manage shell HISTORY. *

    * Using this class is fairly basic: you can add elements to the shell HISTORY through * {@link #add(String)} and browse it through {@link #getHistoryIterator()}. * * @author Nicolas Rinaudo */ public class ShellHistoryManager { private static Logger logger; /** File in which to store the shell HISTORY. */ private static final String DEFAULT_HISTORY_FILE_NAME = "shell_history.xml"; /** List of shell HISTORY registered LISTENERS. */ private static final WeakHashMap LISTENERS; /** Stores the shell HISTORY. */ private static final String[] HISTORY; /** Index of the first element of the HISTORY. */ private static int historyStart; /** Index of the last element of the HISTORY. */ private static int historyEnd; /** Path to the HISTORY file. */ private static AbstractFile historyFile; /** * Prevents instantiations of the class. */ private ShellHistoryManager() {} static { HISTORY = new String[TcConfigurations.getPreferences().getVariable(TcPreference.SHELL_HISTORY_SIZE, TcPreferences.DEFAULT_SHELL_HISTORY_SIZE)]; LISTENERS = new WeakHashMap<>(); } /** * Registers a listener to changes in the shell HISTORY. * @param listener listener to register. */ public static void addListener(ShellHistoryListener listener) { LISTENERS.put(listener, null);} /** * Propagates shell HISTORY events to all registered LISTENERS. * @param command command that was added to the shell HISTORY. */ private static void triggerEvent(String command) { for (ShellHistoryListener listener : LISTENERS.keySet()) { listener.historyChanged(command); } } // - History access ------------------------------------------------------------- // ------------------------------------------------------------------------------ /** * Completely empties the shell HISTORY. */ public static void clear() { // Empties HISTORY. historyStart = 0; historyEnd = 0; // Notifies LISTENERS. for (ShellHistoryListener listener : LISTENERS.keySet()) { listener.historyCleared(); } } /** * Returns a non thread-safe iterator on the HISTORY. * @return an iterator on the HISTORY. */ public static Iterator getHistoryIterator() {return new HistoryIterator();} /** * Adds the specified command to shell HISTORY. * @param command command to add to the shell HISTORY. */ public static void add(String command) { // Ignores empty commands. if (command.trim().isEmpty()) { return; } // Ignores the command if it's the same as the last one. // There is no last command if HISTORY is empty. if (historyEnd != historyStart) { // Computes the index of the previous command. int lastIndex = historyEnd == 0 ? HISTORY.length - 1 : historyEnd - 1; if (command.equals(HISTORY[lastIndex])) { return; } } getLogger().debug("Adding " + command + " to shell HISTORY."); // Updates the HISTORY buffer. HISTORY[historyEnd] = command; historyEnd++; // Wraps around the HISTORY buffer. if (historyEnd == HISTORY.length) { historyEnd = 0; } // Clears items from the beginning of the buffer if necessary. if (historyEnd == historyStart) { if (++historyStart == HISTORY.length) { historyStart = 0; } } // Propagates the event. triggerEvent(command); } // - History saving / loading --------------------------------------------------- // ------------------------------------------------------------------------------ /** * Sets the path of the shell HISTORY file. * @param path where to load the shell HISTORY from. * @exception FileNotFoundException if path is not accessible. * @see #getHistoryFile() * @see #setHistoryFile(File) * @see #setHistoryFile(AbstractFile) */ public static void setHistoryFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setHistoryFile(new File(path)); } else { setHistoryFile(file); } } /** * Sets the path of the shell HISTORY file. * @param file where to load the shell HISTORY from. * @exception FileNotFoundException if path is not accessible. * @see #getHistoryFile() * @see #setHistoryFile(AbstractFile) * @see #setHistoryFile(String) */ public static void setHistoryFile(File file) throws FileNotFoundException { setHistoryFile(FileFactory.getFile(file != null ? file.getAbsolutePath() : null)); } /** * Sets the path of the shell HISTORY file. * @param file where to load the shell HISTORY from. * @exception FileNotFoundException if path is not accessible. * @see #getHistoryFile() * @see #setHistoryFile(File) * @see #setHistoryFile(String) */ public static void setHistoryFile(AbstractFile file) throws FileNotFoundException { // Makes sure file can be used as a shell HISTORY file. if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } historyFile = file; } /** * Returns the path to the shell HISTORY file. *

    * This method cannot guarantee the file's existence, and it's up to the caller * to deal with the fact that the user might not actually have created a HISTORY file yet. *

    * This method's return value can be modified through {@link #setHistoryFile(String)}. * If this wasn't called, the default path will be used: {@link #DEFAULT_HISTORY_FILE_NAME} * in the {@link com.mucommander.PlatformManager#getPreferencesFolder() preferences} folder. * * @return the path to the shell HISTORY file. * @throws IOException if an error occurred while locating the default shell HISTORY file. * @see #setHistoryFile(File) * @see #setHistoryFile(String) * @see #setHistoryFile(AbstractFile) */ public static AbstractFile getHistoryFile() throws IOException { if (historyFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_HISTORY_FILE_NAME); } return historyFile; } /** * Writes the shell HISTORY to hard drive. * @throws IOException if an I/O error occurs. */ public static void writeHistory() throws IOException { try (BackupOutputStream out = new BackupOutputStream(getHistoryFile())) { ShellHistoryWriter.write(out); } } /** * Loads the shell HISTORY. * @throws Exception if an error occurs. */ public static void loadHistory() throws Exception { try (BackupInputStream in = new BackupInputStream(getHistoryFile())) { ShellHistoryReader.read(in); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ShellHistoryManager.class); } return logger; } /** * Iterator used to browse HISTORY. * @author Nicolas Rinaudo */ static class HistoryIterator implements Iterator { /** Index in the HISTORY. */ private int index; /** * Creates a new HISTORY iterator. */ HistoryIterator() { index = ShellHistoryManager.historyStart; } /** * Returns true if there are more elements to iterate through. * @return true if there are more elements to iterate through, false otherwise. */ public boolean hasNext() { return index != ShellHistoryManager.historyEnd; } /** * Returns the next element in the HISTORY. * @return the next element in the HISTORY. */ public String next() throws NoSuchElementException { if (!hasNext()) { throw new NoSuchElementException(); } String value = ShellHistoryManager.HISTORY[index]; if (++index == ShellHistoryManager.HISTORY.length) { index = 0; } return value; } /** * Operation not supported. */ public void remove() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } } ================================================ FILE: src/main/java/com/mucommander/shell/ShellHistoryReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; import java.io.InputStream; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; /** * Parses XML shell history files and populates the {@link com.mucommander.shell.ShellHistoryManager}. * @author Nicolas Rinaudo */ class ShellHistoryReader extends DefaultHandler implements ShellHistoryConstants { /** Parsing hasn't started. */ private static final int STATUS_UNKNOWN = 0; /** Currently parsing the root tag. */ private static final int STATUS_ROOT = 1; /** Currently parsing a command tag. */ private static final int STATUS_COMMAND = 2; /** Reader's current status. */ private int status; /** Buffer for the current command. */ private final StringBuilder command; /** muCommander version that was used to write the shell history file */ private String version; /** * Creates a new shell history reader. */ private ShellHistoryReader() { command = new StringBuilder(); status = STATUS_UNKNOWN; } /** * Returns the muCommander version that was used to write the shell history file, null if it is unknown. *

    * Note: the version attribute was introduced in muCommander 0.8.4. * * @return the muCommander version that was used to write the shell history file, null if it is unknown. */ public String getVersion() { return version; } // - XML interaction ----------------------------------------------------- // ----------------------------------------------------------------------- /** * Reads shell history from the specified input stream. * @param in where to read the history from. */ public static void read(InputStream in) throws Exception {SAXParserFactory.newInstance().newSAXParser().parse(in, new ShellHistoryReader());} /** * Notifies the reader that CDATA has been encountered. */ @Override public void characters(char[] ch, int start, int length) { if (status == STATUS_COMMAND) { command.append(ch, start, length); } } /** * Notifies the reader that a new XML element is starting. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if (qName.equals(ROOT_ELEMENT) && (status == STATUS_UNKNOWN)) { // Root element declaration. status = STATUS_ROOT; version = attributes.getValue(ATTRIBUTE_VERSION); } else if(qName.equals(COMMAND_ELEMENT) && status == STATUS_ROOT) { // Command element declaration. status = STATUS_COMMAND; } } /** * Notifies the reader that the current element declaration is over. */ @Override public void endElement(String uri, String localName, String qName) { if (qName.equals(ROOT_ELEMENT) && (status == STATUS_ROOT)) { // Root element finished. status = STATUS_UNKNOWN; } else if(qName.equals(COMMAND_ELEMENT) && (status == STATUS_COMMAND)) { // Command element finished. status = STATUS_ROOT; // Adds the current command to shell history. ShellHistoryManager.add(command.toString().trim()); command.setLength(0); } } } ================================================ FILE: src/main/java/com/mucommander/shell/ShellHistoryWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.shell; import java.io.OutputStream; import java.util.Iterator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.RuntimeConstants; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; /** * Used to save the content of the {@link com.mucommander.shell.ShellHistoryManager} to a file. * @author Nicolas Rinaudo */ class ShellHistoryWriter implements ShellHistoryConstants { private static final Logger LOGGER = LoggerFactory.getLogger(ShellHistoryWriter.class); /** * Writes the content of the {@link com.mucommander.shell.ShellHistoryManager} to the specified output stream. * @param stream where to save the shell history. */ public static void write(OutputStream stream) { // Initialises writing. Iteratorhistory = ShellHistoryManager.getHistoryIterator(); try { // Opens the file for writing. XmlWriter out = new XmlWriter(stream); // Version the file XmlAttributes attributes = new XmlAttributes(); attributes.add(ATTRIBUTE_VERSION, RuntimeConstants.VERSION); out.startElement(ROOT_ELEMENT, attributes); out.println(); // Writes the content of the shell history. while (history.hasNext()) { out.startElement(COMMAND_ELEMENT); out.writeCData(history.next()); out.endElement(COMMAND_ELEMENT); } out.endElement(ROOT_ELEMENT); } catch(Exception e) { LOGGER.debug("Failed to write shell history", e); } } } ================================================ FILE: src/main/java/com/mucommander/shell/package.html ================================================ Generic shell and shell history API. ================================================ FILE: src/main/java/com/mucommander/tools/AdbTool.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2015 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.tools; import com.mucommander.process.AbstractProcess; import com.mucommander.process.ProcessRunner; /** * @author Oleg Trifonov * Created on 09/09/15. */ public class AdbTool extends ExternalTool { @Override boolean detect() { try { AbstractProcess process = ProcessRunner.execute("adb devices"); process.waitFor(); return process.exitValue() == 0; } catch (Throwable t) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/tools/AvrAssemblerCommandsHelper.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2015 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.tools; import com.mucommander.commons.file.util.ResourceLoader; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.Properties; /** * @author Oleg Trifonov * Created on 27/05/16. */ public class AvrAssemblerCommandsHelper { private static final String AVR_COMMANDS_FILE = "/avr/avr_commands.properties"; private static WeakReference propertiesRef; public static String getCommandDescription(String mnemonic) { if (mnemonic == null) { return null; } if (propertiesRef == null || propertiesRef.get() == null) { propertiesRef = new WeakReference<>(loadProperties()); } return propertiesRef.get().getProperty(mnemonic.toUpperCase()); } private static Properties loadProperties() { Properties properties = new Properties(); try (InputStream is = ResourceLoader.getResourceAsStream(AVR_COMMANDS_FILE)) { properties.load(is); } catch (IOException e) { e.printStackTrace(); } return properties; } } ================================================ FILE: src/main/java/com/mucommander/tools/ExternalTool.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.tools; import java.io.File; /** * Base class for external tools. * * @author Oleg Trifonov * Created on 09/09/15. */ public abstract class ExternalTool { /** * Path to application / file */ private String fullPath; /** * * @return true if the tool is available on current OS */ public boolean isActive() { return true; } /** * Try to find tools * @return true if tool was found */ abstract boolean detect(); /** * * @param path full file path * @return true if file exists */ protected boolean checkFileExists(String path) { return new File(path).exists(); } public String getFullPath() { return fullPath; } public void setFullPath(String fullPath) { this.fullPath = fullPath; } } ================================================ FILE: src/main/java/com/mucommander/tools/FileMergeTool.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.tools; import com.mucommander.commons.runtime.OsFamily; /** * @author Oleg Trifonov * Created on 09/09/15. */ public class FileMergeTool extends ExternalTool { @Override public boolean isActive() { return OsFamily.MAC_OS_X.isCurrent(); } @Override boolean detect() { String path = "/Applications/Xcode.app/Contents/Applications/FileMerge.app"; if (checkFileExists(path)) { setFullPath(path); return true; } // opendiff return false; } } ================================================ FILE: src/main/java/com/mucommander/ui/PreloadedJFrame.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package com.mucommander.ui; import lombok.extern.slf4j.Slf4j; import java.awt.LayoutManager; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; import javax.swing.JFrame; import javax.swing.JPanel; /** * A class that extends JFrame to be later on used by core. * Since this bundle is loaded almost as the first (as it has no deps), it can be * used by core having JFrame "preloaded" aka "cached" in JVM. */ @Slf4j public class PreloadedJFrame extends JFrame { private static final int PRELOAD_FRAMES = 2; private static final int PRELOAD_PANELS = 6; private static final Queue preloadedFrame = new ConcurrentLinkedDeque<>(); private static final Queue preloadedPanels = new ConcurrentLinkedDeque<>(); private Object mainFrameObject; public static void init() { new Thread(() -> { log.info("Going to pre-create a couple of JFrames..."); var start = System.currentTimeMillis(); for (int i = 0; i < PRELOAD_FRAMES; i++) { preloadedFrame.add(new PreloadedJFrame()); } log.info("JFrames pre-creation completed in {}ms", (System.currentTimeMillis() - start)); log.info("Going to pre-create a couple of JPanels..."); start = System.currentTimeMillis(); for (int i = 0; i < PRELOAD_PANELS; i++) { preloadedPanels.add(new JPanel()); } log.info("JPanel pre-creation completed in {}ms", (System.currentTimeMillis() - start)); }, "Preload-JFrame").start(); } private void setMainFrameObject(Object mainFrameObj) { this.mainFrameObject = mainFrameObj; } public Object getMainFrameObject() { return mainFrameObject; } public static JFrame getJFrame(Object mainFrame) { var result = preloadedFrame.poll(); if (result == null) { result = new PreloadedJFrame(); } result.setMainFrameObject(mainFrame); return result; } public static JPanel getJPanel(LayoutManager layout) { var result = preloadedPanels.poll(); if (result == null) { result = new JPanel(layout); } else { result.setLayout(layout); // Re-apply the current L&F defaults. Preloaded panels are created eagerly // in a background thread, potentially before the user's chosen L&F is installed. result.updateUI(); } return result; } } ================================================ FILE: src/main/java/com/mucommander/ui/action/AbstractActionDescriptor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.icon.IconManager; import javax.swing.*; import java.awt.event.KeyEvent; /** * AbstractActionDescriptor is an abstract class which implements ActionDescriptor interface. * this class implements the following methods which are common to all action descriptors: * {@link ActionDescriptor#getLabel()} * {@link ActionDescriptor#getIcon()} * {@link ActionDescriptor#getTooltip()} * * @author Arik Hadas */ public abstract class AbstractActionDescriptor implements ActionDescriptor { protected static final int CTRL_OR_META_DOWN_MASK = OsFamily.MAC_OS_X.isCurrent() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; @Override public String getLabel() { String label = getStandardLabel(); return label != null ? label : getLabelKey(); } @Override public ImageIcon getIcon() { return getStandardIcon(getId()); } @Override public String getTooltip() { return getStandardTooltip(getId()); } @Override public String getDescription() { String tooltip = getTooltip(); return tooltip == null ? getLabel() : tooltip; } /** * Returns the dictionary key for action's label, using the following standard naming convention: *

         *      action_id.label
         * 
    * where action_id is a String identification of the action, as returned by getId(). * * @return the standard dictionary key for the action's label */ @Override public String getLabelKey() { return getId()+".label"; } /** * Implements {@link ActionDescriptor#isParameterized()} by returning false, which suits most actions. * This method can be overridden to change this behavior. * * @return false */ @Override public boolean isParameterized() { return false; } /** * Queries {@link Translator} for a label corresponding to the action using the standard naming convention. * Returns the label or null if no corresponding entry was found in the dictionary. * * @return the standard label corresponding to the MuAction, null if none was found */ private String getStandardLabel() { String labelKey = getLabelKey(); if (!Translator.hasValue(labelKey, true)) return null; return Translator.get(labelKey); } /** * Queries {@link IconManager} for an image icon corresponding to the specified action using standard icon path * conventions. Returns the image icon, null if none was found. * * @param actionId a String identification of MuAction * @return the standard icon image corresponding to the specified MuAction, null if none was found */ protected static ImageIcon getStandardIcon(String actionId) { // Look for an icon image file with the /action/.png path and use it if it exists String iconPath = getStandardIconPath(actionId); return ResourceLoader.getResourceAsURL(iconPath) == null ? null : IconManager.getIcon(iconPath); } /** * Returns the standard path to the icon image for the specified {@link TcAction} id. * The returned path is relative to the application's JAR file. * * @param actionId a String identification of MuAction * @return the standard path to the icon image corresponding to the specified MuAction */ private static String getStandardIconPath(String actionId) { return IconManager.IconSet.ACTION.getFolder() + actionId + ".png"; } /** * Queries {@link Translator} for a tooltip corresponding to the specified action using standard naming conventions. * Returns the tooltip or null if no corresponding entry was found in the dictionary. * * @param actionId a String identification of MuAction * @return the standard tooltip corresponding to the specified MuAction, null if none was found */ private static String getStandardTooltip(String actionId) { String tooltipKey = getStandardTooltipKey(actionId); if (!Translator.hasValue(tooltipKey, true)) return null; return Translator.get(tooltipKey); } /** * Returns the dictionary key for the specified action's tooltip, using the following standard naming convention: *
         *      action_id.tooltip
         * 
    * where action_id is a String identification of the action, as returned by getId(). * * @param actionId a String identification of MuAction * @return the standard dictionary key for the specified action's tooltip */ private static String getStandardTooltipKey(String actionId) { return actionId+".tooltip"; } } ================================================ FILE: src/main/java/com/mucommander/ui/action/AcceleratorMap.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import javax.swing.*; import com.mucommander.commons.util.Pair; import java.util.HashMap; /** * Data structure that maps KeyStroke (accelerator) to MuAction id. * * @author Arik Hadas */ public class AcceleratorMap { // Accelerator types public final static int PRIMARY_ACCELERATOR = 1; public final static int ALTERNATIVE_ACCELERATOR = 2; // Maps KeyStrokes to MuAction id and accelerator type (PRIMARY_ACCELERATOR/ALTERNATIVE_ACCELERATOR) pair. private static final HashMap> map = new HashMap<>(); /** * Register KeyStroke to MuAction as primary accelerator. * * @param ks - accelerator * @param actionId - id of MuAction to which the given accelerator would be registered. */ public void putAccelerator(KeyStroke ks, String actionId) { put(ks, actionId, PRIMARY_ACCELERATOR); } /** * Register KeyStroke to MuAction as alternative accelerator. * * @param ks - alternative accelerator. * @param actionId - id of MuAction to which the given accelerator would be registered. */ public void putAlternativeAccelerator(KeyStroke ks, String actionId) { put(ks, actionId, ALTERNATIVE_ACCELERATOR); } /** * Return id of MuAction that accelerator is registered to. * * @param ks - accelerator. * @return id of MuAction that the given accelerator is registered to. */ public String getActionId(KeyStroke ks) { Pair idAndType = getActionIdAndAcceleratorTypeOfKeyStroke(ks); return idAndType != null ? idAndType.first : null; } /** * Return accelerator type. * * @param ks - accelerator. * @return the type of the given accelerator (primary(1)/alternative(2)/not registered(0)). */ public int getAcceleratorType(KeyStroke ks) { Pair idAndType = getActionIdAndAcceleratorTypeOfKeyStroke(ks); return idAndType != null ? idAndType.second : 0; } /** * Remove accelerator. * * @param ks - accelerator. */ public void remove(KeyStroke ks) { map.remove(ks); } /** * Remove all accelerators. */ public void clear() { map.clear(); } private void put(KeyStroke ks, String actionId, int acceleratorType) { if (ks != null) map.put(ks, new Pair<>(actionId, acceleratorType)); } private Pair getActionIdAndAcceleratorTypeOfKeyStroke(KeyStroke ks) { return map.get(ks); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionCategory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.utils.text.Translator; /** * ActionCategory represent category that can be associated with MuAction action. * * @author Arik Hadas */ public enum ActionCategory { ALL("all") { @Override public boolean contains(String actionId) { return true; } }, NAVIGATION("navigation"), SELECTION("selection"), VIEW("view"), FILES("file_operations"), WINDOW("windows"), TAB("tabs"), MISC("misc"), COMMANDS("commands"); /** The category's label key in the dictionary file */ private final String descriptionKey; ActionCategory(String descriptionKey) { this.descriptionKey = "action_categories." + descriptionKey; } public String getDescriptionKey() { return descriptionKey; } public String getDescription() { return Translator.get(descriptionKey); } public boolean contains(String actionId) { ActionCategory actionCategory = ActionProperties.getActionCategory(actionId); return actionCategory != null && descriptionKey.equals(actionCategory.getDescriptionKey()); } @Override public String toString() { String description = getDescription(); if (description != null) { return description; } return getDescriptionKey(); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionDescriptor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import javax.swing.*; /** * Each MuAction is registered with an object of ActionDescriptor type * that provides its properties. ActionDescriptor is an interface that * defines those action's properties. * * @author Arik Hadas */ public interface ActionDescriptor extends ActionFactory { String getId(); String getDescription(); ActionCategory getCategory(); String getLabel(); String getLabelKey(); KeyStroke getDefaultKeyStroke(); KeyStroke getDefaultAltKeyStroke(); ImageIcon getIcon(); String getTooltip(); /** * Returns true if the action requires parameters at creation time. * * @return true if the action requires parameters at creation time. */ boolean isParameterized(); // MuAction createAction(MainFrame mainFrame, Map properties); } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Each MuAction's factory should implement this interface. * * @author Arik Hadas */ public interface ActionFactory { /** * This is an initiation method that returns an instance of MuAction subclass. * * @param mainFrame - MainFrame. * @param properties - a hashtable of arguments for the action. * @return an instance of MuAction subclass. */ TcAction createAction(MainFrame mainFrame, Map properties); } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionKeymap.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; /** * This class manages keyboard associations with {@link TcAction} ids. * * @author Maxence Bernard, Arik Hadas */ public class ActionKeymap { /** Maps action id onto Keystroke instances */ private static final Map customPrimaryActionKeymap = new HashMap<>(); /** Maps action id instances onto Keystroke instances */ private static final Map customAlternateActionKeymap = new HashMap<>(); /** Maps Keystroke instances onto action id */ private static final AcceleratorMap acceleratorMap = new AcceleratorMap(); /*================= * Public Methods * =================*/ /** * Register all action shortcuts to the given MainFrame's file tables. * * @param mainFrame - MainFrame instance to which all action shortcuts would be registered. */ public static void registerActions(MainFrame mainFrame) { Iterator actionIds = ActionManager.getActionIds(); while (actionIds.hasNext()) { String actionId = actionIds.next(); ActionDescriptor actionDescriptor = ActionProperties.getActionDescriptor(actionId); // Instantiate the action only if it is not parameterized: parameterized actions should only be instantiated // when they are needed and with the required parameters. if (!actionDescriptor.isParameterized()) { registerAction(mainFrame, ActionManager.getActionInstance(actionId, mainFrame)); } } } /** * Register MuAction to JComponent with the given condition. * * @param action - MuAction instance. * @param comp - JComponent to which the action would be registered * @param condition - condition in which the action could be invoked. */ public static void registerActionAccelerators(TcAction action, JComponent comp, int condition) { registerActionAccelerator(action, action.getAccelerator(), comp, condition); registerActionAccelerator(action, action.getAlternateAccelerator(), comp, condition); } /** * Register new accelerators to the given action. * * Note: if accelerator is null, it would be replaced by alternateAccelerator. * * @param actionId - id of the MuAction. * @param accelerator - KeyStroke that would be primary accelerator of the given action. * @param alternateAccelerator - KeyStroke that would be alternative accelerator of the given action. */ public static void changeActionAccelerators(String actionId, KeyStroke accelerator, KeyStroke alternateAccelerator) { // Check whether the given actions are already assigned to the given action if (equals(accelerator, ActionKeymap.getAccelerator(actionId)) && equals(alternateAccelerator, ActionKeymap.getAlternateAccelerator(actionId))) return; // If primary accelerator is already registered to other MuAction, unregister it. String previousActionForAccelerator = getRegisteredActionIdForKeystroke(accelerator); if (previousActionForAccelerator != null && !previousActionForAccelerator.equals(actionId)) unregisterAcceleratorFromAction(previousActionForAccelerator, accelerator); // If alternative accelerator is already registered to other MuAction, unregister it. String previousActionForAlternativeAccelerator = getRegisteredActionIdForKeystroke(alternateAccelerator); if (previousActionForAlternativeAccelerator != null && !previousActionForAlternativeAccelerator.equals(actionId)) unregisterAcceleratorFromAction(previousActionForAlternativeAccelerator, alternateAccelerator); // Remove action's previous accelerators (primary and alternate) acceleratorMap.remove(customPrimaryActionKeymap.remove(actionId)); acceleratorMap.remove(customAlternateActionKeymap.remove(actionId)); // Register new accelerators registerActionAccelerators(actionId, accelerator, alternateAccelerator); } private static void unregisterAcceleratorFromAction(String actionId, KeyStroke accelerator) { switch (getAcceleratorType(accelerator)) { case AcceleratorMap.PRIMARY_ACCELERATOR: registerActionAccelerators(actionId, null, getAlternateAccelerator(actionId)); break; case AcceleratorMap.ALTERNATIVE_ACCELERATOR: registerActionAccelerators(actionId, getAccelerator(actionId), null); break; } } /** * Register all primary and alternative accelerator given. * * @param primary - HashMap that maps action id to primary accelerator. * @param alternate - HashMap that maps action id to alternative accelerator. */ public static void registerActions(Map primary, Map alternate) { for (String actionId : primary.keySet()) { // Add the action/keystroke mapping ActionKeymap.registerActionAccelerators( actionId, primary.get(actionId), alternate.get(actionId)); } } /** * Check whether an accelerator is registered to MuAction. * * @param ks - accelerator. * @return true if the given accelerator is registered to whatever MuAction, false otherwise. */ public static boolean isKeyStrokeRegistered(KeyStroke ks) { return getRegisteredActionIdForKeystroke(ks)!=null; } /** * Check whether the action has accelerator assigned to it. * * @param actionId - id of MuAction. * @return true if there is a shortcut which is assigned to the action, false otherwise. */ public static boolean doesActionHaveShortcut(String actionId) { return getAccelerator(actionId) != null; } /** * Restore the default accelerators assignments (remove custom assignments). */ public static void restoreDefault() { customPrimaryActionKeymap.clear(); customAlternateActionKeymap.clear(); acceleratorMap.clear(); } /////////////////// ///// getters ///// /////////////////// /** * Return primary accelerator of MuAction. * * @param actionId - id of MuAction. * @return primary accelerator of the given MuAction. */ public static KeyStroke getAccelerator(String actionId) { if (customPrimaryActionKeymap.containsKey(actionId)) return customPrimaryActionKeymap.get(actionId); return ActionProperties.getDefaultAccelerator(actionId); } /** * Return alternative accelerator of MuAction. * * @param actionId - id of MuAction. * @return alternative accelerator of the given MuAction. */ public static KeyStroke getAlternateAccelerator(String actionId) { if (customAlternateActionKeymap.containsKey(actionId)) return customAlternateActionKeymap.get(actionId); return ActionProperties.getDefaultAlternativeAccelerator(actionId); } /** * Return the id of MuAction to which accelerator is registered. * * @param ks - accelerator. * @return id of MuAction to which the given accelerator is registered, null if the accelerator is not registered. */ public static String getRegisteredActionIdForKeystroke(KeyStroke ks) { String actionId = acceleratorMap.getActionId(ks); return actionId != null ? actionId : ActionProperties.getDefaultActionForKeyStroke(ks); } /** * Return accelerator type: primary\alternative\not registered. * * @param ks - accelerator. * @return accelerator type. */ private static int getAcceleratorType(KeyStroke ks) { int type = acceleratorMap.getAcceleratorType(ks); return type != 0 ? type : ActionProperties.getDefaultAcceleratorType(ks); } /** * Return ids of actions that their registered accelerators are different from their default accelerators. * * @return Iterator of actions that their accelerators were customized. */ public static Iterator getCustomizedActions() { HashSet modifiedActions = new HashSet<>(); modifiedActions.addAll(customPrimaryActionKeymap.keySet()); modifiedActions.addAll(customAlternateActionKeymap.keySet()); return modifiedActions.iterator(); } /*================== * Private Methods * ==================*/ /** * Register MuAction instance to MainFrame instance. */ private static void registerAction(MainFrame mainFrame, TcAction action) { registerActionAccelerators(action, mainFrame.getLeftPanel().getFileTable(), JComponent.WHEN_FOCUSED); registerActionAccelerators(action, mainFrame.getRightPanel().getFileTable(), JComponent.WHEN_FOCUSED); } /** * Register accelerator of MuAction to JComponent with a condition that states when the action can be invoked. */ private static void registerActionAccelerator(TcAction action, KeyStroke accelerator, JComponent comp, int condition) { if (accelerator != null) { InputMap inputMap = comp.getInputMap(condition); ActionMap actionMap = comp.getActionMap(); String actionId = action.getId();//action.getDescriptor().getId(); inputMap.put(accelerator, actionId); actionMap.put(actionId, action); } } /** * Unregister MuAction instance from MainFrame instance. */ private static void unregisterAction(MainFrame mainFrame, TcAction action) { unregisterActionAccelerators(action, mainFrame.getLeftPanel().getFileTable(), JComponent.WHEN_FOCUSED); unregisterActionAccelerators(action, mainFrame.getRightPanel().getFileTable(), JComponent.WHEN_FOCUSED); } /** * Unregister MuAction from JComponent. */ private static void unregisterActionAccelerators(TcAction action, JComponent comp, int condition) { unregisterActionAccelerator(action, action.getAccelerator(), comp, condition); unregisterActionAccelerator(action, action.getAlternateAccelerator(), comp, condition); } /** * Unregister accelerator of MuAction from JComponent. */ private static void unregisterActionAccelerator(TcAction action, KeyStroke accelerator, JComponent comp, int condition) { if (accelerator != null) { InputMap inputMap = comp.getInputMap(condition); ActionMap actionMap = comp.getActionMap(); inputMap.remove(accelerator); String actionId = action.getId(); // action.getDescriptor().getId() actionMap.remove(actionId); } } /** * Register primary and alternative accelerators to MuAction. */ private static void registerActionAccelerators(String actionId, KeyStroke accelerator, KeyStroke alternateAccelerator) { // If accelerator is null, replace it with alternateAccelerator if (accelerator == null) { accelerator = alternateAccelerator; alternateAccelerator = null; } // New accelerators are identical to the default action's accelerators if (equals(accelerator, ActionProperties.getDefaultAccelerator(actionId)) && equals(alternateAccelerator, ActionProperties.getDefaultAlternativeAccelerator(actionId))) { // Remove all action's accelerators customization customPrimaryActionKeymap.remove(actionId); customAlternateActionKeymap.remove(actionId); acceleratorMap.remove(accelerator); acceleratorMap.remove(alternateAccelerator); } // New accelerators are different from the default accelerators else { customPrimaryActionKeymap.put(actionId, accelerator); acceleratorMap.putAccelerator(accelerator, actionId); customAlternateActionKeymap.put(actionId, alternateAccelerator); acceleratorMap.putAlternativeAccelerator(alternateAccelerator, actionId); } // Update each MainFrame's action instance and input map for(TcAction action : ActionManager.getActionInstances(actionId)) { MainFrame mainFrame = action.getMainFrame(); // Remove action from MainFrame's action and input maps unregisterAction(mainFrame, action); // Change action's accelerators action.setAccelerator(accelerator); action.setAlternateAccelerator(alternateAccelerator); // Add updated action to MainFrame's action and input maps registerAction(mainFrame, action); } } /** * Return true if the two KeyStrokes are equal, false otherwise. */ private static boolean equals(Object first, Object second) { if (first == null) { return second == null; } return first.equals(second); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionKeymapIO.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.helpers.DefaultHandler; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; /** * This class contains the common things to the actions reading and writing. * * @author Maxence Bernard, Arik Hadas */ public abstract class ActionKeymapIO extends DefaultHandler { private static Logger logger; /* Variables used for XML parsing */ protected final static String ROOT_ELEMENT = "keymap"; final static String ACTION_ELEMENT = "action"; final static String CLASS_ATTRIBUTE = "class"; final static String ID_ATTRIBUTE = "id"; final static String PRIMARY_KEYSTROKE_ATTRIBUTE = "keystroke"; final static String ALTERNATE_KEYSTROKE_ATTRIBUTE = "alt_keystroke"; /** Attribute containing the last muCommander version that was used to create the file */ protected static final String VERSION_ATTRIBUTE = "version"; /** Actions file used when calling {@link #loadActionKeymap()} */ private static AbstractFile actionsFile; /** Default actions filename */ private final static String DEFAULT_ACTIONS_FILE_NAME = "action_keymap.xml"; /** Path to the actions resource file within the application JAR file */ public final static String ACTION_KEYMAP_RESOURCE_PATH = "/" + DEFAULT_ACTIONS_FILE_NAME; /** Whether the actions have been modified since the last time they were saved */ protected static boolean wereActionsModified; private static final ActionKeymapWriter writer = new ActionKeymapWriter(); /** * Sets the path to the user actions file to be loaded when calling {@link #loadActionKeymap()}. * By default, this file is {@link #DEFAULT_ACTIONS_FILE_NAME} within the preferences folder. *

    * This is a convenience method and is strictly equivalent to calling setActionsFile(FileFactory.getFile(file)). * * @param path path to the actions file * @throws FileNotFoundException if file is not accessible. */ public static void setActionsFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setActionsFile(new File(path)); } else { setActionsFile(file); } } /** * Sets the path to the user actions file to be loaded when calling {@link #loadActionKeymap()}. * By default, this file is {@link #DEFAULT_ACTIONS_FILE_NAME} within the preferences folder. *

    * This is a convenience method and is strictly equivalent to calling setActionsFile(FileFactory.getFile(file.getAbsolutePath())). * * @param file path to the actions file * @throws FileNotFoundException if file is not accessible. */ private static void setActionsFile(File file) throws FileNotFoundException { setActionsFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the user actions file to be loaded when calling {@link #loadActionKeymap()}. * By default, this file is {@link #DEFAULT_ACTIONS_FILE_NAME} within the preferences folder. * @param file path to the actions file * @throws FileNotFoundException if file is not accessible. */ private static void setActionsFile(AbstractFile file) throws FileNotFoundException { if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } actionsFile = file; } /** * Returns the actions file. * @return the actions file. * @throws IOException if an error occurred while locating the default actions file. */ protected static AbstractFile getActionsFile() throws IOException { if (actionsFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_ACTIONS_FILE_NAME); } return actionsFile; } /** * Mark that actions were modified and therefore should be saved. */ public static void setModified() { wereActionsModified = true; } /** * Writes the current action keymaps to the user's actions file. * @throws IOException */ public static void saveActionKeymap() throws IOException { if (wereActionsModified) writer.write(); else getLogger().debug("Action keymap not modified, not saving"); } protected static void createEmptyFile() { writer.create(); } /** * Loads the action files: loads the one contained in the JAR file first, and then the user's one. * This means any new action in the JAR action keymap (when a new version is released) will have the default * keyboard mapping, but the keyboard mappings customized by the user in the user's action keymap will override * the ones from the JAR action keymap. * *

    This method must be called before requesting and registering any action. */ public static void loadActionKeymap() throws Exception { // Load user's file if exist AbstractFile actionKeymapFile = getActionsFile(); if (actionKeymapFile != null && actionKeymapFile.exists()) { ActionKeymapReader reader = new ActionKeymapReader(actionKeymapFile); ActionKeymap.registerActions(reader.getPrimaryActionsKeymap(), reader.getAlternateActionsKeymap()); } else { createEmptyFile(); getLogger().debug(DEFAULT_ACTIONS_FILE_NAME + " was not found, created empty file"); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ActionKeymapIO.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionKeymapReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import javax.swing.KeyStroke; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.AbstractFile; import com.mucommander.io.backup.BackupInputStream; /** * This class is responsible for reading the actions. * it read and parse the two action files - the default one * (placed in the jar file) and the user's one. * * @author Maxence Bernard, Arik Hadas */ class ActionKeymapReader extends ActionKeymapIO { private static final Logger LOGGER = LoggerFactory.getLogger(ActionKeymapReader.class); /** Maps action Class onto Keystroke instances*/ private Map primaryActionsReadKeymap; /** Maps action Class instances onto Keystroke instances*/ private Map alternateActionsReadKeymap; /** Parsed file */ private final AbstractFile file; /** * Loads the action file: loads the one contained in the JAR file first, and then the user's one. * This means any new action in the JAR action keymap (when a new version gets released) will have the default * keyboard mapping, but the keyboard mappings customized by the user in the user's action keymap will override * the ones from the JAR action keymap. * * Starts parsing the XML actions file. * * @throws ParserConfigurationException * @throws IOException * @throws SAXException */ ActionKeymapReader(AbstractFile file) throws SAXException, IOException, ParserConfigurationException { this.file = file; try (InputStream in = new BackupInputStream(file)) { SAXParserFactory.newInstance().newSAXParser().parse(in, this); } } /** * Parses the keystrokes defined in the given attribute map (if any) and associates them with the given action id. * The keystroke will not be associated in any of the following cases: *

      *
    • the keystrokes attributes do not contain any value.
    • *
    • the keystrokes attributes have values that do not represent a valid KeyStroke (syntax error).
    • *
    * If a given keystroke is already associated to an action, the existing association is replaced. * If there is a valid alternative keystroke defined but there is no valid primary keystroke defined, the primary keystroke * is replaced by the alternative keystroke. * * @param actionId the action id to associate the keystroke with * @param attributes the attributes map that holds the value */ private void processKeystrokeAttribute(String actionId, Attributes attributes) { // Parse the primary keystroke and retrieve the corresponding KeyStroke instance String keyStrokeString = attributes.getValue(PRIMARY_KEYSTROKE_ATTRIBUTE); KeyStroke primaryKeyStroke = null; if (keyStrokeString != null) { primaryKeyStroke = KeyStroke.getKeyStroke(keyStrokeString); if (primaryKeyStroke == null) { LOGGER.info("Action keymap file contains a keystroke which could not be resolved: " + keyStrokeString); } else { String prevAssignedActionId = ActionKeymap.getRegisteredActionIdForKeystroke(primaryKeyStroke); if (prevAssignedActionId != null && !prevAssignedActionId.equals(actionId)) { LOGGER.debug("Canceling previous association of keystroke " + keyStrokeString + ", reassign it to action: " + actionId); } } } // Parse the alternate keystroke and retrieve the corresponding KeyStroke instance keyStrokeString = attributes.getValue(ALTERNATE_KEYSTROKE_ATTRIBUTE); KeyStroke alternateKeyStroke = null; if (keyStrokeString != null) { alternateKeyStroke = KeyStroke.getKeyStroke(keyStrokeString); if (alternateKeyStroke == null) { LOGGER.info("Action keymap file contains a keystroke which could not be resolved: " + keyStrokeString); } else { String prevAssignedActionId = ActionKeymap.getRegisteredActionIdForKeystroke(alternateKeyStroke); if (prevAssignedActionId != null && !prevAssignedActionId.equals(actionId)) { LOGGER.debug("Canceling previous association of keystroke " + keyStrokeString + ", reassign it to action: " + actionId); } } } // If there is no primary shortcut defined for the action but there is an alternative shortcut defined, // turn the alternative shortcut to the action's primary shortcut if (primaryKeyStroke == null) { LOGGER.debug("Action \"" + actionId +"\" has an alternative shortcut with no primary shortcut, so the alternative shortcut become primary"); primaryActionsReadKeymap.put(actionId, alternateKeyStroke); alternateActionsReadKeymap.put(actionId, null); // Mark that the actions keymap file should be updated setModified(); } else { primaryActionsReadKeymap.put(actionId, primaryKeyStroke); alternateActionsReadKeymap.put(actionId, alternateKeyStroke); } } /////////////////// ///// getters ///// /////////////////// Map getPrimaryActionsKeymap() { return primaryActionsReadKeymap; } Map getAlternateActionsKeymap() { return alternateActionsReadKeymap; } /////////////////////////////////// // ContentHandler implementation // /////////////////////////////////// @Override public void startDocument() { LOGGER.trace(file.getAbsolutePath()+" parsing started"); primaryActionsReadKeymap = new HashMap<>(); alternateActionsReadKeymap = new HashMap<>(); } @Override public void endDocument() { LOGGER.trace(file.getAbsolutePath()+" parsing finished"); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if (qName.equals(ACTION_ELEMENT)) { // Retrieve the action id String actionId = attributes.getValue(ID_ATTRIBUTE); // if id attribute not exits, read class attribute if (actionId == null) { String actionClassPath = attributes.getValue(CLASS_ATTRIBUTE); if (actionClassPath == null) { LOGGER.warn("Error in action keymap file: no 'class' or 'id' attribute specified in 'action' element"); return; } // extrapolate the action id from its class path actionId = ActionManager.extrapolateId(actionClassPath); } if (!ActionManager.isActionExist(actionId)) { LOGGER.warn("Error in action keymap file: could not resolve action " + actionId); return; } // Load the action's accelerators (if any) processKeystrokeAttribute(actionId, attributes); } else if (qName.equals(ROOT_ELEMENT)) { // Note: early 0.8 beta3 nightly builds did not have version attribute, so the attribute may be null String fileVersion = attributes.getValue(VERSION_ATTRIBUTE); // if the file's version is not up-to-date, update the file to the current version at quitting. if (!RuntimeConstants.VERSION.equals(fileVersion)) { setModified(); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionKeymapWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import java.io.IOException; import java.io.OutputStream; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import javax.swing.KeyStroke; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.RuntimeConstants; import com.mucommander.io.backup.BackupOutputStream; import com.mucommander.ui.text.KeyStrokeUtils; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; /** * This class is responsible for writing the actions. * When actions are modified, they are written to the user's actions file. * * @author Maxence Bernard, Arik Hadas */ class ActionKeymapWriter extends ActionKeymapIO { private static final Logger LOGGER = LoggerFactory.getLogger(ActionKeymapWriter.class); ActionKeymapWriter() {} public void create() { try (BackupOutputStream bos = new BackupOutputStream(getActionsFile())) { new Writer(bos).writeKeyMap(null); } catch (Exception e) { LOGGER.debug("Caught exception", e); } } void write() { Map combinedMapping = new Hashtable<>(); Iterator modifiedActionsIterator = ActionKeymap.getCustomizedActions(); while(modifiedActionsIterator.hasNext()) { String actionId = modifiedActionsIterator.next(); KeyStroke[] keyStrokes = new KeyStroke[2]; keyStrokes[0] = ActionKeymap.getAccelerator(actionId); keyStrokes[1] = ActionKeymap.getAlternateAccelerator(actionId); combinedMapping.put(actionId, keyStrokes); } try (BackupOutputStream bos = new BackupOutputStream(getActionsFile())) { new Writer(bos).writeKeyMap(combinedMapping); wereActionsModified = false; } catch (Exception e) { LOGGER.debug("Caught exception", e); } } private static class Writer { private final XmlWriter writer; private Writer(OutputStream stream) throws IOException { this.writer = new XmlWriter(stream); } private void writeKeyMap(Map actionMap) throws IOException { try { writer.writeCommentLine("See http://trac.mucommander.com/wiki/ActionKeyMap for information on how to customize this file"); XmlAttributes rootElementAttributes = new XmlAttributes(); rootElementAttributes.add(VERSION_ATTRIBUTE, RuntimeConstants.VERSION); writer.startElement(ROOT_ELEMENT, rootElementAttributes, true); if (actionMap != null) { for (String actionId: actionMap.keySet()) { addMapping(actionId, actionMap.get(actionId)); } } } finally { writer.endElement(ROOT_ELEMENT); } } private void addMapping(String actionId, KeyStroke[] keyStrokes) throws IOException { XmlAttributes attributes = new XmlAttributes(); attributes.add(ID_ATTRIBUTE, actionId); LOGGER.trace(" Writing mapping of " + actionId + " to " + keyStrokes[0] + " and " + keyStrokes[1]); if (keyStrokes[0] != null) { attributes.add(PRIMARY_KEYSTROKE_ATTRIBUTE, KeyStrokeUtils.getKeyStrokeRepresentation(keyStrokes[0])); } if (keyStrokes[1] != null) { attributes.add(ALTERNATE_KEYSTROKE_ATTRIBUTE, KeyStrokeUtils.getKeyStrokeRepresentation(keyStrokes[1])); } writer.writeStandAloneElement(ACTION_ELEMENT, attributes); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.action.impl.*; import com.mucommander.ui.main.MainFrame; import javax.swing.ImageIcon; import javax.swing.KeyStroke; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * ActionManager provides methods to retrieve {@link TcAction} instances and invoke them. It keeps track of all the * action instances it has created and allows them to be reused within a {@link MainFrame}. * *

    MuAction subclasses should not be instantiated directly, getActionInstance * methods should be used instead. Using ActionManager to retrieve a MuAction ensures that only one instance * exists for a given {@link MainFrame}. This is particularly important because actions are stateful and can be used * in several components of a MainFrame at the same time; if an action's state changes, the change must be reflected * everywhere the action is used. It is also important for performance reasons: sharing one action throughout a * {@link com.mucommander.ui.main.MainFrame} saves some memory and also CPU cycles as some actions listen to particular events to change * their state accordingly. * * @see TcAction * @see ActionParameters * @see ActionKeymap * @author Maxence Bernard, Arik Hadas */ public class ActionManager { //private static final Logger LOGGER = LoggerFactory.getLogger(ActionManager.class); /* MuAction id -> factory map */ //static final Map actionFactories = new HashMap<>(); /** MainFrame -> MuAction map */ private static final WeakHashMap> mainFrameActionsMap = new WeakHashMap<>(); /** Pattern to resolve the action ID from action class path */ private final static Pattern PATTERN = Pattern.compile(".*\\.(.*)?Action"); public static void registerActions() { registerAction(new AddBookmarkAction.Descriptor()); registerAction(new AddTabAction.Descriptor()); registerAction(new BatchRenameAction.Descriptor()); registerAction(new BringAllToFrontAction.Descriptor()); registerAction(new CalculateChecksumAction.Descriptor()); registerAction(new ChangeDateAction.Descriptor()); registerAction(new ChangeReplicationAction.Descriptor()); registerAction(new ChangeLocationAction.Descriptor()); registerAction(new ChangePermissionsAction.Descriptor()); registerAction(new CheckForUpdatesAction.Descriptor()); registerAction(new CloneTabToOtherPanelAction.Descriptor()); registerAction(new CloseDuplicateTabsAction.Descriptor()); registerAction(new CloseOtherTabsAction.Descriptor()); registerAction(new CloseWindowAction.Descriptor()); registerAction(new CloseTabAction.Descriptor()); // registerAction(new CommandAction.Descriptor()); registerAction(new CompareFoldersAction.Descriptor()); registerAction(new CompareFolderFilesAction.Descriptor()); registerAction(new ConnectToServerAction.Descriptor()); registerAction(new CopyAction.Descriptor()); registerAction(new CopyFileBaseNamesAction.Descriptor()); registerAction(new CopyFileNamesAction.Descriptor()); registerAction(new CopyFilePathsAction.Descriptor()); registerAction(new CopyFilesToClipboardAction.Descriptor()); registerAction(new FocusPreviousAction.Descriptor()); registerAction(new FocusNextAction.Descriptor()); registerAction(new DeleteAction.Descriptor()); registerAction(new DonateAction.Descriptor()); registerAction(new DuplicateTabAction.Descriptor()); registerAction(new EditAction.Descriptor()); registerAction(new EditBookmarksAction.Descriptor()); registerAction(new EditCredentialsAction.Descriptor()); registerAction(new EmailAction.Descriptor()); registerAction(new EmptyTrashAction.Descriptor()); registerAction(new ExploreBookmarksAction.Descriptor()); // registerAction(new GarbageCollectAction.Descriptor()); registerAction(new GoBackAction.Descriptor()); registerAction(new GoForwardAction.Descriptor()); registerAction(new GoToDocumentationAction.Descriptor()); registerAction(new GoToForumsAction.Descriptor()); registerAction(new GoToHomeAction.Descriptor()); registerAction(new GoToParentAction.Descriptor()); registerAction(new GoToParentInBothPanelsAction.Descriptor()); registerAction(new GoToParentInOtherPanelAction.Descriptor()); registerAction(new GoToRootAction.Descriptor()); registerAction(new GoToWebsiteAction.Descriptor()); registerAction(new InternalEditAction.Descriptor()); registerAction(new InternalViewAction.Descriptor()); registerAction(new InvertSelectionAction.Descriptor()); registerAction(new LocalCopyAction.Descriptor()); registerAction(new MarkAllAction.Descriptor()); registerAction(new MarkExtensionAction.Descriptor()); registerAction(new MarkGroupAction.Descriptor()); registerAction(new MarkNextBlockAction.Descriptor()); registerAction(new MarkNextPageAction.Descriptor()); registerAction(new MarkNextRowAction.Descriptor()); registerAction(new MarkPreviousBlockAction.Descriptor()); registerAction(new MarkPreviousPageAction.Descriptor()); registerAction(new MarkPreviousRowAction.Descriptor()); registerAction(new MarkSelectedFileAction.Descriptor()); registerAction(new MarkToFirstRowAction.Descriptor()); registerAction(new MarkToLastRowAction.Descriptor()); registerAction(new MaximizeWindowAction.Descriptor()); registerAction(new CombineFilesAction.Descriptor()); registerAction(new MinimizeWindowAction.Descriptor()); registerAction(new MkdirAction.Descriptor()); registerAction(new MkfileAction.Descriptor()); registerAction(new MoveAction.Descriptor()); registerAction(new MoveTabToOtherPanelAction.Descriptor()); registerAction(new NewWindowAction.Descriptor()); registerAction(new NextTabAction.Descriptor()); registerAction(new OpenAction.Descriptor()); registerAction(new OpenAsAction.Descriptor()); registerAction(new OpenInBothPanelsAction.Descriptor()); registerAction(new OpenInNewTabAction.Descriptor()); registerAction(new OpenInOtherPanelAction.Descriptor()); registerAction(new OpenLeftInRightPanelAction.Descriptor()); registerAction(new OpenRightInLeftPanelAction.Descriptor()); // registerAction(new OpenLocationAction.Descriptor()); registerAction(new OpenNativelyAction.Descriptor()); registerAction(new OpenTrashAction.Descriptor()); registerAction(new OpenURLInBrowserAction.Descriptor()); registerAction(new PackAction.Descriptor()); registerAction(new PasteClipboardFilesAction.Descriptor()); registerAction(new PermanentDeleteAction.Descriptor()); registerAction(new PopupLeftDriveButtonAction.Descriptor()); registerAction(new PopupRightDriveButtonAction.Descriptor()); registerAction(new PreviousTabAction.Descriptor()); registerAction(new QuitAction.Descriptor()); registerAction(new RecallNextWindowAction.Descriptor()); registerAction(new RecallPreviousWindowAction.Descriptor()); registerAction(new RecallWindow10Action.Descriptor()); registerAction(new RecallWindow1Action.Descriptor()); registerAction(new RecallWindow2Action.Descriptor()); registerAction(new RecallWindow3Action.Descriptor()); registerAction(new RecallWindow4Action.Descriptor()); registerAction(new RecallWindow5Action.Descriptor()); registerAction(new RecallWindow6Action.Descriptor()); registerAction(new RecallWindow7Action.Descriptor()); registerAction(new RecallWindow8Action.Descriptor()); registerAction(new RecallWindow9Action.Descriptor()); registerAction(new RecallWindowAction.Descriptor()); registerAction(new RefreshAction.Descriptor()); registerAction(new RenameAction.Descriptor()); registerAction(new ReportBugAction.Descriptor()); registerAction(new RevealInDesktopAction.Descriptor()); registerAction(new ReverseSortOrderAction.Descriptor()); registerAction(new RunCommandAction.Descriptor()); registerAction(new SelectPreviousBlockAction.Descriptor()); registerAction(new SelectPreviousPageAction.Descriptor()); registerAction(new SelectPreviousRowAction.Descriptor()); registerAction(new SelectNextBlockAction.Descriptor()); registerAction(new SelectNextPageAction.Descriptor()); registerAction(new SelectNextRowAction.Descriptor()); registerAction(new SelectFirstRowAction.Descriptor()); registerAction(new SelectLastRowAction.Descriptor()); registerAction(new LeftArrowAction.Descriptor()); registerAction(new RightArrowAction.Descriptor()); registerAction(new SetSameFolderAction.Descriptor()); registerAction(new SetTabTitleAction.Descriptor()); registerAction(new ShowAboutAction.Descriptor()); registerAction(new ShowBookmarksQLAction.Descriptor()); registerAction(new CustomizeCommandBarAction.Descriptor()); registerAction(new ShowDebugConsoleAction.Descriptor()); registerAction(new ShowFilePropertiesAction.Descriptor()); registerAction(new ShowFilePopupMenuAction.Descriptor()); registerAction(new ShowKeyboardShortcutsAction.Descriptor()); registerAction(new ShowParentFoldersQLAction.Descriptor()); registerAction(new ShowPreferencesAction.Descriptor()); registerAction(new ShowRecentExecutedFilesQLAction.Descriptor()); registerAction(new ShowRecentLocationsQLAction.Descriptor()); registerAction(new ShowRootFoldersQLAction.Descriptor()); registerAction(new ShowRecentViewedFilesQLAction.Descriptor()); registerAction(new ShowRecentEditedFilesQLAction.Descriptor()); registerAction(new ShowEditorBookmarksQLAction.Descriptor()); registerAction(new ShowServerConnectionsAction.Descriptor()); registerAction(new ShowTabsQLAction.Descriptor()); registerAction(new SortByDateAction.Descriptor()); registerAction(new SortByExtensionAction.Descriptor()); registerAction(new SortByGroupAction.Descriptor()); registerAction(new SortByNameAction.Descriptor()); registerAction(new SortByOwnerAction.Descriptor()); registerAction(new SortByPermissionsAction.Descriptor()); registerAction(new SortBySizeAction.Descriptor()); registerAction(new SplitEquallyAction.Descriptor()); registerAction(new SplitFileAction.Descriptor()); registerAction(new SplitHorizontallyAction.Descriptor()); registerAction(new SplitVerticallyAction.Descriptor()); registerAction(new StopAction.Descriptor()); registerAction(new ToggleSinglePanelAction.Descriptor()); registerAction(new SwapFoldersAction.Descriptor()); registerAction(new SwitchActiveTableAction.Descriptor()); registerAction(new ToggleAutoSizeAction.Descriptor()); // registerAction(new ToggleColumnAction.Descriptor()); registerAction(new ToggleCommandBarAction.Descriptor()); registerAction(new ToggleDateColumnAction.Descriptor()); registerAction(new ToggleExtensionColumnAction.Descriptor()); registerAction(new ToggleGroupColumnAction.Descriptor()); registerAction(new ToggleHiddenFilesAction.Descriptor()); registerAction(new ToggleLockTabAction.Descriptor()); registerAction(new ToggleOwnerColumnAction.Descriptor()); registerAction(new TogglePermissionsColumnAction.Descriptor()); registerAction(new ToggleShowFoldersFirstAction.Descriptor()); registerAction(new ToggleFoldersAlwaysAlphabeticalAction.Descriptor()); registerAction(new ToggleSizeColumnAction.Descriptor()); registerAction(new ToggleStatusBarAction.Descriptor()); registerAction(new ToggleToolBarAction.Descriptor()); registerAction(new ToggleTreeAction.Descriptor()); registerAction(new UnmarkAllAction.Descriptor()); registerAction(new UnmarkGroupAction.Descriptor()); registerAction(new MarkEmptyFilesAction.Descriptor()); registerAction(new UnpackAction.Descriptor()); registerAction(new ViewAction.Descriptor()); registerAction(new ViewAsAction.Descriptor()); registerAction(new EditAsAction.Descriptor()); registerAction(new TerminalAction.Descriptor()); if (OsFamily.MAC_OS_X.isCurrent()) { registerAction(new TerminalAltAction.Descriptor()); } registerAction(new FindFileAction.Descriptor()); registerAction(new CalculatorAction.Descriptor()); registerAction(new CreateSymlinkAction.Descriptor()); registerAction(new LocateSymlinkAction.Descriptor()); registerAction(new EditCommandsAction.Descriptor()); registerAction(new TerminalPanelAction.Descriptor()); registerAction(new ShowFoldersSizeAction.Descriptor()); registerAction(new ToggleTableViewModeFullAction.Descriptor()); registerAction(new ToggleTableViewModeCompactAction.Descriptor()); registerAction(new ToggleTableViewModeShortAction.Descriptor()); registerAction(new EjectDriveAction.Descriptor()); registerAction(new CompareFilesAction.Descriptor()); registerAction(new TogglePanelPreviewModeAction.Descriptor()); registerAction(new TextEditorsListAction.Descriptor()); registerAction(new UserMenuAction.Descriptor()); } public static void registerCommandsActions() { // register "open with" commands as actions, to allow for keyboard shortcuts for them for (Command command : CommandManager.commands()) { if (command.getType() == CommandType.NORMAL_COMMAND) { ActionManager.registerAction(new CommandAction.Descriptor(command)); } } } /** * Registration method for MuActions. * * @param actionDescriptor - ActionDescriptor instance of the action. */ private static void registerAction(ActionDescriptor actionDescriptor) { ActionProperties.addActionDescriptor(actionDescriptor); } /** * Return all ids of the registered actions. * * @return Enumeration of all registered actions' ids. */ public static Iterator getActionIds() { return ActionProperties.actionDescriptors.keySet().iterator(); } /** * Return the id of MuAction in a given path. * * @param actionClassPath - path to MuAction class. * @return String representing the id of the MuAction in the specified path. null is returned if the given path is invalid. */ public static String extrapolateId(String actionClassPath) { if (actionClassPath == null) { return null; } Matcher matcher = PATTERN.matcher(actionClassPath); return matcher.matches() ? matcher.group(1) : actionClassPath; } /** * Checks whether an MuAction is registered. * * @param actionId - id of MuAction. * @return true if an MuAction which is represented by the given id is registered, otherwise return false. */ public static boolean isActionExist(String actionId) { //return actionId != null && actionFactories.containsKey(actionId); return actionId != null && ActionProperties.actionDescriptors.containsKey(actionId); } /** * Convenience method that returns an instance of the action corresponding to the given Command, * and associated with the specified MainFrame. This method gets the ID of the relevant action, * passes it to {@link #getActionInstance(String, MainFrame)} and returns the {@link TcAction} instance. * * @param command the command that is invoked by the returned action * @param mainFrame the MainFrame instance the action belongs to * @return a MuAction instance matching the given action ID and MainFrame, null if the * @see #getActionInstance(String, MainFrame) * action could not be found or could not be instantiated. */ public static TcAction getActionInstance(Command command, MainFrame mainFrame) { return getActionInstance(new CommandAction.Descriptor(command).getId(), mainFrame); } /** * Convenience method that returns an instance of the action denoted by the given ID, and associated with the * specified MainFrame. This method creates an ActionParameters with no initial property, passes it to * {@link #getActionInstance(ActionParameters, MainFrame)} and returns the {@link TcAction} instance. * * @param actionId ID of the action to instantiate * @param mainFrame the MainFrame instance the action belongs to * @return a MuAction instance matching the given action ID and MainFrame, null if the * @see #getActionInstance(ActionParameters, MainFrame) * action could not be found or could not be instantiated. */ public static TcAction getActionInstance(String actionId, MainFrame mainFrame) { return getActionInstance(new ActionParameters(actionId), mainFrame); } public static Optional getActionInstance2(ActionParameters actionParameters, MainFrame mainFrame) { return Optional.ofNullable(getActionInstance(actionParameters,mainFrame)); } /** * Returns an instance of the MuAction class denoted by the given ActionParameters and for the * specified MainFrame. If an existing instance corresponding to the same ActionParameters and MainFrame is found, * it is simply returned. * If no matching instance could be found, a new instance is created, added to the internal action instances map * (for further use) and returned. * If the action denoted by the specified ActionParameters cannot be found or cannot be instantiated, * null is returned. * * @param actionParameters a descriptor of the action to instantiate with initial properties * @param mainFrame the MainFrame instance the action belongs to * @return a MuAction instance matching the given ActionParameters and MainFrame, null if the * MuAction action denoted by the ActionParameters could not be found or could not be instantiated. */ public static TcAction getActionInstance(ActionParameters actionParameters, MainFrame mainFrame) { Map mainFrameActions = mainFrameActionsMap.computeIfAbsent(mainFrame, k -> new Hashtable<>()); // Looks for an existing MuAction instance used by the specified MainFrame if (mainFrameActions.containsKey(actionParameters)) { return mainFrameActions.get(actionParameters).getAction(); } else { String actionId = actionParameters.getActionId(); // Looks for the action's factory //ActionFactory actionFactory = actionFactories.get(actionId); ActionFactory actionFactory = ActionProperties.actionDescriptors.get(actionId); if (actionFactory == null) { // LOGGER.debug("couldn't initiate action: " + actionId + ", its factory wasn't found"); // return null; throw new IllegalStateException("couldn't initiate action: " + actionId + ", its factory wasn't found"); } Map properties = actionParameters.getInitProperties(); // If no properties hashtable is specified in the action descriptor if (properties == null) { properties = Collections.emptyMap(); } // else clone the hashtable to ensure that it doesn't get modified by action instances. // Since cloning is an expensive operation, this is done only if the hashtable is not empty. else if(!properties.isEmpty()) { properties = new Hashtable<>(properties); } // Instantiate the MuAction class TcAction action = actionFactory.createAction(mainFrame, properties); mainFrameActions.put(actionParameters, new ActionAndIdPair(action, actionId)); // If the action's label has not been set yet, use the action descriptor's if (action.getLabel() == null) { // Retrieve the standard label entry from the dictionary and use it as this action's label String label = ActionProperties.getActionLabel(actionId); // Append '...' to the label if this action invokes a dialog when performed if (action.getClass().isAnnotationPresent(InvokesDialog.class)) { label += "..."; } action.setLabel(label); // Looks for a standard label entry in the dictionary and if it is defined, use it as this action's tooltip String tooltip = ActionProperties.getActionTooltip(actionId); if (tooltip != null) action.setToolTipText(tooltip); } // If the action's accelerators have not been set yet, use the ones from ActionKeymap if (action.getAccelerator() == null) { // Retrieve the standard accelerator (if any) and use it as this action's accelerator KeyStroke accelerator = ActionKeymap.getAccelerator(actionId); if (accelerator != null) { action.setAccelerator(accelerator); } // Retrieve the standard alternate accelerator (if any) and use it as this action's alternate accelerator accelerator = ActionKeymap.getAlternateAccelerator(actionId); if (accelerator != null) { action.setAlternateAccelerator(accelerator); } } // If the action's icon has not been set yet, use the action descriptor's if (action.getIcon() == null) { // Retrieve the standard icon image (if any) and use it as the action's icon ImageIcon icon = ActionProperties.getActionIcon(actionId); if (icon != null) { action.setIcon(icon); } } return action; } } /** * Returns a ArrayList of all MuAction instances matching the specified action id. * * @param muActionId the MuAction id to compare instances against * @return a ArrayList of all MuAction instances matching the specified action id */ static List getActionInstances(String muActionId) { List actionInstances = new ArrayList<>(); // Iterate on all MainFrame instances for (Map actionParametersActionAndIdPairHashtable : mainFrameActionsMap.values()) { // Iterate on all the MainFrame's actions and their ids pairs for (ActionAndIdPair actionAndIdPair : actionParametersActionAndIdPairHashtable.values()) { if (actionAndIdPair.getId().equals(muActionId)) { // Found an action matching the specified class actionInstances.add(actionAndIdPair.getAction()); // Jump to the next MainFrame break; } } } return actionInstances; } /** * Convenience method that retrieves an instance of the action denoted by the given ID and associated * with the given {@link MainFrame} and calls {@link TcAction#performAction()} on it. * Returns true if an instance of the action could be retrieved and performed, false * if the MuAction could not be found or could not be instantiated. * * @param actionId ID of the action to perform * @param mainFrame the MainFrame the action belongs to * @return true if the action instance could be retrieved and the action performed, false otherwise */ public static boolean performAction(String actionId, MainFrame mainFrame) { return performAction(new ActionParameters(actionId), mainFrame); } /** * Convenience method that retrieves an instance of the MuAction denoted by the given {@link ActionParameters} * and associated with the given {@link com.mucommander.ui.main.MainFrame} and calls {@link TcAction#performAction()} on it. * Returns true if an instance of the action could be retrieved and performed, false * if the MuAction could not be found or could not be instantiated. * * @param actionParameters the ActionParameters of the action to perform * @param mainFrame the MainFrame the action belongs to * @return true if the action instance could be retrieved and the action performed, false otherwise */ public static boolean performAction(ActionParameters actionParameters, MainFrame mainFrame) { TcAction action = getActionInstance(actionParameters, mainFrame); if (action == null) { return false; } action.performAction(); return true; } /** * Helper class to represent a pair of instance and id of MuAction. */ private static class ActionAndIdPair { private final TcAction action; private final String id; ActionAndIdPair(TcAction action, String id) { this.action = action; this.id = id; } public TcAction getAction() { return action; } public String getId() { return id; } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionParameters.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import java.util.Map; /** * A descriptor class for {@link TcAction} instances. An ActionParameters is the combination of a MuAction * class (a class extending MuAction and following its conventions) and a set of properties used for instantiation. * Thus, it not only identifies an action class but also the way it is instantiated. * *

    Two ActionParameters instances are equal only if: *

      *
    • they refer to the same action ID *
    • both sets of initialization properties are equal, i.e. they contain the same key/value pairs (deep equality) *
    * This means that two ActionParameters instances referring to the same MuAction ID but with a different set of * initialization properties will not be equal. * *

    This class is used by ActionManager to instance MuAction and allow several instances to live in memory only if * they have different initialization properties, which translated into action speak means a different appearance * and/or behavior. * * @see ActionManager * @see TcAction * * @author Maxence Bernard */ public class ActionParameters { /** Action ID */ private final String actionId; /** Initialization properties, null if there are no initialization properties */ private final Map properties; /** * Convenience constructor which has the same effect as calling {@link #ActionParameters(String, Map)} * with a null value for the properties parameter. * * @param actionId a MuAction id */ public ActionParameters(String actionId) { this(actionId, null); } /** * Creates a new ActionParameters which identifies the specified combination of MuAction action ID and * initialization properties. The properties parameter may be null if the action class is * to be instantiated without any initialization properties. * * @param actionId a MuAction id * @param initProperties a Hashtable containing the properties that will be used to instantiate the specified MuAction class */ public ActionParameters(String actionId, Map initProperties) { this.actionId = actionId; this.properties = initProperties; } /** * Returns the action ID that was used to create this object. * * @return the action ID that was used to create this object. */ public String getActionId() { return actionId; } /** * Returns the list of properties that are to be used to instantiate the MuAction class, or null if * there are none. * * @return the list of properties that are to be used to instantiate the MuAction class, or null if * there are none */ public Map getInitProperties() { return properties; } /** * Returns true if the given object is an ActionParameters that is equal to this one. * ActionParameters instances are considered equal if they refer to the same {@link TcAction} class and * set of initialization properties. */ @Override public boolean equals(Object o) { if(!(o instanceof ActionParameters)) return false; ActionParameters ad = (ActionParameters)o; return actionId.equals(ad.actionId) && (((properties==null || properties.isEmpty()) && (ad.properties==null || ad.properties.isEmpty())) || (properties!=null && ad.properties!=null && properties.equals(ad.properties))); } /** * Returns a hash code value for this ActionParameters, making this class suitable for use as a key in a Hashtable. */ @Override public int hashCode() { return actionId.hashCode() + 27*(properties==null?0:properties.hashCode()); } /** * Returns a String representation of this ActionParameters. */ @Override public String toString() { return super.toString()+" class="+actionId+" properties="+properties; } } ================================================ FILE: src/main/java/com/mucommander/ui/action/ActionProperties.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.util.*; /** * Class that maintains properties of the registered MuAction-s: * 1. General properties of all registered actions, such as all existing action categories. * 2. ActionDescriptors and helper methods for fetching specific property from ActionDescriptor. * 3. Default actions <-> shortcuts mapping. * * @author Arik Hadas */ public class ActionProperties { /* Maps action id -> action descriptor */ static final Map actionDescriptors = new HashMap<>(200); private static final ActionDescriptor nullActionDescriptor = new NullActionDescriptor(); /* Contains all used action categories (i.e. for each category at least one action is registered) */ private static final TreeSet actionCategories = new TreeSet<>(); /* Maps action id -> primary shortcut */ private static final Map defaultPrimaryActionKeymap = new HashMap<>(); /* Maps action id -> alternative shortcut */ private static final Map defaultAlternateActionKeymap = new HashMap<>(); /* Maps shortcut -> action id */ private static final AcceleratorMap defaultAcceleratorMap = new AcceleratorMap(); /** * Getter for ActionDescriptor. * * @param actionDescriptor - an ActionDescriptor instance to be registered. */ public static void addActionDescriptor(ActionDescriptor actionDescriptor) { String actionId = actionDescriptor.getId(); // Add the descriptor to the descriptors map. actionDescriptors.put(actionId, actionDescriptor); // Add the category in the descriptor to the categories pool ActionCategory category = actionDescriptor.getCategory(); if (category != null) { actionCategories.add(category); } // Add the shortcuts in the descriptor to the default keymap KeyStroke defaultActionKeyStroke = actionDescriptor.getDefaultKeyStroke(); if (defaultActionKeyStroke != null) { defaultPrimaryActionKeymap.put(actionId, defaultActionKeyStroke); defaultAcceleratorMap.putAccelerator(defaultActionKeyStroke, actionId); } KeyStroke defaultActionAlternativeKeyStroke = actionDescriptor.getDefaultAltKeyStroke(); if (defaultActionAlternativeKeyStroke != null) { defaultAlternateActionKeymap.put(actionId, defaultActionAlternativeKeyStroke); defaultAcceleratorMap.putAlternativeAccelerator(defaultActionAlternativeKeyStroke, actionId); } } /** * Getter for MuAction's descriptor. * * @param actionId - id of MuAction. * @return ActionDescriptor of the given MuAction. null is returned if ActionDescriptor doesn't exist. */ public static ActionDescriptor getActionDescriptor(String actionId) { return actionDescriptors.get(actionId); } /** * Getter for MuAction's description. * MuAction Description is: * 1. action's tooltip. * 2. if tooltip doesn't exist then action's label. * 3. if tooltip and label don't exist, then action's label key. * * @param actionId - id of MuAction. * @return Description of MuAction as described above. */ public static String getActionDescription(String actionId) { return getNullSafeActionDescriptor(actionId).getDescription(); } /** * Getter for MuAction's category. * * @param actionId - id of MuAction. * @return ActionCategory of the given MuAction. null is returned if ActionCategory doesn't exist. */ public static ActionCategory getActionCategory(String actionId) { return getNullSafeActionDescriptor(actionId).getCategory(); } /** * Getter for MuAction's default primary shortcut. * * @param actionId - id of MuAction. * @return default shortcut of the given MuAction. null is returned if default shortcut doesn't exist. */ public static KeyStroke getDefaultAccelerator(String actionId) { return defaultPrimaryActionKeymap.get(actionId); } /** * Getter for MuAction's alternative shortcut. * * @param actionId - id of MuAction. * @return alternative shortcut for the given MuAction. null is returned if alternative shortcut doesn't exist. */ public static KeyStroke getDefaultAlternativeAccelerator(String actionId) { return defaultAlternateActionKeymap.get(actionId); } /** * Getter for shortcut's default MuAction. * * @param keyStroke - shortcut. * @return default MuAction which the given shortcut is assigned for. null is returned if the shortcut doesn't * assign to any MuAction by default. */ static String getDefaultActionForKeyStroke(KeyStroke keyStroke) { return defaultAcceleratorMap.getActionId(keyStroke); } /** * Getter for shortcut's default type. * The shortcut's type can be either PRIMARY_ACCELERATOR or ALTERNATIVE_ACCELERATOR or 0 if the shortcut doesn't exist by default. * * @param keyStroke - shortcut. * @return default shortcut's type (PRIMARY_ACCELERATOR/ALTERNATIVE_ACCELERATOR). */ static int getDefaultAcceleratorType(KeyStroke keyStroke) { return defaultAcceleratorMap.getAcceleratorType(keyStroke); } /** * Getter for MuAction's label. * * @param actionId - id of MuAction. * @return Label of MuAction. if the label doesn't exist in the dictionary, its key is returned. * null is returned if label's key doesn't exist. */ public static String getActionLabel(String actionId) { return getNullSafeActionDescriptor(actionId).getLabel(); } /** * Getter for MuAction's label key. * * @param actionId - id of MuAction. */ public static String getActionLabelKey(String actionId) { return getNullSafeActionDescriptor(actionId).getLabelKey(); } /** * Getter for MuAction's icon. * * @param actionId - id of MuAction. * @return Icon of MuAction. null is returned if there is no icon for the action. */ public static ImageIcon getActionIcon(String actionId) { return getNullSafeActionDescriptor(actionId).getIcon(); } /** * Getter for MuAction's tooltip. * * @param actionId - id of MuAction. * @return Tooltip of MuAction. null is returned if there is no tooltip for the action. */ public static String getActionTooltip(String actionId) { return getNullSafeActionDescriptor(actionId).getTooltip(); } /** * Getter for all existed categories. * Existed category means an actions' category which at least one of its actions is registered. * * The categories are ordered based on the alphabet order of their descriptions (labels). * * @return Set of existed action categories. */ public static Set getActionCategories() { return actionCategories; } private static ActionDescriptor getNullSafeActionDescriptor(String actionId) { ActionDescriptor actionDescriptor = actionDescriptors.get(actionId); return actionDescriptor != null ? actionDescriptor : nullActionDescriptor; } /** * Helper class that represent ActionDescriptor with null values */ private static class NullActionDescriptor implements ActionDescriptor { public ActionCategory getCategory() { return null; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public String getDescription() { return null; } public ImageIcon getIcon() { return null; } public String getId() { return null; } public String getLabel() { return null; } public String getLabelKey() { return null; } public String getTooltip() { return null; } public boolean isParameterized() { return false; } public TcAction createAction(MainFrame mainFrame, Map properties) { return null; } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/AwtActionProxy.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * AWTActionProxy acts as a proxy between a given java.awt.event.ActionListener and * java.swing.Action, transferring received action events to the Action's actionPerformed * method. * This class provides an easy way to use java.swing.Action instances in AWT components. *

    * Usage: after creating an AWTActionProxy instance, the addActionListener method must be * called on the AWT component which action events are to be proxied, using the AWTActionProxy instance as * a parameter. * * * @author Maxence Bernard */ public class AwtActionProxy implements ActionListener { /** Proxied Action */ private final Action proxiedAction; /** * Creates a new AWTActionProxy instance that will transfer ActionEvents caught by {@link #actionPerformed(java.awt.event.ActionEvent)} * to the specified Action. * * @param action the Action instance to transfer the ActionEvents to. */ public AwtActionProxy(Action action) { this.proxiedAction = action; } /** * Returns the Action instance to which the ActionEvents received by {@link #actionPerformed(java.awt.event.ActionEvent)} * are transferred. */ public Action getProxiedAction() { return proxiedAction; } /** * Forwards the specified ActionEvent to the proxied Action. */ public void actionPerformed(ActionEvent actionEvent) { proxiedAction.actionPerformed(actionEvent); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/InvokesDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * This Annotation should mark {@link TcAction} classes that invoke a dialog when the action is performed, * in order to automatically append '...' to the action's label. * * @author Maxence Bernard */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface InvokesDialog { } ================================================ FILE: src/main/java/com/mucommander/ui/action/TcAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.Map; /** * MuAction extends AbstractAction to add more functionalities and make it easier to integrate within * muCommander. The biggest difference with AbstractAction is that MuAction instances are bound to a * specific {@link MainFrame}.
    * Note that by being an Action, MuAction can be used in every Swing components that accept Action instances. * *

    The MuAction class is abstract. MuAction subclasses must implement the {@link #performAction()} method * to provide a response to the action trigger, and must provide a constructor with the * {@link #TcAction(MainFrame, Map)} signature. * *

    MuAction subclasses should not be instantiated directly, {@link ActionManager}'s getActionInstance * methods should be used instead. Using {@link ActionManager} to retrieve a MuAction ensures that only one instance * exists for a given {@link com.mucommander.ui.main.MainFrame}. This is particularly important because actions are stateful and can be used * in several components of a MainFrame at the same time; if an action's state changes, the change must be reflected * everywhere the action is used. It is also important for performance reasons: sharing one action throughout a * {@link MainFrame} saves some memory and also CPU cycles as some actions listen to particular events to change * their state accordingly. * * @see ActionManager * @see ActionKeymap * @author Maxence Bernard */ public abstract class TcAction extends AbstractAction { TcAction() { mainFrame = null; } /** The MainFrame associated with this MuAction */ protected final MainFrame mainFrame; /** if true, action events are ignored while the MainFrame is in 'no events mode'. Enabled by default. */ private boolean honourNoEventsMode = true; /** if true, #performAction() is called from a separate thread (and not from the event thread) when this action is * performed. Disabled by default. */ private boolean performActionInSeparateThread = false; /** Name of the alternate accelerator KeyStroke property */ private final static String ALTERNATE_ACCELERATOR_PROPERTY_KEY = "alternate_accelerator"; /** * Creates a new MuAction associated with the specified {@link MainFrame}. The properties contained by * the given {@link Map} are used to initialize this action's property map. * * @param mainFrame the MainFrame to associate with this new MuAction * @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial * properties are specified. */ public TcAction(MainFrame mainFrame, Map properties) { this.mainFrame = mainFrame; // Add properties to this Action. for (String key : properties.keySet()) { putValue(key, properties.get(key)); } } /** * Return the {@link MainFrame} this MuAction is associated. * * @return the MainFrame this action is associated with */ public MainFrame getMainFrame() { return this.mainFrame; } /** * Returns the label of this action, null if this action has no label. * The label value is stored in the {@link #NAME} property. * * @return the label of this action, null if this action has no label */ public String getLabel() { return (String)getValue(Action.NAME); } /** * Sets the label for this action, null for no label. * The label value is stored in the {@link #NAME} property. * * @param label the new text label for this action, replacing the previous one (if any) */ public void setLabel(String label) { putValue(Action.NAME, label); } /** * Returns the tooltip text of this action, null if this action has no tooltip. * The tooltip value is stored in the {@link #SHORT_DESCRIPTION} property. * * @return the tooltip text of this action, null if this action has no tooltip */ public String getToolTipText() { return (String)getValue(Action.SHORT_DESCRIPTION); } /** * Sets the tooltip for this action, null for no tooltip. * The tooltip value is stored in the {@link #SHORT_DESCRIPTION} property. * * @param toolTipText the new tooltip text for this action replacing the previous one (if any) */ public void setToolTipText(String toolTipText) { putValue(Action.SHORT_DESCRIPTION, toolTipText); } /** * Return the icon of this action, null if this action has no icon. * The icon value is stored in the {@link #SMALL_ICON} property. * * @return the icon of this action, null if this action has no icon */ public ImageIcon getIcon() { return (ImageIcon)getValue(Action.SMALL_ICON); } /** * Sets the icon for this action, null if this action has no icon. * The icon value is stored in the {@link #SMALL_ICON} property. * * @param icon the new image icon for this action, replacing the previous one (if any) */ public void setIcon(ImageIcon icon) { putValue(Action.SMALL_ICON, icon); } /** * Returns the accelerator KeyStroke of this action, null if this action has no accelerator. * The accelerator value is stored in the Action.ACCELERATOR_KEY property. * * @return the accelerator KeyStroke of this action, null if this action has no accelerator */ public KeyStroke getAccelerator() { return (KeyStroke)getValue(Action.ACCELERATOR_KEY); } /** * Sets the accelerator KeyStroke for this action, null for no accelerator. * The tooltip value is stored in the Action.ACCELERATOR_KEY property. * * @param keyStroke the new accelerator KeyStroke for this action, replacing the previous one (if any) */ public void setAccelerator(KeyStroke keyStroke) { putValue(Action.ACCELERATOR_KEY, keyStroke); } /** * Returns the alternate accelerator KeyStroke of this action, null if it doesn't have any. * The accelerator accelerator value is stored in the {@link #ALTERNATE_ACCELERATOR_PROPERTY_KEY} property. * * @return the alternate accelerator KeyStroke of this action, null if it doesn't have any */ KeyStroke getAlternateAccelerator() { return (KeyStroke)getValue(ALTERNATE_ACCELERATOR_PROPERTY_KEY); } /** * Sets the alternate accelerator KeyStroke for this action, null for none. * The accelerator accelerator value is stored in the {@link #ALTERNATE_ACCELERATOR_PROPERTY_KEY} property. * * @param keyStroke the new alternate accelerator KeyStroke for this action, replacing the previous one (if any) */ void setAlternateAccelerator(KeyStroke keyStroke) { putValue(ALTERNATE_ACCELERATOR_PROPERTY_KEY, keyStroke); } /** * Returns true if both keystrokes' {@link KeyStroke#getKeyChar() char}, * {@link KeyStroke#getKeyCode() code} and {@link KeyStroke#getModifiers() modifiers} are equal. * Unlike {@link KeyStroke#equals(Object)}, this method does not take into account the * {@link KeyStroke#isOnKeyRelease() onKeyRelease} flag. * * @param ks1 first keystroke to test * @param ks2 second keystroke to test * @return true if both keystrokes' char, code and modifiers are equal */ private boolean acceleratorsEqual(KeyStroke ks1, KeyStroke ks2) { return ks1.getKeyChar() == ks2.getKeyChar() && ks1.getKeyCode() == ks2.getKeyCode() && ks1.getModifiers() == ks2.getModifiers(); } /** * Returns true if the given KeyStroke is one of this action's accelerators. Keystrokes are compared * using {@link #acceleratorsEqual(KeyStroke, KeyStroke)}, so that the {@link KeyStroke#isOnKeyRelease()} flag * is not taken into account. This method always returns false if this method has no accelerator. * * @param keyStroke the KeyStroke to test against this action's accelerators * @return true if the given KeyStroke is one of this action's accelerators */ public boolean isAccelerator(KeyStroke keyStroke) { KeyStroke accelerator = getAccelerator(); if (accelerator != null && acceleratorsEqual(accelerator, keyStroke)) return true; accelerator = getAlternateAccelerator(); return accelerator != null && acceleratorsEqual(accelerator, keyStroke); } /** * Returns a displayable String representation of this action's accelerator, in the * [modifier]+[modifier]+...+key format. * This method returns null if this action has no accelerator. * * @return a String representation of the accelerator, or null if this action has no accelerator. */ public String getAcceleratorText() { KeyStroke accelerator = getAccelerator(); if (accelerator == null) { return null; } String text = KeyEvent.getKeyText(accelerator.getKeyCode()); int modifiers = accelerator.getModifiers(); if (modifiers != 0) { text = KeyEvent.getModifiersExText(modifiers) + "+" + text; } return text; } /** * Return true if action events are ignored while the MainFrame associated with this * action is in 'no events mode' (see {@link MainFrame} for an explanation about this mode). * By default, this method returns true. * * @return true if action events are ignored while the MainFrame associated with this * action is in 'no events' mode */ private boolean honourNoEventsMode() { return honourNoEventsMode; } /** * Sets whether action events are to be ignored while the MainFrame associated with this action is in * 'no events mode' (see {@link MainFrame} for an explanation about this mode). * By default (unless this method has been called), 'no events mode' is honoured. * * @param honourNoEventsMode if true, actions events will be ignored while the MainFrame associated * with this action is in 'no events mode' */ protected void setHonourNoEventsMode(boolean honourNoEventsMode) { this.honourNoEventsMode = honourNoEventsMode; } /** * Returns true if {@link #performAction()} is called from a separate thread (and not from the event * thread) when this action is performed. By default, false is returned, i.e. actions are performed * from the main event thread. * *

    Actions that have the potential to hold the caller thread for a substantial amount of time should perform the * action in a separate thread, to avoid locking the event thread. * * @return true if {@link #performAction()} is called from a separate thread (and not from the event * thread) when this action is performed */ private boolean performActionInSeparateThread() { return performActionInSeparateThread; } /** * Sets whether {@link #performAction()} is called from a separate thread (and not from the event thread) when this * action is performed. By default (unless this method has been called), actions are performed from the main event * thread. * *

    Actions that have the potential to hold the caller thread for a substantial amount of time should perform the * action in a separate thread, to avoid locking the event thread. * * @param performActionInSeparateThread true to have {@link #performAction()} called from a separate * thread (and not from the event thread) when this action is performed */ protected void setPerformActionInSeparateThread(boolean performActionInSeparateThread) { this.performActionInSeparateThread = performActionInSeparateThread; } /** * Shorthand for {@link #getStandardIcon(Class)} called with the Class instance returned by {@link #getClass()}. * * @return the standard icon corresponding to this MuAction class, null if none was found */ public ImageIcon getStandardIcon() { return getStandardIcon(getClass()); } /** * Shorthand for {@link #getStandardIconPath(Class)} called with the Class instance returned by {@link #getClass()}. * * @return the standard path for this action's image icon */ public String getStandardIconPath() { return getStandardIconPath(getClass()); } /** * Queries {@link IconManager} for an image icon corresponding to the specified action using standard icon path * conventions. Returns the image icon, null if none was found. * * @param action a MuAction class descriptor * @return the standard icon image corresponding to the specified MuAction class, null if none was found */ public static ImageIcon getStandardIcon(Class action) { // Look for an icon image file with the /action/.png path and use it if it exists String iconPath = getStandardIconPath(action); if (ResourceLoader.getResourceAsURL(iconPath) == null) { return null; } return IconManager.getIcon(iconPath); } /** * Returns the standard path to the icon image for the specified {@link TcAction} class. The returned path is * relative to the application's JAR file. * * @param action a MuAction class descriptor * @return the standard path to the icon image corresponding to the specified MuAction class */ private static String getStandardIconPath(Class action) { return IconManager.IconSet.ACTION.getFolder() + getActionName(action) + ".png"; } private static String getActionName(Class action) { return action.getSimpleName().replace("Action", ""); } public String getId() { return getClass().getSimpleName().replace("Action", ""); } /////////////////////////////////// // AbstractAction implementation // /////////////////////////////////// /** * Intercepts action events and filters them out when the {@link MainFrame} associated with this action is in * 'no events' mode and {@link #honourNoEventsMode()} returns true. * If the action event is not filtered out, {@link #performAction()} is called to provide a response to the action event. */ public void actionPerformed(ActionEvent e) { // Discard this event while in 'no events mode' if (!(mainFrame.getNoEventsMode() && honourNoEventsMode())) { if (performActionInSeparateThread()) { new Thread(this::performAction).start(); } else { performAction(); } } } ////////////////////// // Abstract methods // ////////////////////// /** * Called when this action has been triggered. This method provides a response to the action trigger. */ public abstract void performAction(); /** * Returns the ActionDescriptor of the action. * @return the ActionDescriptor of the action. */ public abstract ActionDescriptor getDescriptor(); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/AbstractViewerAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import com.mucommander.command.Command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.job.TempOpenWithJob; import com.mucommander.process.ProcessRunner; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; /** * Provides a common base for viewer and editor actions. * @author Maxence Bernard, Nicolas Rinaudo */ abstract class AbstractViewerAction extends SelectedFileAction { /** * Creates a new instance of AbstractViewerAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ AbstractViewerAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Enable this action only if the currently selected file is can be read. setSelectedFileFilter(new FileOperationFilter(FileOperation.READ_FILE)); } /** * Edits the currently selected file. */ @Override public synchronized void performAction() { final AbstractFile file = mainFrame.getActiveTable().getSelectedFile(false, true); // At this stage, no assumption should be made on the type of file that is allowed to be viewed/edited: // viewer/editor implementations will decide whether they allow a particular file or not. if (file == null) { return; } Command customCommand = getCustomCommand(file); if (preferInternalAction(file) || customCommand == null) { // If we're not using a custom editor, this action behaves exactly like its parent. performInternalAction(file); return; } // If it's local, init the custom editor on it. if (file.hasAncestor(LocalFile.class)) { try { ProcessRunner.execute(customCommand.getTokens(file), file); } catch(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } else { // If it's distant, copies it locally before running the custom editor on it. ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); TempOpenWithJob job = new TempOpenWithJob(progressDialog, mainFrame, file, customCommand); progressDialog.start(job); } } protected boolean preferInternalAction(AbstractFile file) { return false; } /** * Opens the specified file without a custom command. * @param file file to open. */ protected abstract void performInternalAction(AbstractFile file); protected abstract Command getCustomCommand(AbstractFile file); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ActiveTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.tabs.ActiveTabListener; /** * This class is an abstract {@link TcAction} that operates on the current tab. It monitors changes in the active * tab's properties and calls {@link #toggleEnabledState()} when the properties have changed, or when the active * tab itself has changed, in order to enable or disable this action. * * @author Arik Hadas */ public abstract class ActiveTabAction extends TcAction implements ActivePanelListener, ActiveTabListener { public ActiveTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Listen to active table change events mainFrame.addActivePanelListener(this); // Listen to active tab change events mainFrame.getLeftPanel().getTabs().addActiveTabListener(this); mainFrame.getRightPanel().getTabs().addActiveTabListener(this); toggleEnabledState(); } ////////////////////// // Abstract methods // ////////////////////// /** * Enables or disables this action based on the location of the currently active {@link FolderPanel}. * This method is called once by the constructor to set the initial state. Then it is called every time the location * of the currently active FolderPanel has changed, and when the currently active FolderPanel * has changed. */ protected abstract void toggleEnabledState(); ///////////////////////////////// // ActivePanelListener methods // ///////////////////////////////// public void activePanelChanged(FolderPanel folderPanel) { toggleEnabledState(); } ///////////////////////////////// // ActivePanelListener methods // ///////////////////////////////// public void activeTabChanged() { toggleEnabledState(); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/AddBookmarkAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.bookmark.AddBookmarkDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings up the 'Add bookmark' dialog that allows to bookmark the current folder. * * @author Maxence Bernard */ @InvokesDialog public class AddBookmarkAction extends TcAction { private AddBookmarkAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new AddBookmarkDialog(mainFrame); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "AddBookmark"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_B, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new AddBookmarkAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/AddTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; /** * This action adds a new tab in the active panel with the location * that is currently presented in the active tab. * * @author Arik Hadas */ public class AddTabAction extends TcAction { private AddTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getTabs().add(LocalFile.getUserHome()); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "AddTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new AddTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/BatchRenameAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.filter.OrFileFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.BatchRenameDialog; import com.mucommander.ui.main.MainFrame; /** * This action invokes the 'Batch-Rename' dialog which allows to * rename selected files. * * @author Mariusz Jakubowski */ @InvokesDialog public class BatchRenameAction extends SelectedFilesAction { private BatchRenameAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new OrFileFilter( new FileOperationFilter(FileOperation.RENAME), new AndFileFilter( new FileOperationFilter(FileOperation.READ_FILE), new FileOperationFilter(FileOperation.WRITE_FILE) ) )); } @Override public void performAction(FileSet files) { new BatchRenameDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "BatchRename"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F6, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new BatchRenameAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/BringAllToFrontAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import javax.swing.KeyStroke; import java.util.List; import java.util.Map; /** * Brings all MainFrame windows to front, from the last window index to the first, except for the current * (or last active) MainFrame which is brought to the front last. . * After this action has been performed, minimized windows will return to a normal state and windows will be stacked * in the following order: *

      *
    • Current MainFrame *
    • MainFrame #1 *
    • MainFrame #2 *
    • ... *
    • MainFrame #N *
    * * @author Maxence Bernard */ public class BringAllToFrontAction extends TcAction { private BringAllToFrontAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { List mainFrames = WindowManager.getMainFrames(); MainFrame currentMainFrame = WindowManager.getCurrentMainFrame(); int nbMainFrames = mainFrames.size(); MainFrame mainFrame; for (int i = nbMainFrames-1; i >= 0; i--) { mainFrame = mainFrames.get(i); if (mainFrame != currentMainFrame) { mainFrame.toFront(); } } currentMainFrame.toFront(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "BringAllToFront"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new BringAllToFrontAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CalculateChecksumAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.CalculateChecksumDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action invokes the {@link com.mucommander.ui.dialog.file.CalculateChecksumDialog} which allows to calculate * the checksum of the selected files and store the results in a pseudo-standard checksum file. * * @author Maxence Bernard */ @InvokesDialog public class CalculateChecksumAction extends SelectedFilesAction { private CalculateChecksumAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.READ_FILE)); } @Override public void performAction(FileSet files) { new CalculateChecksumDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CalculateChecksum"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_K, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CalculateChecksumAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CalculatorAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import ru.trolsoft.calculator.CalculatorDialog; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Created on 04/06/14. * @author Oleg Trifonov */ @InvokesDialog public class CalculatorAction extends TcAction { private CalculatorAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new CalculatorDialog(mainFrame.getJFrame()).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Calculator"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.ALL; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CalculatorAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ChangeDateAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.ChangeDateDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Brings up a dialog that allows the user to change the date of the currently selected/marked files. * * @author Maxence Bernard */ @InvokesDialog public class ChangeDateAction extends SelectedFilesAction { private ChangeDateAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.CHANGE_DATE)); } @Override public void performAction(FileSet files) { new ChangeDateDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ChangeDate"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_D, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ChangeDateAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ChangeLocationAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action transfers focus to the location field of the currently active FolderPanel to edit or type in * a new folder location. * * @author Maxence Bernard */ public class ChangeLocationAction extends ActiveTabAction { private ChangeLocationAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * current tab is not locked, this action will be enabled, * if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } @Override public void performAction() { mainFrame.getActivePanel().changeCurrentLocation(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ChangeLocation"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_G, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ChangeLocationAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ChangePermissionsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.ChangePermissionsDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Brings up a dialog that allows the user to change the file permissions the currently selected/marked files. * * @author Maxence Bernard */ @InvokesDialog public class ChangePermissionsAction extends SelectedFilesAction { private ChangePermissionsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.CHANGE_PERMISSION)); } @Override public void performAction(FileSet files) { new ChangePermissionsDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ChangePermissions"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ChangePermissionsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ChangeReplicationAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.ChangeReplicationDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Brings up a dialog that allows the user to change the date of the currently selected/marked files. * * @author Maxence Bernard */ @InvokesDialog public class ChangeReplicationAction extends SelectedFilesAction { private ChangeReplicationAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.CHANGE_REPLICATION)); } @Override public void performAction(FileSet files) { new ChangeReplicationDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ChangeReplication"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ChangeReplicationAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CheckForUpdatesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.startup.CheckVersionDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action checks for a new version of muCommander. * * @author Maxence Bernard */ @InvokesDialog public class CheckForUpdatesAction extends TcAction { private CheckForUpdatesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new CheckVersionDialog(mainFrame, null, true); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CheckForUpdates"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CheckForUpdatesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CloneTabToOtherPanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Add a new tab in the other panel with the same location as the one presented in the currently selected tab * * @author Arik Hadas */ public class CloneTabToOtherPanelAction extends TcAction { private CloneTabToOtherPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { AbstractFile currentLocation = mainFrame.getActivePanel().getCurrentFolder(); mainFrame.getInactivePanel().getTabs().add(currentLocation); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CloneTabToOtherPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CloneTabToOtherPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CloseDuplicateTabsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Close duplicate tabs in the folder panel * * @author Arik Hadas */ public class CloseDuplicateTabsAction extends TcAction { private CloseDuplicateTabsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getTabs().closeDuplicateTabs(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CloseDuplicateTabs"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CloseDuplicateTabsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CloseOtherTabsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Close all tabs in the tabbedpane except the selected tab * * @author Arik Hadas */ public class CloseOtherTabsAction extends TcAction { private CloseOtherTabsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getTabs().closeOtherTabs(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CloseOtherTabs"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CloseOtherTabsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CloseTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.tabs.FileTableTabs; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Close the current selected tab * * @author Arik Hadas */ public class CloseTabAction extends ActiveTabAction { private CloseTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * current tab is not locked and is not the only tab in the panel, * this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { FileTableTabs tabs = mainFrame.getActivePanel().getTabs(); setEnabled(!tabs.getCurrentTab().isLocked() && tabs.getTabsCount() > 1); } @Override public void performAction() { // Changes the current folder to make it the user home folder mainFrame.getActivePanel().getTabs().closeCurrentTab(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CloseTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_W, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CloseTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CloseWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * If there is more than one window currently open, this action disposes the currently active MainFrame * (i.e. the one this action is attached to). On the contrary, if there is only one MainFrame currently open, this * action performs {@link com.mucommander.ui.action.impl.QuitAction} to quit the application after confirmation by the user, * if the quit confirmation has not been disabled. * * @author Maxence Bernard */ public class CloseWindowAction extends TcAction { private CloseWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { // Closing the last window is equivalent to quitting the application: perform QuitAction in that case if (WindowManager.getMainFrames().size() == 1) { ActionManager.performAction(QuitAction.Descriptor.ACTION_ID, mainFrame); } else { mainFrame.getJFrame().dispose(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CloseWindow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_W, KeyEvent.META_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F10, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CloseWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CombineFilesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.CombineFilesDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action invokes the merge file dialog which allows to combine file parts into the original file. * * @author Mariusz Jakubowski */ @InvokesDialog public class CombineFilesAction extends SelectedFilesAction { private CombineFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.READ_FILE)); } @Override public void performAction(FileSet files) { // Filter out files that are not regular files FileFilter filter = new AttributeFileFilter(FileAttribute.FILE); filter.filter(files); if (files.isEmpty()) { return; } AbstractFile destFolder = mainFrame.getInactivePanel().getCurrentFolder(); new CombineFilesDialog(mainFrame, files, destFolder).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CombineFiles"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CombineFilesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CommandAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.command.Command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.TempOpenWithJob; import com.mucommander.process.ProcessRunner; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import lombok.Getter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.KeyStroke; import java.util.Map; /** * @author Nicolas Rinaudo */ public class CommandAction extends TcAction { private static final Logger LOGGER = LoggerFactory.getLogger(CommandAction.class); /** Command to init. */ private final Command command; /** * Creates a new CommandAction initialized with the specified parameters. * @param mainFrame frame that will be affected by this action. * @param properties ignored. * @param command command to init when this action is called. */ private CommandAction(MainFrame mainFrame, Map properties, Command command) { super(mainFrame, properties); this.command = command; setLabel(command.getDisplayName()); } // - Action code ----------------------------------------------------------- // ------------------------------------------------------------------------- @Override public void performAction() { // Retrieves the current selection. FileSet selectedFiles = mainFrame.getActiveTable().getSelectedFiles(); // If no files are either selected or marked, aborts. if (selectedFiles.isEmpty() && command.hasSelectedFileKeyword()) { return; } // If we're working with local files, go ahead and runs the command. AbstractFile baseFolder = selectedFiles.getBaseFolder(); if (baseFolder.getURL().getScheme().equals(FileProtocols.FILE) && (baseFolder.hasAncestor(LocalFile.class))) { try { ProcessRunner.execute(command.getTokens(selectedFiles), selectedFiles.getBaseFolder()); } catch(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); LOGGER.debug("Failed to execute command: {}", command.getCommand(), e); } } // Otherwise, copies the files locally before running the command. else { ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); progressDialog.start(new TempOpenWithJob(new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")), mainFrame, selectedFiles, command)); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(command); } public static final class Descriptor extends AbstractActionDescriptor { private final Command command; private static final String ACTION_ID_PREFIX = "OpenWith_"; private final String ACTION_ID; @Getter private final String label; public Descriptor(Command command) { this.command = command; ACTION_ID = ACTION_ID_PREFIX + command.getAlias() + ":" + command.getDisplayName(); label = String.format("%s %s", Translator.get("file_menu.open_with"), command.getDisplayName()); } public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.COMMANDS; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CommandAction(mainFrame, properties, command); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CompareFilesAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AbstractFileFilter; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.process.ExecutorUtils; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.io.File; import java.io.IOException; import java.util.Map; /** * Created on 01/02/16. * @author Oleg Trifonov */ public class CompareFilesAction extends SelectedFilesAction { private static final String OPENDIFF_PATH = "/usr/bin/opendiff"; private static final String MELD_PATH = "/usr/bin/meld"; public enum DiffMethod { MAC_OS_X_DIFF { @Override void exec(String file1, String file2) throws IOException, InterruptedException { ExecutorUtils.execute(new String[]{OPENDIFF_PATH, file1, file2}); } }, LINUX_MELD { @Override void exec(String file1, String file2) throws IOException, InterruptedException { ExecutorUtils.execute(new String[]{MELD_PATH, file1, file2}); } }; abstract void exec(String file1, String file2) throws IOException, InterruptedException; } private static DiffMethod diffMethod; private CompareFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new AndFileFilter( new FileOperationFilter(FileOperation.READ_FILE), new AbstractFileFilter() { @Override public boolean accept(AbstractFile file) { if (supported()) { AbstractFile leftFile = mainFrame.getLeftPanel().getFileTable().getSelectedFile(); AbstractFile rightFile = mainFrame.getRightPanel().getFileTable().getSelectedFile(); return isLocalFile(leftFile) && isLocalFile(rightFile); } return false; } } )); } private static boolean isLocalFile(AbstractFile file) { return file != null && !file.isDirectory() && file instanceof LocalFile; } @Override public void performAction(FileSet files) { AbstractFile leftFile = mainFrame.getLeftPanel().getFileTable().getSelectedFile(); AbstractFile rightFile = mainFrame.getRightPanel().getFileTable().getSelectedFile(); if (leftFile == null || rightFile == null) { return; } String leftFilePath = leftFile.getAbsolutePath().replace(" ", "\\ "); String rightFilePath = rightFile.getAbsolutePath().replace(" ", "\\ "); compareTwoFiles(leftFilePath, rightFilePath); } public static void compareTwoFiles(String file1, String file2) { if (diffMethod == null || file1 == null || file2 == null) { return; } new Thread(() -> { try { diffMethod.exec(file1, file2); } catch (IOException | InterruptedException e) { e.printStackTrace(); } }).start(); } public static boolean supported() { if (diffMethod != null) { return true; } switch (OsFamily.getCurrent()) { case MAC_OS_X: if (new File(OPENDIFF_PATH).exists()) { diffMethod = DiffMethod.MAC_OS_X_DIFF; return true; } break; case LINUX: if (new File(MELD_PATH).exists()) { diffMethod = DiffMethod.LINUX_MELD; return true; } break; } return false; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CompareFiles"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CompareFilesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CompareFolderFilesAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; /** * This action compares the content of the 2 MainFrame's file tables and marks the files with different size or content. * * Created on 10/07/17. * @author Oleg Trifonov */ public class CompareFolderFilesAction extends TcAction { private CompareFolderFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); FileTable inactiveTableTable = mainFrame.getInactiveTable(); BaseFileTableModel activeTableModel = activeTable.getFileTableModel(); BaseFileTableModel inactiveTableModel = inactiveTableTable.getFileTableModel(); if (compare(activeTableModel, inactiveTableModel)) { activeTable.repaint(); } // Notify registered listeners that currently marked files have changed on the file tables activeTable.fireMarkedFilesChangedEvent(); } private boolean compare(BaseFileTableModel firstTableModel, BaseFileTableModel secondTableModel) { boolean result = false; int nbFilesFirst = firstTableModel.getFileCount(); int nbFilesSecond = secondTableModel.getFileCount(); MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return false; } for (int i = 0; i < nbFilesFirst; i++) { AbstractFile tempFile = firstTableModel.getFileAt(i); if (tempFile.isDirectory()) { continue; } String tempFileName = tempFile.getName(); int fileIndex = -1; for (int j = 0; j < nbFilesSecond; j++) { if (secondTableModel.getFileAt(j).getName().equals(tempFileName)) { fileIndex = j; break; } } if (fileIndex < 0 || !checkEqual(digest, secondTableModel.getFileAt(fileIndex), tempFile)) { firstTableModel.setFileMarked(tempFile, true); result = true; } } return result; } private boolean checkEqual(MessageDigest digest, AbstractFile file1, AbstractFile file2) { if (file1.getSize() != file2.getSize()) { return false; } String checksum1 = getChecksum(digest, file1); String checksum2 = getChecksum(digest, file2); return checksum1 != null && checksum1.equals(checksum2); } private String getChecksum(MessageDigest digest, AbstractFile file) { digest.reset(); try (InputStream is = file.getInputStream()) { return AbstractFile.calculateChecksum(is, digest); } catch (IOException e) { e.printStackTrace(); return null; } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CompareFolderFiles"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CompareFolderFilesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CompareFoldersAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action compares the content of the 2 MainFrame's file tables and marks the files that are different. * * @author Maxence Bernard */ public class CompareFoldersAction extends TcAction { private CompareFoldersAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable leftTable = mainFrame.getLeftPanel().getFileTable(); FileTable rightTable = mainFrame.getRightPanel().getFileTable(); BaseFileTableModel leftTableModel = leftTable.getFileTableModel(); BaseFileTableModel rightTableModel = rightTable.getFileTableModel(); if (compare(leftTableModel, rightTableModel)) { leftTable.repaint(); } if (compare(rightTableModel, leftTableModel)) { rightTable.repaint(); } // Notify registered listeners that currently marked files have changed on the file tables leftTable.fireMarkedFilesChangedEvent(); rightTable.fireMarkedFilesChangedEvent(); } private boolean compare(BaseFileTableModel firstTableModel, BaseFileTableModel secondTableModel) { boolean result = false; int nbFilesFirst = firstTableModel.getFileCount(); int nbFilesSecond = secondTableModel.getFileCount(); for (int i = 0; i < nbFilesFirst; i++) { AbstractFile tempFile = firstTableModel.getFileAt(i); if (tempFile.isDirectory()) { continue; } String tempFileName = tempFile.getName(); int fileIndex = -1; for (int j = 0; j < nbFilesSecond; j++) { if (secondTableModel.getFileAt(j).getName().equals(tempFileName)) { fileIndex = j; break; } } if (fileIndex < 0 || secondTableModel.getFileAt(fileIndex).getLastModifiedDate() < tempFile.getLastModifiedDate()) { firstTableModel.setFileMarked(tempFile, true); result = true; } } return result; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CompareFolders"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { // if (OsFamily.getCurrent() != OsFamily.MAC_OS_X) { return KeyStroke.getKeyStroke(KeyEvent.VK_M, KeyEvent.CTRL_DOWN_MASK); // } else { // return KeyStroke.getKeyStroke(KeyEvent.VK_M, KeyEvent.META_DOWN_MASK); // } } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CompareFoldersAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ConnectToServerAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.server.ServerConnectDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the 'Connect to Server' dialog that assists the user in connecting to a remote server. * * @author Maxence Bernard */ @InvokesDialog public class ConnectToServerAction extends ActiveTabAction { private ConnectToServerAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * current tab is not locked, this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } @Override public void performAction() { new ServerConnectDialog(mainFrame.getActivePanel()).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ConnectToServer"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_K, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ConnectToServerAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CopyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.file.CopyDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action invokes the 'Copy dialog' which allows to copy the currently selected/marked files to a specified destination. * * @author Maxence Bernard */ public class CopyAction extends SelectedFilesAction { private CopyAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.READ_FILE)); } @Override public void performAction(FileSet files) { new CopyDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Copy"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CopyAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CopyFileBaseNamesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.dnd.TransferableFileSet; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action copies the file base name(s) (without extension) of the currently selected / marked files(s) to the system clipboard. * * @author Chen Rozenes */ public class CopyFileBaseNamesAction extends SelectedFilesAction { private CopyFileBaseNamesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { // create a TransferableFileSet and make DataFlavour.stringFlavor (text) the only DataFlavour supported TransferableFileSet tfs = new TransferableFileSet(files); // Disable unwanted data flavors tfs.setJavaFileListDataFlavorSupported(false); tfs.setTextUriFlavorSupported(false); // Note: not disabling this flavor would throw an exception because the flavor data is not serializable tfs.setFileSetDataFlavorSupported(false); // Transfer filenames, not file paths tfs.setStringDataFlavourTransfersFilename(true); // Transfer base names (filename without its extension) tfs.setStringDataFlavourTransfersFileBaseName(true); ClipboardSupport.setClipboardContents(tfs); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CopyFileBaseNames"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.ALT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CopyFileBaseNamesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CopyFileNamesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.dnd.TransferableFileSet; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action copies the filename(s) of the currently selected / marked files(s) to the system clipboard. * * @author Maxence Bernard */ public class CopyFileNamesAction extends SelectedFilesAction { private CopyFileNamesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { // create a TransferableFileSet and make DataFlavour.stringFlavor (text) the only DataFlavour supported TransferableFileSet tfs = new TransferableFileSet(files); // Disable unwanted data flavors tfs.setJavaFileListDataFlavorSupported(false); tfs.setTextUriFlavorSupported(false); // Note: not disabling this flavor would throw an exception because the flavor data is not serializable tfs.setFileSetDataFlavorSupported(false); // Transfer filenames, not file paths tfs.setStringDataFlavourTransfersFilename(true); ClipboardSupport.setClipboardContents(tfs); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CopyFileNames"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { if (OsFamily.getCurrent() != OsFamily.MAC_OS_X) { return KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK); } else { return KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.META_DOWN_MASK); } } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CopyFileNamesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CopyFilePathsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.dnd.TransferableFileSet; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action copies the path(s) of the currently selected / marked files(s) to the system clipboard. * * @author Maxence Bernard */ public class CopyFilePathsAction extends SelectedFilesAction { private CopyFilePathsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { // create a TransferableFileSet and make DataFlavour.stringFlavor (text) the only DataFlavour supported TransferableFileSet tfs = new TransferableFileSet(files); // Disable unwanted data flavors tfs.setJavaFileListDataFlavorSupported(false); tfs.setTextUriFlavorSupported(false); // Note: not disabling this flavor would throw an exception because the flavor data is not serializable tfs.setFileSetDataFlavorSupported(false); ClipboardSupport.setClipboardContents(tfs); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CopyFilePaths"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CopyFilePathsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CopyFilesToClipboardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action copies the selected / marked files to the system clipboard, allowing to paste * them to muCommander or another application. * * @author Maxence Bernard */ public class CopyFilesToClipboardAction extends SelectedFilesAction { private CopyFilesToClipboardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { ClipboardSupport.setClipboardFiles(files); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CopyFilesToClipboard"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.META_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_C, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CopyFilesToClipboardAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CreateSymlinkAction.java ================================================ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.symlink.CreateSymLinkDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings up the 'create symlink' dialog. * * @author Oleg Trifonov */ @InvokesDialog public class CreateSymlinkAction extends ParentFolderAction { private CreateSymlinkAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected void toggleEnabledState() { AbstractFile targetFile = mainFrame.getActiveTable().getSelectedFile(); if (targetFile == null) { AbstractFile f = mainFrame.getActiveTable().getFileTableModel().getFileAt(0); if (f != null) { targetFile = f.getParent(); } } AbstractFile linkPath = mainFrame.getInactivePanel().getCurrentFolder(); setEnabled(targetFile != null && linkPath != null && linkPath.isFileOperationSupported(FileOperation.CREATE_DIRECTORY)); } @Override public void performAction() { AbstractFile targetFile = mainFrame.getActiveTable().getSelectedFile(); if (targetFile == null) { targetFile = mainFrame.getActiveTable().getFileTableModel().getFileAt(0).getParent(); } AbstractFile linkPath = mainFrame.getInactivePanel().getCurrentFolder(); new CreateSymLinkDialog(mainFrame.getJFrame(), linkPath, targetFile).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CreateSymlink"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F7, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CreateSymlinkAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CustomizeCommandBarAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.customization.CommandBarDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action opens the dialog which allows the user to customize the command-bar. * * @author Arik Hadas */ @InvokesDialog public class CustomizeCommandBarAction extends TcAction { private CustomizeCommandBarAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new CommandBarDialog(mainFrame).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CustomizeCommandBar"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CustomizeCommandBarAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/CutFilesToClipboardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dnd.ClipboardOperations; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action cuts the selected / marked files to the system clipboard, allowing to paste * them to muCommander. * * @author Nicholai R. Svarre */ public class CutFilesToClipboardAction extends SelectedFilesAction { private CutFilesToClipboardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { ClipboardSupport.setClipboardFiles(files); ClipboardSupport.setOperation(ClipboardOperations.CUT); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "CutFilesToClipboard"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.META_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_X, KeyEvent.CTRL_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new CutFilesToClipboardAction(mainFrame, properties); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/DeleteAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.file.DeleteDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action invokes a Delete confirmation dialog to delete currently the selected / marked files * in the currently active folder. Files are moved to the system trash when possible, i.e. if there is a trash available * on the current OS environment, and if the selected files are on a filesystem that allows it (usually only local files * can be moved to the trash). * * @see com.mucommander.ui.action.impl.PermanentDeleteAction * @author Maxence Bernard */ public class DeleteAction extends SelectedFilesAction { private DeleteAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.DELETE)); } @Override public void performAction(FileSet files) { new DeleteDialog(mainFrame, files, false).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Delete"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new DeleteAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/DonateAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action opens the mucommander.com donation page URL in the system's default browser. * * @author Maxence Bernard */ public class DonateAction extends OpenURLInBrowserAction { private DonateAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); putValue(URL_PROPERTY_KEY, com.mucommander.RuntimeConstants.DONATION_URL); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Donate"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new DonateAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/DuplicateTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Open a new tab in the current folder panel with the same location as the currently selected tab * * @author Arik Hadas */ public class DuplicateTabAction extends TcAction { private DuplicateTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getTabs().duplicate(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "DuplicateTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_D, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new DuplicateTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EditAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.viewer.EditorRegistrar; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * User configurable variant of {@link InternalEditAction}. * @author Maxence Bernard, Nicolas Rinaudo */ public class EditAction extends InternalEditAction { /** * Creates a new instance of EditAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private EditAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected boolean preferInternalAction(AbstractFile file) { return !EditorRegistrar.getAllEditors(file).isEmpty(); } @Override protected Command getCustomCommand(AbstractFile file) { return CommandManager.getCommandForAlias(CommandManager.EDITOR_ALIAS, file); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Edit"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EditAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EditAsAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2019 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.quicklist.EditAsQL; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; public class EditAsAction extends SelectedFilesAction { /** * Creates a new instance of EditAsAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private EditAsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); ImageIcon icon = getStandardIcon(EditAction.class); if (icon != null) { setIcon(icon); } } @Override public ActionDescriptor getDescriptor() { return new EditAsAction.Descriptor(); } @Override public void performAction(FileSet files) { AbstractFile file = mainFrame.getActiveTable().getSelectedFile(false, true); // At this stage, no assumption should be made on the type of file that is allowed to be viewed/edited: // viewer/editor implementations will decide whether they allow a particular file or not. if (file == null || file.isDirectory()) { return; } new EditAsQL(mainFrame, file).show(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "EditAs"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EditAsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EditBookmarksAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.bookmark.EditBookmarksDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action brings up the 'Edit bookmarks' dialog that allows to edit bookmarks. * * @author Maxence Bernard */ @InvokesDialog public class EditBookmarksAction extends TcAction { private EditBookmarksAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new EditBookmarksDialog(mainFrame); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "EditBookmarks"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EditBookmarksAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EditCommandsAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.commands.EditCommandsDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * @author Oleg Trifonov * Created on 09/10/14. */ @InvokesDialog public class EditCommandsAction extends TcAction { /** * Creates a new MuAction associated with the specified {@link com.mucommander.ui.main.MainFrame}. The properties contained by * the given {@link Map} are used to initialize this action's property map. * * @param mainFrame the MainFrame to associate with this new MuAction * @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial */ private EditCommandsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { var frame = mainFrame.getJFrame(); new EditCommandsDialog(frame, frame).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "EditCommands"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EditCommandsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EditCredentialsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.auth.EditCredentialsDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action brings up the 'Edit credentials' dialog that allows to edit persistent credentials (the ones stored * to disk). * * @author Maxence Bernard */ @InvokesDialog public class EditCredentialsAction extends TcAction { private EditCredentialsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new EditCredentialsDialog(mainFrame); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "EditCredentials"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EditCredentialsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EjectDriveAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.MountedDriveFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.macosx.AppleScript; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.statusbar.TaskWidget; import javax.swing.*; import java.util.List; import java.util.Map; /** * * Created on 26/01/16. * @author Oleg Trifonov */ public class EjectDriveAction extends SelectedFilesAction { private EjectDriveAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new MountedDriveFilter()); } @Override public void performAction(FileSet files) { if (files.size() == 1) { eject(mainFrame, files.get(0)); mainFrame.tryRefreshCurrentFolders(); } } public static void eject(MainFrame mainFrame, AbstractFile file) { if (OsFamily.MAC_OS_X.isCurrent()) { new EjectWorker(mainFrame, file.getName()).execute(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "EjectDrive"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EjectDriveAction(mainFrame, properties); } } private static class EjectWorker extends SwingWorker { private final MainFrame mainFrame; private final TaskWidget taskWidget; private final String fileName; private boolean taskWidgetAttached; private int progress; EjectWorker(MainFrame mainFrame, String fileName) { this.mainFrame = mainFrame; this.fileName = fileName; this.taskWidget = new TaskWidget(); taskWidget.setText(Translator.get("EjectDrive.label")); } @Override protected Void doInBackground() { try { publish(); StringBuilder sb = new StringBuilder(); publish(); Thread t = new Thread(() -> { //try {Thread.sleep(5000); } catch (Throwable e) {} AppleScript.execute("tell application \"Finder\"\n" + " eject disk \"" + fileName + "\"\n" + "end tell", sb); }); t.start(); while (t.isAlive() || progress < 100) { if (!t.isAlive()) { progress += 10; } Thread.sleep(50); publish(); } progress = 100; publish(); } catch (Throwable ignore) {} return null; } @Override protected void process(List chunks) { if (!taskWidgetAttached) { mainFrame.getStatusBar().getTaskPanel().addTask(taskWidget); mainFrame.getStatusBar().revalidate(); mainFrame.getStatusBar().repaint(); taskWidgetAttached = true; } if (progress < 100) { progress += 10; } taskWidget.setProgress(progress); } @Override protected void done() { taskWidget.removeFromPanel(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EmailAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.SendMailJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.file.EmailFilesDialog; import com.mucommander.ui.dialog.pref.general.GeneralPreferencesDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the 'Email files' dialog that allows to email the currently marked files as attachment. * * @author Maxence Bernard */ @InvokesDialog public class EmailAction extends SelectedFilesAction { private EmailAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.READ_FILE)); } @Override public void performAction(FileSet files) { // Notifies the user that mail preferences are not set and brings the preferences dialog if (!SendMailJob.mailPreferencesSet()) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), Translator.get("email_dialog.prefs_not_set"), Translator.get("email_dialog.prefs_not_set_title")); SwingUtilities.invokeLater(() -> { GeneralPreferencesDialog preferencesDialog = GeneralPreferencesDialog.getDialog(); preferencesDialog.setActiveTab(GeneralPreferencesDialog.MAIL_TAB); preferencesDialog.showDialog(); }); return; } new EmailFilesDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Email"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_S, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EmailAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/EmptyTrashAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Empties the system trash. This action is enabled only if the current platform has an * {@link com.mucommander.desktop.AbstractTrash} implementation and if it is capable of emptying the trash, * as reported by {@link com.mucommander.desktop.AbstractTrash#canEmpty()}. * * @author Maxence Bernard */ public class EmptyTrashAction extends TcAction { private EmptyTrashAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); AbstractTrash trash = DesktopManager.getTrash(); setEnabled(trash!=null && trash.canEmpty()); } @Override public void performAction() { AbstractTrash trash = DesktopManager.getTrash(); if (trash != null) { trash.empty(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "EmptyTrash"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new EmptyTrashAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ExploreBookmarksAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action changes the current folder of the currently active {@link com.mucommander.ui.main.FolderPanel} to * bookmark:// which is the root of bookmark filesystem, allowing to explore all the bookmarks the user has. * * @author Nicolas Rinaudo */ public class ExploreBookmarksAction extends ActiveTabAction { private ExploreBookmarksAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().tryChangeCurrentFolder("bookmark://"); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } /** * Enables or disables this action based on the currently active folder's * current tab is not locked, this action will be enabled, * if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ExploreBookmarks"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ExploreBookmarksAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/FileAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.event.TableSelectionListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * FileAction is an abstract action that operates on the currently active FileTable. It is enabled only when * the table condition as tested by {@link #getFileTableCondition(FileTable) getFileTableCondition()} * method is satisfied. * *

    Those tests are performed when: *

      *
    • the selected file on the currently active FileTable has changed *
    • the marked files on the currently active FileTable has changed *
    • the currently active FileTable has changed *
    * * @author Maxence Bernard */ public abstract class FileAction extends TcAction implements TableSelectionListener, ActivePanelListener { /** Filter that restricts the enabled condition to files that match it (can be null) */ protected FileFilter filter; FileAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); init(mainFrame); } private void init(MainFrame mainFrame) { mainFrame.addActivePanelListener(this); mainFrame.getLeftPanel().getFileTable().addTableSelectionListener(this); mainFrame.getRightPanel().getFileTable().addTableSelectionListener(this); // Set initial enabled state updateEnabledState(mainFrame.getActiveTable()); } /** * Enables/disables this action if both of the {@link #getFileTableCondition(FileTable)} and file IMAGE_FILTER * (if there is one) tests are satisfied. * *

    This method is called each time: *

      *
    • the selected file on the currently active FileTable has changed *
    • the marked files on the currently active FileTable has changed *
    • the currently active FileTable has changed *
    * * @param fileTable the currently active FileTable */ protected void updateEnabledState(FileTable fileTable) { // Note: AbstractAction checks if enabled value has changed before firing an event setEnabled(getFileTableCondition(fileTable)); } /** * This method is called to determine if the current FileTable state allows this action to be enabled. * If false is returned, the action will be disabled. * If true is returned, the action will be enabled if the file IMAGE_FILTER (if there is one) matches the * selected file. * * @param fileTable currently active FileTable */ protected abstract boolean getFileTableCondition(FileTable fileTable); /////////////////////////////////////////// // TableSelectionListener implementation // /////////////////////////////////////////// /** * Updates this action's enabled status based on the new currently selected file. */ public void selectedFileChanged(FileTable source) { // No need to update state if the originating FileTable is not the currently active one if (source == mainFrame.getActiveTable()) { updateEnabledState(source); } } /** * Updates this action's enabled status based on the new currently marked files. */ public void markedFilesChanged(FileTable source) { // No need to update state if the originating FileTable is not the currently active one if (source == mainFrame.getActiveTable()) { updateEnabledState(source); } } //////////////////////////////////////// // ActivePanelListener implementation // //////////////////////////////////////// public void activePanelChanged(FolderPanel folderPanel) { updateEnabledState(folderPanel.getFileTable()); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/FindFileAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.FindFileDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Find File action * @author Oleg Trifonov */ @InvokesDialog public class FindFileAction extends ParentFolderAction { private FindFileAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected void toggleEnabledState() { } @Override public void performAction() { AbstractFile currentFolder = mainFrame.getActiveTable().getFileTableModel().getCurrentFolder(); new FindFileDialog(mainFrame, currentFolder).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "FindFile"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new FindFileAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/FocusNextAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.helper.FocusRequester; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action allows to cycle forward through the current FolderPanel's focusable components: file table, folder tree * and location field. The action has no effect when the focus is not in the MainFrame this action is tied to. * * @author Maxence Bernard */ public class FocusNextAction extends TcAction { private FocusNextAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Perform the action also when in 'no events' mode setHonourNoEventsMode(false); } @Override public void performAction() { Component focusOwner = mainFrame.getJFrame().getFocusOwner(); // Abort if the focus is not in the MainFrame this action is tied to if (focusOwner == null) { return; } FolderPanel folderPanel = mainFrame.getActivePanel(); FileTable fileTable = folderPanel.getFileTable(); JTextField locationField = folderPanel.getLocationTextField(); JTree tree = folderPanel.getFoldersTreePanel().getTree(); // Request focus on the 'next' component, the cycle order being from left to right, top to bottom. Component nextComponent; if (focusOwner == locationField) { nextComponent = folderPanel.isTreeVisible() ? tree : fileTable; } else if (focusOwner == tree) { nextComponent = fileTable; } else if(focusOwner == fileTable) { nextComponent = locationField; } else { return; } FocusRequester.requestFocusInWindow(nextComponent); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "FocusNext"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_TAB, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new FocusNextAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/FocusPreviousAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.helper.FocusRequester; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action allows to cycle backward through the current {@link FolderPanel}'s focusable components: file table, * folder tree and location field. The action has no effect when the focus is not in the {@link MainFrame} this action * is tied to. * * @author Maxence Bernard */ public class FocusPreviousAction extends TcAction { private FocusPreviousAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Perform the action also when in 'no events' mode setHonourNoEventsMode(false); } @Override public void performAction() { Component focusOwner = mainFrame.getJFrame().getFocusOwner(); // Abort if the focus is not in the MainFrame this action is tied to if (focusOwner == null) { return; } FolderPanel folderPanel = mainFrame.getActivePanel(); FileTable fileTable = folderPanel.getFileTable(); JTextField locationField = folderPanel.getLocationTextField(); JTree tree = folderPanel.getFoldersTreePanel().getTree(); // Request focus on the 'previous' component, the cycle order being from right to left, bottom to top. Component previousComponent; if (focusOwner == fileTable) { previousComponent = folderPanel.isTreeVisible() ? tree : locationField; } else if (focusOwner == tree) { previousComponent = locationField; } else if(focusOwner == locationField) { previousComponent = fileTable; } else { return; } FocusRequester.requestFocusInWindow(previousComponent); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "FocusPrevious"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK| CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new FocusPreviousAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GarbageCollectAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.icon.CacheableFileIconProvider; import com.mucommander.commons.file.icon.impl.SwingFileIconProvider; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action invokes the garbage collector and is here for debugging purposes only. * * @author Maxence Bernard */ public class GarbageCollectAction extends TcAction { private GarbageCollectAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { var iconProvider = FileFactory.getDefaultFileIconProvider(); if (iconProvider instanceof CacheableFileIconProvider cip) { cip.cleanCache(); } System.gc(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GarbageCollect"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return null; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GarbageCollectAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoBackAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action recalls the previous folder in the current FolderPanel's history. * * @author Maxence Bernard */ public class GoBackAction extends ActiveTabAction { private GoBackAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getFolderHistory().goBack(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } /** * Enables or disables this action based on the history of the currently active FolderPanel: if there is a previous * folder in the history and the current tab is not locked, this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(mainFrame.getActivePanel().getFolderHistory().hasBackFolder() && !mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoBack"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoBackAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoForwardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action recalls the next folder in the current FolderPanel's history. * * @author Maxence Bernard */ public class GoForwardAction extends ActiveTabAction { private GoForwardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getFolderHistory().goForward(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } /** * Enables or disables this action based on the history of the currently active FolderPanel: if there is a next * folder in the history and the current tab is not locked, this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(mainFrame.getActivePanel().getFolderHistory().hasForwardFolder() && !mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoForward"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoForwardAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToDocumentationAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Map; /** * Opens the muCommander online documentation in the system's default browser. The {@link #TOPIC_PROPERTY_KEY} * property allows to specify a specific documentation topic which the browser will be sent to. If it is not defined, * the base documentation URL will be opened. * * @author Maxence Bernard */ public class GoToDocumentationAction extends OpenURLInBrowserAction implements PropertyChangeListener { /** Key to the topic property */ public final static String TOPIC_PROPERTY_KEY = "topic"; public GoToDocumentationAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setIcon(IconManager.getIcon(IconManager.IconSet.COMMON, "help.png")); // Set the URL updateURL(); // Listen to changes made to the topic property addPropertyChangeListener(this); } /** * Sets the URL to sent the browser to, using the base URL defined in the runtime constants and * the optional topic defined in the {@link #TOPIC_PROPERTY_KEY}. The URL is stored in the {@link #URL_PROPERTY_KEY} * property. */ private void updateURL() { String url = com.mucommander.RuntimeConstants.DOCUMENTATION_URL; String topic = (String)getValue(TOPIC_PROPERTY_KEY); // If there is a topic, append it to the URL if (topic != null) { if (url.endsWith("/")) { url += "/"; } url += topic; } putValue(URL_PROPERTY_KEY, url); } /////////////////////////////////////////// // PropertyChangeListener implementation // /////////////////////////////////////////// public void propertyChange(PropertyChangeEvent propertyChangeEvent) { if (propertyChangeEvent.getPropertyName().equals(TOPIC_PROPERTY_KEY)) { updateURL(); } } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToDocumentation"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToDocumentationAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToForumsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action opens the mucommander.com forums URL in the system's default browser. * * @author Maxence Bernard */ public class GoToForumsAction extends OpenURLInBrowserAction { private GoToForumsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); putValue(URL_PROPERTY_KEY, com.mucommander.RuntimeConstants.FORUMS_URL); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToForums"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToForumsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToHomeAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action changes the current folder of the currently active FolderPanel to the user home folder. * * @author Maxence Bernard */ public class GoToHomeAction extends ActiveTabAction { private GoToHomeAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * current tab is not locked, this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } @Override public void performAction() { // Changes the current folder to make it the user home folder AbstractFile homeFolder = LocalFile.getUserHome(); if(homeFolder!=null) mainFrame.getActivePanel().tryChangeCurrentFolder(homeFolder); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToHome"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToHomeAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToParentAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action changes the current folder of the currently active FolderPanel to the current folder's parent. * This action only gets enabled when the current folder has a parent and current tab is not locked. * * @author Maxence Bernard, Nicolas Rinaudo */ public class GoToParentAction extends ActiveTabAction { /** * Creates a new GoToParentAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private GoToParentAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * has a parent and current tab is not locked, this action will be enabled, * if not it will be disabled. */ @Override protected void toggleEnabledState() { FolderPanel activePanel = mainFrame.getActivePanel(); boolean isLocked = activePanel.getTabs().getCurrentTab().isLocked(); AbstractFile currentFolder = activePanel.getCurrentFolder(); setEnabled(!isLocked && currentFolder != null && currentFolder.getParent() != null); } /** * Updates panel's location to its parent. * * @param panel in which to change the location. * @return true if panel has a parent, false otherwise. */ private boolean goToParent(FolderPanel panel) { AbstractFile parent = panel.getCurrentFolder().getParent(); if (parent != null) { panel.tryChangeCurrentFolder(parent, null, true); return true; } return false; } /** * Goes to the current location's parent in the active panel. */ @Override public void performAction() { // Changes the current folder to make it the current folder's parent. // Does nothing if the current folder doesn't have a parent. goToParent(mainFrame.getActivePanel()); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToParent"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToParentAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToParentInBothPanelsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Changes the current directory to its parent and tries to do the same in the inactive panel. *

    * When possible, this action will open the active panel's current folder's parent. Additionally, * if the inactive panel's current folder has a parent, it will open that one as well. *

    * Note that this action's behavior is strictly equivalent to that of {@link GoToParentAction} in the * active panel. Differences will only occur in the inactive panel, and then again only when possible. *

    * This action opens both files synchronously: it will wait for the active panel location change confirmation * before performing the inactive one. * * @author Nicolas Rinaudo */ public class GoToParentInBothPanelsAction extends ActiveTabAction { /** * Creates a new GoToParentInBothPanelsAction instance with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private GoToParentInBothPanelsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Perform this action in a separate thread, to avoid locking the event thread setPerformActionInSeparateThread(true); } /** * Enables or disables this action based on the currently active folder's * has a parent and both tabs in the two panel are not locked, * this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { AbstractFile currentFolder = mainFrame.getActivePanel().getCurrentFolder(); setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked() && !mainFrame.getInactivePanel().getTabs().getCurrentTab().isLocked() && currentFolder != null && currentFolder.getParent() != null); } /** * Opens both the active and inactive folder panel's parent directories. */ @Override public void performAction() { // If the current panel has a parent file, navigate to it. AbstractFile parent = mainFrame.getActivePanel().getCurrentFolder().getParent(); if (parent != null) { Thread openThread = mainFrame.getActivePanel().tryChangeCurrentFolder(parent); // If the inactive panel has a parent file, wait for the current panel change to be complete and navigate to it. parent = mainFrame.getInactivePanel().getCurrentFolder().getParent(); if (parent != null) { if (openThread != null) { while (openThread.isAlive()) { try { openThread.join(); } catch(InterruptedException ignore) {} } } mainFrame.getInactivePanel().tryChangeCurrentFolder(parent); } } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToParentInBothPanels"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, KeyEvent.SHIFT_DOWN_MASK| CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToParentInBothPanelsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToParentInOtherPanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Opens the active panel's parent in the inactive panel. *

    * This action is only enabled when the active panel has a parent, * and the selected tab in the other panel is not locked. * * @author Nicolas Rinaudo */ public class GoToParentInOtherPanelAction extends ParentFolderAction { /** * Creates a new GoToParentInOtherPanelAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private GoToParentInOtherPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Goes to sourcePanel's parent in destPanel. *

    * If sourcePanel doesn't have a parent, nothing will happen. * * @param sourcePanel panel whose parent should be used. * @param destPanel panel in which to change the location. * @return true if sourcePanel has a parent, false otherwise. */ private boolean goToParent(FolderPanel sourcePanel, FolderPanel destPanel) { AbstractFile parent = sourcePanel.getCurrentFolder().getParent(); if (parent != null) { destPanel.tryChangeCurrentFolder(parent, null, true); return true; } return false; } /** * Enables or disables this action based on the currently active folder's * has a parent and selected tab in the other panel is not locked, * this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { AbstractFile currentFolder = mainFrame.getActivePanel().getCurrentFolder(); setEnabled(!mainFrame.getInactivePanel().getTabs().getCurrentTab().isLocked() && currentFolder != null && currentFolder.getParent() != null); } /** * Opens the active panel's parent in the inactive panel. */ @Override public void performAction() { goToParent(mainFrame.getActivePanel(), mainFrame.getInactivePanel()); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToParentInOtherPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToParentInOtherPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToRootAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action changes the current folder of the currently active FolderPanel to the current folder's root. * This action only gets enabled when the current folder has a parent. * * @author Maxence Bernard */ public class GoToRootAction extends ActiveTabAction { private GoToRootAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * has a parent and current tab is not locked, this action will be enabled, * if not it will be disabled. */ @Override protected void toggleEnabledState() { AbstractFile currentFolder = mainFrame.getActivePanel().getCurrentFolder(); setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked() && currentFolder != null && currentFolder.getParent() != null); } @Override public void performAction() { // Changes the current folder to make it the current folder's root folder. // Does nothing if the current folder already is the root. FolderPanel folderPanel = mainFrame.getActivePanel(); AbstractFile currentFolder = folderPanel.getCurrentFolder(); folderPanel.tryChangeCurrentFolder(currentFolder.getRoot()); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToRoot"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SLASH, KeyEvent.CTRL_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToRootAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/GoToWebsiteAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action opens the mucommander.com URL in the system's default browser. * * @author Maxence Bernard */ public class GoToWebsiteAction extends OpenURLInBrowserAction { private GoToWebsiteAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); putValue(URL_PROPERTY_KEY, com.mucommander.RuntimeConstants.HOMEPAGE_URL); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "GoToWebsite"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new GoToWebsiteAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/InternalEditAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.command.Command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.file.ChangePermissionsDialog; import com.mucommander.ui.dialog.symlink.EditSymlinkDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.viewer.EditorRegistrar; import javax.swing.*; import java.util.Map; /** * Opens the current file in edit mode. * @author Maxence Bernard, Nicolas Rinaudo */ public class InternalEditAction extends AbstractViewerAction { /** * Creates a new instance of EditAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ InternalEditAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Edit requires being able to write the file (in addition to view requirements) setSelectedFileFilter(new AndFileFilter( new FileOperationFilter(FileOperation.WRITE_FILE), getSelectedFileFilter() )); ImageIcon icon = getStandardIcon(EditAction.class); if (icon != null) { setIcon(icon); } } /** * Opens the internal editor on the specified file. * @param file file to edit. */ @Override protected void performInternalAction(AbstractFile file) { if (file.isSymlink() && file.getURL().getScheme().equals(FileProtocols.FILE)) { new EditSymlinkDialog(mainFrame.getJFrame(), file).showDialog(); } else if (file.isDirectory()) { FileSet fileSet = new FileSet(); fileSet.add(file); new ChangePermissionsDialog(mainFrame, fileSet).showDialog(); } else { EditorRegistrar.createEditorFrame(mainFrame, file, getIcon().getImage()); } } @Override protected Command getCustomCommand(AbstractFile file) { return null; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "InternalEdit"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new InternalEditAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/InternalViewAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.command.Command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import com.mucommander.ui.viewer.ViewerRegistrar; import javax.swing.ImageIcon; import javax.swing.KeyStroke; import java.util.Map; /** * Opens the current file in view mode. * @author Maxence Bernard, Nicolas Rinaudo */ public class InternalViewAction extends AbstractViewerAction { // - Initialization ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Creates a new instance of InternalViewAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ InternalViewAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); ImageIcon icon = getStandardIcon(ViewAction.class); if (icon != null) { setIcon(icon); } } // - AbstractViewerAction implementation --------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- @Override protected void performInternalAction(AbstractFile file) { if (file.isDirectory()) { FileTable activeTable = mainFrame.getActiveTable(); BaseFileTableModel fileTableModel = (BaseFileTableModel)activeTable.getModel(); fileTableModel.startDirectorySizeCalculation(activeTable, file); } else { ViewerRegistrar.createViewerFrame(mainFrame, file, getIcon().getImage()); } } @Override protected Command getCustomCommand(AbstractFile file) { return null; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "InternalView"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new InternalViewAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/InvertSelectionAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action . * * @author Maxence Bernard */ public class InvertSelectionAction extends TcAction { private InvertSelectionAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable fileTable = mainFrame.getActiveTable(); BaseFileTableModel tableModel = fileTable.getFileTableModel(); // Starts at 1 if current folder is not root so that '..' is not marked int nbFiles = fileTable.getFilesCount(); for (int i = 0; i < nbFiles; i++) { AbstractFile file = tableModel.getFileAt(i); if (!file.isDirectory()) { tableModel.setFileMarked(i, !tableModel.isFileMarked(i)); } } fileTable.repaint(); // Notify registered listeners that currently marked files have changed on the FileTable fileTable.fireMarkedFilesChangedEvent(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "InvertSelection"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_MULTIPLY, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new InvertSelectionAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/LeftArrowAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.TableViewMode; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * @author Oleg Trifonov * Created on 27/10/16. */ public class LeftArrowAction extends TcAction { public LeftArrowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable table = mainFrame.getActiveTable(); if (table == null) { return; } if (table.getViewMode() != TableViewMode.FULL) { return; } if (table.getSelectedFileIndex() == 0) { FolderPanel panel = mainFrame.getActivePanel(); AbstractFile parent = panel.getCurrentFolder().getParent(); if (parent != null) { panel.tryChangeCurrentFolder(parent, null, true); } } else { table.selectFile(0); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "LeftArrowAction"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new LeftArrowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/LocalCopyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.file.LocalCopyDialog; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action invokes the 'Copy dialog' which allows to copy the currently selected/marked files to a specified destination. * The only difference with {@link com.mucommander.ui.action.impl.CopyAction} is that if a single file is selected, * the destination will be preset to the selected file's name so that it can easily be copied to a similar filename in * the current directory. * * @author Maxence Bernard */ public class LocalCopyAction extends SelectedFileAction { private LocalCopyAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new AndFileFilter( new FileOperationFilter(FileOperation.READ_FILE), new FileOperationFilter(FileOperation.WRITE_FILE) )); } @Override public void performAction() { FolderPanel activePanel = mainFrame.getActivePanel(); AbstractFile selectedFile = activePanel.getFileTable().getSelectedFile(false, true); // Display local copy dialog only if a file other than '..' is currently selected if (selectedFile != null) { new LocalCopyDialog(mainFrame, new FileSet(activePanel.getCurrentFolder(), selectedFile)).showDialog(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "LocalCopy"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return null; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F5, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new LocalCopyAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/LocateSymlinkAction.java ================================================ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * * @author Oleg Trifonov */ public class LocateSymlinkAction extends SelectedFilesAction { private LocateSymlinkAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new AttributeFileFilter(AttributeFileFilter.FileAttribute.SYMLINK)); } @Override public void performAction(FileSet files) { AbstractFile link = mainFrame.getActiveTable().getSelectedFile(); AbstractFile target = link.getCanonicalFile(); mainFrame.getInactivePanel().tryChangeCurrentFolder(target.getParent(), target, false); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "LocateSymlink"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new LocateSymlinkAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkAllAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action marks all files in the current file table. * * @author Maxence Bernard */ public class MarkAllAction extends TcAction { private final boolean mark; MarkAllAction(MainFrame mainFrame, Map properties, boolean mark) { super(mainFrame, properties); this.mark = mark; } private MarkAllAction(MainFrame mainFrame, Map properties) { this(mainFrame, properties, true); } @Override public void performAction() { FileTable fileTable = mainFrame.getActiveTable(); BaseFileTableModel tableModel = fileTable.getFileTableModel(); int nbFiles = tableModel.getFilesCount(); for (int i=tableModel.getFirstMarkableIndex(); i < nbFiles; i++) { tableModel.setFileMarked(i, mark); } fileTable.repaint(); // Notify registered listeners that currently marked files have changed on the FileTable fileTable.fireMarkedFilesChangedEvent(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkAll"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_A, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkAllAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkBackwardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * Marks/Unmarks a range of file/rows in the active {@link FileTable}, from the currently selected row to the * the previous {@link #getRowDecrement()} ones. * The row immediately before the last marked/unmarked row will become the currently selected row. * * @author Maxence Bernard */ public abstract class MarkBackwardAction extends TcAction { MarkBackwardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } ///////////////////////////// // MuAction implementation // ///////////////////////////// @Override public void performAction() { FileTable fileTable = mainFrame.getActiveTable(); int currentFileIndex = fileTable.getSelectedFileIndex(); int endIndex = Math.max(0, currentFileIndex-getRowDecrement()+1); // int currentRow = fileTable.getSelectedRow(); // int endRow = Math.max(0, currentRow-getRowDecrement()+1); fileTable.setRangeMarked(currentFileIndex, endIndex, !fileTable.getFileTableModel().isFileMarked(currentFileIndex)); fileTable.selectFile(Math.max(0, endIndex - 1)); } ////////////////////// // Abstract methods // ////////////////////// /** * Returns the number of rows to mark/unmark. * * @return the number of rows to mark/unmark. */ protected abstract int getRowDecrement(); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkEmptyFilesAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.filter.EmptyFileFilter; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.*; import java.util.Map; public class MarkEmptyFilesAction extends TcAction { private MarkEmptyFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable fileTable = mainFrame.getActiveTable(); BaseFileTableModel tableModel = fileTable.getFileTableModel(); tableModel.setFilesMarked(new EmptyFileFilter(), true); // Notify registered listeners that currently marked files have changed on this FileTable fileTable.fireMarkedFilesChangedEvent(); fileTable.repaint(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkEmpty"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkEmptyFilesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkExtensionAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.AbstractFilenameFilter; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Action that marks / unmarks all files with a specific extension. *

    * Marking behaves as follows: *

      *
    • * If the current selection is marked, all files whose extension matches that of the current selection will * be unmarked. *
    • *
    • * If the current selection isn't marked, all files whose extension matches that of the current selection will * be marked. *
    • *
    * *

    * By default, this action will mark all files whose extension match that of the current selection in a case-insensitive fashion. * It can, however, be configured: *

      *
    • * If the extension property is set, its value prepended by a . is always going to be used regardless of the * current selection. *
    • *
    • * If the case_sensitive property is set to true, extension matching will be done in a case sensitive fashion. *
    • *
    * * @author Nicolas Rinaudo */ public class MarkExtensionAction extends TcAction { // - Property names ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** Key that controls which extension should be matched. */ private static final String EXTENSION_PROPERTY_KEY = "extension"; /** Key that controls whether extension matching should be done in a case sensitive fashion (defaults to false). */ private static final String CASE_SENSITIVE_PROPERTY_KEY = "case_sensitive"; // - Initialization ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Creates a new MarkExtensionAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private MarkExtensionAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } // - Properties retrieval ------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Returns the extension that was configured in the action's properties. * @return the extension that was configured in the action's properties, null if none. */ private String getExtension() { Object o = getValue(EXTENSION_PROPERTY_KEY); // If the key wasn't set, return null. if (o == null) { return null; } // If the value is a string, return it. if (o instanceof String) { return (String) o; } // Otherwise, return null. return null; } /** * Returns true if the action must compare string in a case-sensitive fashion. * @return true if the action must compare string in a case-sensitive fashion, false otherwise. */ private boolean isCaseSensitive() { Object o = getValue(CASE_SENSITIVE_PROPERTY_KEY); // If the action hasn't been configured, defaults to false. if (o == null) { return false; } // Returns the configured value if it's a string, false otherwise. return o instanceof String && o.equals("true"); } // - Action code --------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Creates a {@link com.mucommander.commons.file.filter.FilenameFilter} that should be applied to all current files. *

    * If the action has been configured using the file.extension property, the returned IMAGE_FILTER * will match that extension. Otherwise, the currently selected file's extension will be used. If it doesn't * have one, the returned IMAGE_FILTER will match all files such that * file.getExtension() == null. * * @param file currently selected file. * @return the IMAGE_FILTER that should be applied by this action. */ private FilenameFilter getFilter(AbstractFile file) { String ext = getExtension(); // If no extension has been configured, analyse the current selection. if (ext == null) { // If there is no current selection, abort. if (file == null) { return null; } // If the current file doesn't have an extension, return a filename IMAGE_FILTER that // match null extensions. ext = file.getExtension(); if (ext == null) { return new AbstractFilenameFilter() { public boolean accept(String name) { return AbstractFile.getExtension(name) == null; } }; } } // At this point, ext contains the extension that should be matched. ExtensionFilenameFilter filter = new ExtensionFilenameFilter("." + ext); // Initializes the IMAGE_FILTER's case-sensitive depending on the action's properties. filter.setCaseSensitive(isCaseSensitive()); return filter; } /** * Marks all files whose extension matches the current selection. */ @Override public void performAction() { // Initialization. Aborts if there is no selected file. FileTable fileTable = mainFrame.getActiveTable(); FilenameFilter filter = getFilter(fileTable.getSelectedFile(false, true)); if (filter == null) { return; } BaseFileTableModel tableModel = fileTable.getFileTableModel(); int filesCount = tableModel.getFilesCount(); boolean mark = !tableModel.isFileMarked(fileTable.getSelectedFileIndex()); // Goes through all files in the active table, marking all that match 'IMAGE_FILTER'. for (int i = tableModel.getFirstMarkableIndex(); i < filesCount; i++) { if (filter.accept(tableModel.getCachedFileAt(i))) { tableModel.setFileMarked(i, mark); } } fileTable.repaint(); // Notify registered listeners that currently marked files have changed on the FileTable fileTable.fireMarkedFilesChangedEvent(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkExtension"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ADD, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkExtensionAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkForwardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * Marks/Unmarks a range of file/rows in the active {@link FileTable}, from the currently selected row to the * the next {@link #getRowIncrement()} ones. * The row immediately after the last marked/unmarked row will become the currently selected row. * * @author Maxence Bernard */ public abstract class MarkForwardAction extends TcAction { MarkForwardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable fileTable = mainFrame.getActiveTable(); int currentFileIndex = fileTable.getSelectedFileIndex(); int lastIndex = fileTable.getFilesCount()-1; // int currentRow = fileTable.getSelectedRow(); // int lastRow = fileTable.getRowCount()-1; int endIndex = Math.min(lastIndex, currentFileIndex + getRowIncrement() - 1); fileTable.setRangeMarked(currentFileIndex, endIndex, !fileTable.getFileTableModel().isFileMarked(currentFileIndex)); fileTable.selectFile(Math.min(lastIndex, endIndex + 1)); } /** * Returns the number of rows to mark/unmark. * * @return the number of rows to mark/unmark. */ protected abstract int getRowIncrement(); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkGroupAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.FileSelectionDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings up the 'File selection' dialog which allows to mark a group of files that match a specified expression. * * @author Maxence Bernard */ @InvokesDialog public class MarkGroupAction extends TcAction { private MarkGroupAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new FileSelectionDialog(mainFrame, true).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkGroup"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkGroupAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkNextBlockAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks the previous block's rows in the current {@link FileTable}, starting with the * current row, and moves the selected row right before the last marked/unmarked row. * * @author Maxence Bernard */ public class MarkNextBlockAction extends MarkForwardAction { /** Number of file/rows a block represents */ // TODO: make this value configurable private static final int BLOCK_SIZE = 5; private MarkNextBlockAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { return BLOCK_SIZE; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkNextBlock"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkNextBlockAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkNextPageAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks the next page's rows in the current {@link FileTable}, starting with the * current row, and moves the selected row right after the last marked/unmarked row. * * @author Maxence Bernard */ public class MarkNextPageAction extends MarkForwardAction { private MarkNextPageAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { // Note: the page row increment varies with the file table's height return mainFrame.getActiveTable().getPageRowIncrement()+1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkNextPage"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkNextPageAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkNextRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks the current row and moves the selection to the next row. * * @author Maxence Bernard */ public class MarkNextRowAction extends MarkForwardAction { private MarkNextRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { return 1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkNextRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkNextRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkPreviousBlockAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks the previous block's rows in the current {@link FileTable}, starting with the * current row, and moves the selected row right before the last marked/unmarked row. * * @author Maxence Bernard */ public class MarkPreviousBlockAction extends MarkBackwardAction { /** Number of file/rows a block represents */ // TODO: make this value configurable private static final int BLOCK_SIZE = 5; private MarkPreviousBlockAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { return BLOCK_SIZE; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkPreviousBlock"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkPreviousBlockAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkPreviousPageAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks the previous page's rows in the current {@link FileTable}, starting with the * current row, and moves the selected row right before the last marked/unmarked row. * * @author Maxence Bernard */ public class MarkPreviousPageAction extends MarkBackwardAction { private MarkPreviousPageAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { // Note: the page row increment varies with the file table's height return mainFrame.getActiveTable().getPageRowIncrement()+1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkPreviousPage"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkPreviousPageAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkPreviousRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks the current row and moves the selection to the previous row. * * @author Maxence Bernard */ public class MarkPreviousRowAction extends MarkBackwardAction { private MarkPreviousRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { return 1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkPreviousRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkPreviousRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkSelectedFileAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.full.FileTableModel; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks or unmarks the current selected file (current row) and advance current row to the next one, * with the following exceptions: *

      *
    • if quick search is active, this method does nothing *
    • if '..' file is selected, file is not marked but current row is still advanced to the next one *
    • if the {@link com.mucommander.ui.action.impl.MarkSelectedFileAction} key event is repeated and the last file has already * been marked/unmarked since the key was last released, the file is not marked in order to avoid * marked/unmarked flaps when the mark key is kept pressed. *
    * * @author Maxence Bernard */ public class MarkSelectedFileAction extends TcAction { private MarkSelectedFileAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { if (TcConfigurations.getPreferences().getVariable(TcPreference.CALCULATE_FOLDER_SIZE_ON_MARK, TcPreferences.DEFAULT_CALCULATE_FOLDER_SIZE_ON_MARK)) { calculateFolderSize(); } mainFrame.getActiveTable().markSelectedFile(); } private void calculateFolderSize() { FileTable table = mainFrame.getActiveTable(); AbstractFile file = table.getSelectedFile(); if (file != null && file.isDirectory()) { FileTableModel model = (FileTableModel) mainFrame.getActiveTable().getModel(); model.startDirectorySizeCalculation(table, table.getSelectedFile()); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkSelectedFile"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkSelectedFileAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkToFirstRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks files in the active FileTable, from the currently selected row to the first row (inclusive). * The first row will also become the currently selected row. * *

    The currently selected row's marked state determines whether the rows will be marked or unmarked : if the selected * row is marked, the rows will be unmarked and vice versa. * * @author Maxence Bernard */ public class MarkToFirstRowAction extends MarkBackwardAction { private MarkToFirstRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { return mainFrame.getActiveTable().getSelectedRow(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkToFirstRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_HOME, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkToFirstRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MarkToLastRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Marks/unmarks files in the active FileTable, from the currently selected row to the last row (inclusive). * The last row will also become the currently selected row. * *

    The currently selected row's marked state determines whether the rows will be marked or unmarked : if the selected * row is marked, the rows will be unmarked and vice-versa. * * @author Maxence Bernard */ public class MarkToLastRowAction extends MarkForwardAction { private MarkToLastRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { FileTable activeTable = mainFrame.getActiveTable(); return activeTable.getRowCount()-activeTable.getSelectedRow(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MarkToLastRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_END, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MarkToLastRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MaximizeWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.JFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Maximizes the {@link MainFrame} this action is associated with. * * @author Maxence Bernard * @see com.mucommander.ui.action.impl.MinimizeWindowAction */ public class MaximizeWindowAction extends TcAction { private MaximizeWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getJFrame().setExtendedState(JFrame.MAXIMIZED_BOTH); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MaximizeWindow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public String getLabel() { // Use a special label for Mac OS X, if it exists, use the standard action label otherwise String macLabelKey = ActionProperties.getActionLabelKey(ACTION_ID)+".mac_os_x"; if (OsFamily.MAC_OS_X.isCurrent() && Translator.hasValue(macLabelKey, false)) { return Translator.get(macLabelKey); } return super.getLabel(); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MaximizeWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MinimizeWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.JFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Minimizes the {@link MainFrame} this action is associated with. * * @author Maxence Bernard * @see com.mucommander.ui.action.impl.MaximizeWindowAction */ public class MinimizeWindowAction extends TcAction { private MinimizeWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getJFrame().setExtendedState(JFrame.ICONIFIED); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MinimizeWindow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_M, KeyEvent.META_DOWN_MASK); } @Override public String getLabel() { // Use a special label for Mac OS X, if it exists, use the standard action label otherwise String macLabelKey = ActionProperties.getActionLabelKey(ACTION_ID)+".mac_os_x"; if (OsFamily.MAC_OS_X.isCurrent() && Translator.hasValue(macLabelKey, false)) { return Translator.get(macLabelKey); } return super.getLabel(); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MinimizeWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MkdirAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.MakeDirectoryFileDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings up the 'Make directory' dialog which allows to create a new directory in the currently active * folder. * * @author Maxence Bernard */ public class MkdirAction extends ParentFolderAction { private MkdirAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected void toggleEnabledState() { AbstractFile firstFile = mainFrame.getActiveTable().getFileTableModel().getFileAt(0); // If there is no file at all, do not rely on the action being supported by the current folder as this // would be incorrect for some filesystems which do not support operations consistently across the // filesystem (e.g. S3). In that case, err on the safe side and enable the action, even if the operation // end up not being supported. setEnabled(firstFile == null || firstFile.isFileOperationSupported(FileOperation.CREATE_DIRECTORY)); } @Override public void performAction() { new MakeDirectoryFileDialog(mainFrame, false).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Mkdir"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MkdirAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MkfileAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.file.MakeDirectoryFileDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings up the 'Make file' dialog which allows to create a new file in the currently active folder. * * @author Maxence Bernard */ public class MkfileAction extends ParentFolderAction { private MkfileAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } protected void toggleEnabledState() { AbstractFile firstFile = mainFrame.getActiveTable().getFileTableModel().getFileAt(0); // If there is no file at all, do not rely on the action being supported by the current folder as this // would be incorrect for some filesystems which do not support operations consistently across the // filesystem (e.g. S3). In that case, err on the safe side and enable the action, even if the operation // end up not being supported. setEnabled(firstFile == null || firstFile.isFileOperationSupported(FileOperation.WRITE_FILE)); } @Override public void performAction() { new MakeDirectoryFileDialog(mainFrame, true).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Mkfile"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F7, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MkfileAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MoveAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.filter.OrFileFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.MoveDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action invokes the 'Move dialog' which allows to move the currently selected/marked files * in the current folder to a specified destination. * * @author Maxence Bernard */ public class MoveAction extends SelectedFilesAction { private MoveAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new OrFileFilter( new FileOperationFilter(FileOperation.RENAME), new AndFileFilter( new FileOperationFilter(FileOperation.READ_FILE), new FileOperationFilter(FileOperation.WRITE_FILE) ) )); } @Override public void performAction(FileSet files) { new MoveDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Move"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MoveAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MoveTabToOtherPanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.tabs.FileTableTab; import com.mucommander.ui.main.tabs.FileTableTabs; import javax.swing.KeyStroke; import java.util.Map; /** * Close current tab and open the same tab at the other FolderPanel * * @author Arik Hadas */ public class MoveTabToOtherPanelAction extends ActiveTabAction { private MoveTabToOtherPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Enables or disables this action based on the currently active folder's * current tab is not locked and is not the only tab in the panel, * this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { FileTableTabs tabs = mainFrame.getActivePanel().getTabs(); setEnabled(!tabs.getCurrentTab().isLocked() && tabs.getTabsCount() > 1); } @Override public void performAction() { FileTableTab tab = mainFrame.getActivePanel().getTabs().closeCurrentTab(); mainFrame.getInactivePanel().getTabs().add(tab); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "MoveTabToOtherPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new MoveTabToOtherPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/MuteProxyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import javax.swing.*; import java.awt.event.ActionEvent; /** * MuteProxyAction is an implementation of {@link ProxyAction} where {@link #actionPerformed(java.awt.event.ActionEvent)} * does nothing. * * @author Maxence Bernard */ public class MuteProxyAction extends ProxyAction { public MuteProxyAction(Action proxiedAction) { super(proxiedAction); } /** * This method is a No-op, i.e. does absolutely nothing. */ public void actionPerformed(ActionEvent actionEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/NewWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.main.frame.ClonedMainFrameBuilder; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action creates a new muCommander window. * * @author Maxence Bernard */ public class NewWindowAction extends TcAction { private NewWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // This action must be performed in a separate thread as it will otherwise lock the AWT event thread setPerformActionInSeparateThread(true); } @Override public void performAction() { WindowManager.createNewMainFrame(new ClonedMainFrameBuilder()); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "NewWindow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_N, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new NewWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/NextTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Change the selected tab to the tab which is located to the right of the current displayed tab. * If the current displayed tab is the rightmost tab, the leftmost tab will be displayed. * * @author Arik Hadas */ public class NextTabAction extends TcAction { private NextTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getTabs().nextTab(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "NextTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, CTRL_OR_META_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new NextTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.TempExecJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.quicklist.RecentExecutedFilesQL; import com.mucommander.ui.main.tabs.FileTableTabs; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.io.IOException; import java.net.MalformedURLException; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * This action 'opens' the currently selected file or folder in the active FileTable. * This means different things depending on the kind of file that is currently selected: *

      *
    • For browsable files (directory, archive...): shows file contents *
    • For local file that are not an archive or archive entry: executes file with native file associations *
    • For any other file type, remote or local: copies file to a temporary local file and executes it with native file associations *
    * * @author Maxence Bernard, Nicolas Rinaudo */ public class OpenAction extends TcAction { /** * Creates a new OpenAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ OpenAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Opens the specified file in the specified folder panel. *

    * file will be opened using the following rules: *

      *
    • * If file is {@link com.mucommander.commons.file.AbstractFile#isBrowsable() browsable}, * it will be opened in destination. *
    • *
    • * If file is local, it will be opened using its native associations. *
    • *
    • * If file is remote, it will first be copied in a temporary local file and * then opened using its native association. *
    • *
    * * @param file file to open. * @param destination if file is browsable, folder panel in which to open the file. */ protected void open(final AbstractFile file, FolderPanel destination) { AbstractFile resolvedFile = resolveIfSymlink(file); if (resolvedFile == null) { return; } if (resolvedFile.isBrowsable()) { // Opens browsable files in the destination FolderPanel. resolvedFile = cdFollowsSymlinks() ? resolvedFile : file; openBrowsable(resolvedFile, destination); } else if (resolvedFile.isLocalFile()) { // Opens local files using their native associations. openLocalFile(resolvedFile); } else { // Copies non-local file in a temporary local file and opens them using their native association. ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); TempExecJob job = new TempExecJob(progressDialog, mainFrame, resolvedFile); progressDialog.start(job); } } private void openLocalFile(AbstractFile file) { try { DesktopManager.open(file); RecentExecutedFilesQL.addFile(file); } catch (IOException e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } AbstractFile resolveIfSymlink(AbstractFile file) { if (!file.isSymlink()) { return file; } AbstractFile resolvedFile = resolveSymlink(file); if (resolvedFile == null) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), Translator.get("cannot_open_cyclic_symlink")); return null; } return resolvedFile; } private void openBrowsable(AbstractFile file, FolderPanel destination) { if (BookmarkManager.isBookmark(file)) { openBookmark(file, destination); } else { FileTableTabs tabs = destination.getTabs(); if (tabs.getCurrentTab().isLocked()) { tabs.add(file); } else { destination.tryChangeCurrentFolder(file); } } } private void openBookmark(AbstractFile file, FolderPanel destination) { Bookmark bookmark = BookmarkManager.getBookmark(file.getName()); if (bookmark == null) { return; } String bookmarkLocation = bookmark.getLocation(); try { FileURL bookmarkURL = FileURL.getFileURL(bookmarkLocation); FileTableTabs tabs = destination.getTabs(); if (tabs.getCurrentTab().isLocked()) { tabs.add(bookmarkURL); } else { destination.tryChangeCurrentFolder(bookmarkURL); } } catch (MalformedURLException ignore) {} } private static boolean cdFollowsSymlinks() { return TcConfigurations.getPreferences().getVariable(TcPreference.CD_FOLLOWS_SYMLINKS, TcPreferences.DEFAULT_CD_FOLLOWS_SYMLINKS); } private AbstractFile resolveSymlink(AbstractFile symlink) { return resolveSymlink(symlink, new HashSet<>()); } private AbstractFile resolveSymlink(AbstractFile file, Set visitedFiles) { if (visitedFiles.contains(file)) { return null; } visitedFiles.add(file); return file.isSymlink() ? resolveSymlink(file.getCanonicalFile(), visitedFiles) : file; } /** * Opens the currently selected file in the active folder panel. */ @Override public void performAction() { // Retrieves the currently selected file, aborts if none. // Note: a CachedFile instance is retrieved to avoid blocking the event thread. AbstractFile file = mainFrame.getActiveTable().getSelectedFile(true, true); if (file != null) { open(file, mainFrame.getActivePanel()); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Open"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenAsAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.io.IOException; import java.util.Map; /** * Open a file as if it has the specified file extension. */ public class OpenAsAction extends OpenAction { private String extension; private OpenAsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); extension = (String) properties.get("extension"); } /** * Opens the currently selected file in the active folder panel. */ @Override public void performAction() { // Retrieves the currently selected file, // Note: a CachedFile instance is retrieved to avoid blocking the event thread. AbstractFile file = mainFrame.getActiveTable().getSelectedFile(true, true); if (file == null) { return; } AbstractFile resolvedFile = resolveIfSymlink(file); try { resolvedFile = FileFactory.wrapArchive(resolvedFile, extension); open(resolvedFile, mainFrame.getActivePanel()); } catch (IOException ignore) {} } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenAs"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenAsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenInBothPanelsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.*; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import org.jetbrains.annotations.Nullable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Opens the currently selected file and its equivalent in the inactive folder panel if it exists. *

    * This action will analyse the current selection and, if applicable, any file from the inactive * panel that bears the same name and: *

      *
    • * If both the selection and its inactive equivalent are browsable, both will be explored in their * respective panels. *
    • *
    • * If both are non-browsable, both will be opened as defined in {@link OpenAction}. *
    • *
    • * If one is browsable an not the other one, only the current selection will be opened. *
    • *
    * *

    * Note that this action's behavior is strictly equivalent to that of {@link OpenAction} in the * active panel. Differences will only occur in the inactive panel, and then again only when possible. * *

    * This action opens both files synchronously: it will wait for the active panel file to have been * opened before opening the inactive panel one. * * @author Nicolas Rinaudo */ public class OpenInBothPanelsAction extends SelectedFileAction { /** * Creates a new OpenInBothPanelsAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private OpenInBothPanelsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Perform this action in a separate thread, to avoid locking the event thread setPerformActionInSeparateThread(true); } @Override public void activePanelChanged(FolderPanel folderPanel) { super.activePanelChanged(folderPanel); if (mainFrame.getInactivePanel().getTabs().getCurrentTab().isLocked()) { setEnabled(false); } } /** * This method is overridden to enable this action when the parent folder is selected. */ @Override protected boolean getFileTableCondition(FileTable fileTable) { AbstractFile selectedFile = fileTable.getSelectedFile(true, true); return selectedFile != null && selectedFile.isBrowsable(); } // - Action code --------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Opens the current selection and its inactive equivalent. */ @Override public void performAction() { // Retrieves the current selection, aborts if none (should not normally happen). AbstractFile selectedFile = mainFrame.getActiveTable().getSelectedFile(true, true); if (selectedFile == null || !selectedFile.isBrowsable()) { return; } AbstractFile otherFile = findOtherFile(selectedFile); // Opens 'file' in the active panel. Thread openThread = mainFrame.getActivePanel().tryChangeCurrentFolder(selectedFile); // Opens 'otherFile' (if any) in the inactive panel. if (otherFile != null) { // Waits for the previous folder change to be finished. if (openThread != null) { while (openThread.isAlive()) { try { openThread.join(); } catch(InterruptedException ignore) {} } } mainFrame.getInactivePanel().tryChangeCurrentFolder(otherFile); } } @Nullable private AbstractFile findOtherFile(AbstractFile selectedFile) { try { BaseFileTableModel otherTableModel = mainFrame.getInactiveTable().getFileTableModel(); if (mainFrame.getActiveTable().isParentFolderSelected()) { return otherTableModel.getParentFolder(); } else { // Look for a file in the other table with the same name as the selected one (case insensitive) int fileCount = otherTableModel.getFileCount(); String targetFilename = selectedFile.getName(); AbstractFile otherFile = null; for (int i = 0; i < fileCount; i++) { otherFile = otherTableModel.getCachedFileAt(i); if (otherFile.getName().equalsIgnoreCase(targetFilename)) { break; } if (i == fileCount-1) { otherFile = null; } } return otherFile; } } catch (Exception e) { return null; } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenInBothPanels"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_O, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenInBothPanelsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenInNewTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.net.MalformedURLException; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; /** * Opens browsable file in a new tab. *

    * This action is only enabled if the current selection is browsable as defined by * {@link com.mucommander.commons.file.AbstractFile#isBrowsable()}. * * @author Arik Hadas */ public class OpenInNewTabAction extends SelectedFileAction { private OpenInNewTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * This method is overridden to enable this action when the parent folder is selected. */ @Override protected boolean getFileTableCondition(FileTable fileTable) { AbstractFile selectedFile = fileTable.getSelectedFile(true, true); return selectedFile != null && selectedFile.isBrowsable(); } @Override public void performAction() { AbstractFile file = mainFrame.getActiveTable().getSelectedFile(true, true); // Retrieves the currently selected file, aborts if none (should not normally happen). if (file == null || !file.isBrowsable()) { return; } FileURL fileURL = file.getURL(); if (BookmarkManager.isBookmark(fileURL)) { String bookmarkLocation = BookmarkManager.getBookmark(file.getName()).getLocation(); try { fileURL = FileURL.getFileURL(bookmarkLocation); } catch (MalformedURLException e) { return; } } // Opens the currently selected file in a new tab mainFrame.getActivePanel().getTabs().add(fileURL); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenInNewTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenInNewTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenInOtherPanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.*; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Opens browsable files in the inactive panel. *

    * This action is only enabled if the current selection is browsable as defined by * {@link com.mucommander.commons.file.AbstractFile#isBrowsable()}. * * @author Nicolas Rinaudo */ public class OpenInOtherPanelAction extends SelectedFileAction { /** * Creates a new OpenInOtherPanelAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private OpenInOtherPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void activePanelChanged(FolderPanel folderPanel) { super.activePanelChanged(folderPanel); if (mainFrame.getInactivePanel().getTabs().getCurrentTab().isLocked()) setEnabled(false); } /** * This method is overridden to enable this action when the parent folder is selected. */ @Override protected boolean getFileTableCondition(FileTable fileTable) { AbstractFile selectedFile = fileTable.getSelectedFile(true, true); return selectedFile!=null && selectedFile.isBrowsable(); } /** * Opens the currently selected file in the inactive folder panel. */ @Override public void performAction() { AbstractFile file = mainFrame.getActiveTable().getSelectedFile(true, true); // Retrieves the currently selected file, aborts if none (should not normally happen). if (file == null || !file.isBrowsable()) { return; } // Opens the currently selected file in the inactive panel. mainFrame.getInactivePanel().tryChangeCurrentFolder(file); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenInOtherPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_O, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenInOtherPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenLeftInRightPanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.tabs.FileTableTabs; /** * Opens browsable files in the right panel. * *

    * If the left panel is the active panel and the selected file is browsable as defined by * {@link com.mucommander.commons.file.AbstractFile#isBrowsable()} then the selected file will be opened. * *

    * Otherwise the left panel's location will be opened. * *

    * If the right panel is locked then a new tab in the right panel will be opened first. * * @author Martin Kortkamp */ public class OpenLeftInRightPanelAction extends FileAction { /** * Creates a new OpenLeftInRightPanelAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ OpenLeftInRightPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * This action is always enabled. */ @Override protected boolean getFileTableCondition(FileTable fileTable) { return true; } /** * Opens the currently selected file or the current location in right folder panel. */ @Override public void performAction() { AbstractFile destFile = getDestinationFile(); if (destFile.isBrowsable()) { FileTableTabs destTabs = getDestPanel().getTabs(); if (destTabs.getCurrentTab().isLocked()) { destTabs.add(destFile); } else { getDestPanel().tryChangeCurrentFolder(destFile); } } } private AbstractFile getDestinationFile() { AbstractFile destFile = null; FolderPanel srcPanel = getSrcPanel(); if (mainFrame.getActivePanel() == srcPanel) { AbstractFile selectedFile = srcPanel.getFileTable().getSelectedFile(); if (selectedFile != null && selectedFile.isBrowsable()) { destFile = selectedFile; } } if (destFile == null) { destFile = srcPanel.getCurrentFolder(); } return destFile; } protected FolderPanel getDestPanel() { return mainFrame.getRightPanel(); } protected FolderPanel getSrcPanel() { return mainFrame.getLeftPanel(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenLeftInRightPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenLeftInRightPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenLocationAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.Image; import java.util.Map; import com.mucommander.bonjour.BonjourService; import com.mucommander.bookmark.Bookmark; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.impl.BatchRenameAction.Descriptor; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.utils.FileIconsCache; import javax.swing.ImageIcon; /** * This action opens a specified location in the current active FileTable. The location can be designated by either a * FileURL, path, or AbstractFile. * * @author Maxence Bernard */ public class OpenLocationAction extends ActiveTabAction { private FileURL url; private AbstractFile file; private String path; /** * Creates a new OpenLocationAction instance using the provided url's string representation * (with credentials stripped out) as label. * * @param mainFrame main frame * @param properties properties to use in this action * @param url location to open */ public OpenLocationAction(MainFrame mainFrame, Map properties, FileURL url) { this(mainFrame, properties, url, url.getScheme().equals(FileProtocols.FILE) ? url.getPath() : url.toString(false)); } /** * Creates a new OpenLocationAction instance using the provided FileURL and label. * * @param mainFrame main frame * @param properties properties to use in this action * @param url location to open * @param label tooltip label */ private OpenLocationAction(MainFrame mainFrame, Map properties, FileURL url, String label) { super(mainFrame, properties); this.url = url; setLabel(label); boolean isLocalFile = url.getScheme().equals(FileProtocols.FILE); if (isLocalFile) { Image icon = FileIconsCache.getInstance().getImageIcon(FileFactory.getFile(url)); setIcon(new ImageIcon(icon)); } setToolTipText(isLocalFile ? url.getPath() : url.toString(false)); } /** * Creates a new OpenLocationAction instance using the filename of the provided AbstractFile * as label. * * @param mainFrame main frame * @param properties properties to use in this action * @param file location to open */ public OpenLocationAction(MainFrame mainFrame, Map properties, AbstractFile file) { this(mainFrame, properties, file, file.getName()); } /** * Creates a new OpenLocationAction instance using the provided AbstractFile and label. * * @param mainFrame main frame * @param properties properties to use in this action * @param file location to open * @param label tooltip label */ private OpenLocationAction(MainFrame mainFrame, Map properties, AbstractFile file, String label) { super(mainFrame, properties); this.file = file; setLabel(label); setToolTipText(file.getAbsolutePath()); } /** * Creates a new OpenLocationAction instance using the provided path as label. * * @param mainFrame main frame * @param properties properties to use in this action * @param path path to open */ public OpenLocationAction(MainFrame mainFrame, Map properties, String path) { this(mainFrame, properties, path, path); } /** * Creates a new OpenLocationAction instance using the provided path and label. * * @param mainFrame main frame * @param properties properties to use in this action * @param path path to open * @param label tooltip label */ private OpenLocationAction(MainFrame mainFrame, Map properties, String path, String label) { super(mainFrame, properties); this.path = path; setLabel(label); setToolTipText(path); } /** * Convenience constructor, same effect as calling {@link #OpenLocationAction(MainFrame, Map, String, String)} with * {@link Bookmark#getLocation()} and {@link Bookmark#getName()}. * * @param mainFrame main frame * @param properties properties to use in this action * @param bookmark location top open */ public OpenLocationAction(MainFrame mainFrame, Map properties, Bookmark bookmark) { this(mainFrame, properties, bookmark.getLocation(), bookmark.getName()); } /** * Convenience constructor, same effect as calling {@link #OpenLocationAction(MainFrame, Map, FileURL, String)} with * {@link BonjourService#getURL()} and {@link BonjourService#getNameWithProtocol()} ()}. * * @param mainFrame main frame * @param properties properties to use in this action * @param bonjourService location to open */ public OpenLocationAction(MainFrame mainFrame, Map properties, BonjourService bonjourService) { this(mainFrame, properties, bonjourService.getURL(), bonjourService.getNameWithProtocol()); } /** * Returns the {@link FolderPanel} on which to change the current folder. This method returns the currently active * panel but can be overridden if another panel should be used. * * @return the currently active panel */ protected FolderPanel getFolderPanel() { return mainFrame.getActivePanel(); } /** * Enables or disables this action based on the current tab is not locked, * this action will be enabled, if not it will be disabled. */ @Override protected void toggleEnabledState() { setEnabled(!mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } ///////////////////////////// // MuAction implementation // ///////////////////////////// @Override public void performAction() { FolderPanel folderPanel = getFolderPanel(); if (url != null) { folderPanel.tryChangeCurrentFolder(url); } else if (file != null) { folderPanel.tryChangeCurrentFolder(file); } else if (path != null) { folderPanel.tryChangeCurrentFolder(path); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenNativelyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractArchiveEntryFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.TempExecJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.quicklist.RecentExecutedFilesQL; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.Map; /** * This action opens the currently selected file or folder with native file associations. * * @author Maxence Bernard */ public class OpenNativelyAction extends TcAction { private OpenNativelyAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { AbstractFile selectedFile = mainFrame.getActiveTable().getSelectedFile(true, true); if (selectedFile == null) { return; } // Copy file to a temporary local file and execute it with native file associations if // file is not on a local filesystem or file is an archive entry if (!FileProtocols.FILE.equals(selectedFile.getURL().getScheme()) || selectedFile.hasAncestor(AbstractArchiveEntryFile.class)) { ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); TempExecJob job = new TempExecJob(progressDialog, mainFrame, selectedFile); progressDialog.start(job); } else { // Tries to execute file with native file associations try { DesktopManager.open(selectedFile); RecentExecutedFilesQL.addFile(selectedFile); } catch(IOException e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenNatively"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenNativelyAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenRightInLeftPanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Opens browsable files in the left panel. * *

    * If the right panel is the active panel and the selected file is browsable as defined by * {@link com.mucommander.commons.file.AbstractFile#isBrowsable()} then the selected file will be opened. * *

    * Otherwise the right panel's location will be opened. * *

    * If the left panel is locked then a new tab in the left panel will be opened first. * * @author Martin Kortkamp */ public class OpenRightInLeftPanelAction extends OpenLeftInRightPanelAction { /** * Creates a new OpenRightInLeftPanelAction with the specified parameters. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private OpenRightInLeftPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } protected FolderPanel getDestPanel() { return mainFrame.getLeftPanel(); } protected FolderPanel getSrcPanel() { return mainFrame.getRightPanel(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenRightInLeftPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenRightInLeftPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenTrashAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Opens the trash in the default file manager of the current OS/Desktop manager. This action is enabled only * if the current platform has an {@link com.mucommander.desktop.AbstractTrash} implementation and if it is capable * of opening the trash, as reported by {@link com.mucommander.desktop.AbstractTrash#canOpen()}. * * @author Maxence Bernard */ public class OpenTrashAction extends TcAction { private OpenTrashAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); AbstractTrash trash = DesktopManager.getTrash(); setEnabled(trash != null && trash.canOpen()); } @Override public void performAction() { DesktopManager.getTrash().open(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenTrash"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenTrashAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/OpenURLInBrowserAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.net.URL; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.main.MainFrame; /** * This action opens a URL in the system's default browser. This action is enabled only if the OS/Window manager * is capable of doing do. * * @author Maxence Bernard */ public class OpenURLInBrowserAction extends TcAction { /** Key to the URL property */ final static String URL_PROPERTY_KEY = "url"; OpenURLInBrowserAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Enable this action only if the current platform is capable of opening URLs in the default browser. setEnabled(DesktopManager.canBrowse()); } @Override public void performAction() { Object url = getValue(URL_PROPERTY_KEY); if (url instanceof String) { try { DesktopManager.browse(new URL((String)url)); } catch(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "OpenURLInBrowser"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return null; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public boolean isParameterized() { return true; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new OpenURLInBrowserAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PackAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.PackDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the 'Pack files' dialog that allows to create an archive file with the currently marked files. * * @author Maxence Bernard */ @InvokesDialog public class PackAction extends SelectedFilesAction { private PackAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.READ_FILE)); } @Override public void performAction(FileSet files) { new PackDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Pack"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_I, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PackAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ParentFolderAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; /** * This class is an abstract {@link TcAction} that operates on the current folder. It monitors changes in the active * panel's location and calls {@link #toggleEnabledState()} when the location has changed, or when the active panel * itself has changed, in order to enable or disable this action. * * @author Maxence Bernard, Nicolas Rinaudo */ public abstract class ParentFolderAction extends TcAction implements ActivePanelListener, LocationListener { ParentFolderAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Listen to active table change events mainFrame.addActivePanelListener(this); // Listen to location change events mainFrame.getLeftPanel().getLocationManager().addLocationListener(this); mainFrame.getRightPanel().getLocationManager().addLocationListener(this); toggleEnabledState(); } ////////////////////// // Abstract methods // ////////////////////// /** * Enables or disables this action based on the location of the currently active {@link FolderPanel}. * This method is called once by the constructor to set the initial state. Then it is called every time the location * of the currently active FolderPanel has changed, and when the currently active FolderPanel * has changed. */ protected abstract void toggleEnabledState(); ///////////////////////////////// // ActivePanelListener methods // ///////////////////////////////// public void activePanelChanged(FolderPanel folderPanel) { toggleEnabledState(); } /********************************** * LocationListener Implementation **********************************/ public void locationChanged(LocationEvent e) { toggleEnabledState(); } public void locationChanging(LocationEvent locationEvent) { } public void locationCancelled(LocationEvent locationEvent) { } public void locationFailed(LocationEvent locationEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PasteClipboardFilesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.job.CopyJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.dnd.ClipboardNotifier; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pastes the files contained by the system clipboard to the currently active folder. * Does nothing if the clipboard doesn't contain any file. * *

    Under Java 1.5 and up, this action gets automatically enabled/disabled when files are present/not present * in the clipboard. * * @author Maxence Bernard */ public class PasteClipboardFilesAction extends TcAction { private PasteClipboardFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Allows this action to be dynamically enabled when the clipboard contains files, and disabled otherwise. // ClipboardNotifier does not work under Mac OS X (tested under Tiger with Java 1.5.0_06) if (!OsFamily.MAC_OS_X.isCurrent()) { new ClipboardNotifier(this); } } @Override public void performAction() { // Retrieve clipboard files FileSet clipboardFiles = ClipboardSupport.getClipboardFiles(); if (clipboardFiles != null && !clipboardFiles.isEmpty()) { startCopyingFiles(clipboardFiles); } } private void startCopyingFiles(FileSet clipboardFiles) { ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); AbstractFile destFolder = mainFrame.getActivePanel().getCurrentFolder(); CopyJob job = new CopyJob(progressDialog, mainFrame, clipboardFiles, destFolder, null, CopyJob.Mode.COPY, FileCollisionDialog.ASK_ACTION); progressDialog.start(job); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "PasteClipboardFiles"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_V, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PasteClipboardFilesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PasteFromArchiveToFilesFromClipboardAction.java ================================================ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dnd.ClipboardOperations; import com.mucommander.ui.dnd.ClipboardSupport; import com.mucommander.ui.main.MainFrame; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; /** * This action cuts the selected / marked files to the system clipboard, allowing to paste * them to muCommander. * * @author Nicholai R. Svarre */ public class PasteFromArchiveToFilesFromClipboardAction extends SelectedFilesAction { private PasteFromArchiveToFilesFromClipboardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { ClipboardSupport.setClipboardFiles(files); ClipboardSupport.setOperation(ClipboardOperations.ARCHIVE); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "PasteFromArchiveToFilesFromClipboard"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_D, KeyEvent.META_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_D, KeyEvent.CTRL_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PasteFromArchiveToFilesFromClipboardAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PermanentDeleteAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.DeleteDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action invokes a Delete confirmation dialog to delete the currently selected / marked files in the currently * active folder. Unlike {@link com.mucommander.ui.action.impl.DeleteAction}, the system trash is not used, files are * permanently deleted. * * @see com.mucommander.ui.action.impl.DeleteAction * @author Maxence Bernard */ public class PermanentDeleteAction extends SelectedFilesAction { private PermanentDeleteAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new FileOperationFilter(FileOperation.DELETE)); } @Override public void performAction(FileSet files) { new DeleteDialog(mainFrame, files, true).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "PermanentDelete"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.SHIFT_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PermanentDeleteAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PopupLeftDriveButtonAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.tabs.ActiveTabListener; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Pops up the DrivePopupButton (the drop down button that allows to quickly select a volume or bookmark) * of the left FolderPanel. * * @author Maxence Bernard */ public class PopupLeftDriveButtonAction extends TcAction implements ActiveTabListener { private PopupLeftDriveButtonAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); mainFrame.getLeftPanel().getTabs().addActiveTabListener(this); activeTabChanged(); } /** * Enables or disables this action based on the current tab is not locked, * this action will be enabled, if not it will be disabled. */ public void activeTabChanged() { setEnabled(!mainFrame.getLeftPanel().getTabs().getCurrentTab().isLocked()); } @Override public void performAction() { mainFrame.getLeftPanel().getDriveButton().popupMenu(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "PopupLeftDriveButton"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F1, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PopupLeftDriveButtonAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PopupRightDriveButtonAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.tabs.ActiveTabListener; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * Pops up the DrivePopupButton (the drop down button that allows to quickly select a volume or bookmark) * of the left FolderPanel. * * @author Maxence Bernard */ public class PopupRightDriveButtonAction extends TcAction implements ActiveTabListener { private PopupRightDriveButtonAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); mainFrame.getRightPanel().getTabs().addActiveTabListener(this); activeTabChanged(); } /** * Enables or disables this action based on the current tab is not locked, * this action will be enabled, if not it will be disabled. */ public void activeTabChanged() { setEnabled(!mainFrame.getRightPanel().getTabs().getCurrentTab().isLocked()); } @Override public void performAction() { mainFrame.getRightPanel().getDriveButton().popupMenu(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "PopupRightDriveButton"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F2, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PopupRightDriveButtonAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/PreviousTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Change the selected tab to the tab which is located to the left of the current displayed tab. * If the current displayed tab is the leftmost tab, the rightmost tab will be displayed. * * @author Arik Hadas */ public class PreviousTabAction extends TcAction { private PreviousTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActivePanel().getTabs().previousTab(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "PreviousTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, CTRL_OR_META_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new PreviousTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ProxyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import javax.swing.*; import java.beans.PropertyChangeListener; /** * ProxyAction is a proxy for an Action instance. All Action methods are proxied except for actionPerformed(). * That means all properties of the proxied Action are preserved but the proxied Action is not performed. * *

    ProxyAction is useful to keep the visual properties of an Action instance in a component (JButton for instance) * but perform a different action. * *

    This class is abstract, leaving actionPerformed() unimplemented. {@link MuteProxyAction} provides an * implementation where actionPerformed() does nothing. * * @author Maxence Bernard */ public abstract class ProxyAction implements Action { /** Proxied action */ private Action proxiedAction; /** * Creates a new ProxyAction that acts as a proxy to the provided Action instance. * * @param proxiedAction the action to proxy */ ProxyAction(Action proxiedAction) { this.proxiedAction = proxiedAction; } /** * Returns the Action instance that this ProxyAction proxies. */ public Action getProxiedAction() { return proxiedAction; } ///////////////////// // Proxied methods // ///////////////////// public Object getValue(String key) { return proxiedAction.getValue(key); } public void putValue(String key, Object value) { proxiedAction.putValue(key, value); } public void setEnabled(boolean b) { proxiedAction.setEnabled(b); } public boolean isEnabled() { return proxiedAction.isEnabled(); } public void addPropertyChangeListener(PropertyChangeListener propertyChangeListener) { proxiedAction.addPropertyChangeListener(propertyChangeListener); } public void removePropertyChangeListener(PropertyChangeListener propertyChangeListener) { proxiedAction.removePropertyChangeListener(propertyChangeListener); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/QuitAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.shutdown.QuitDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the Quit confirmation dialog (if it hasn't been disabled) and if quit has been confirmed, * quits the application. * * @author Maxence Bernard */ public class QuitAction extends TcAction { private QuitAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { if (QuitDialog.confirmQuit()) { WindowManager.quit(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Quit"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_Q, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new QuitAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallNextWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings the next window (next window number) to the front. * * @author Maxence Bernard */ public class RecallNextWindowAction extends TcAction { private RecallNextWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { WindowManager.switchToNextWindow(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "RecallNextWindow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallNextWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallPreviousWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings the previous window (previous window number) to the front. * * @author Maxence Bernard */ public class RecallPreviousWindowAction extends TcAction { private RecallPreviousWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { WindowManager.switchToPreviousWindow(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "RecallPreviousWindow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallPreviousWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow10Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 10 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow10Action extends RecallWindowAction { private RecallWindow10Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 10); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"10"; public Descriptor() { super(10); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow10Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow1Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 1 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow1Action extends RecallWindowAction { private RecallWindow1Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 1); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"1"; public Descriptor() { super(1); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow1Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow2Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 2 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow2Action extends RecallWindowAction { private RecallWindow2Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 2); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"2"; public Descriptor() { super(2); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow2Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow3Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 3 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow3Action extends RecallWindowAction { private RecallWindow3Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 3); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"3"; public Descriptor() { super(3); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow3Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow4Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 4 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow4Action extends RecallWindowAction { private RecallWindow4Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 4); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"4"; public Descriptor() { super(4); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow4Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow5Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 5 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow5Action extends RecallWindowAction { private RecallWindow5Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 5); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"5"; public Descriptor() { super(5); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow5Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow6Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 6 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow6Action extends RecallWindowAction { private RecallWindow6Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 6); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"6"; public Descriptor() { super(6); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow6Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow7Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 7 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow7Action extends RecallWindowAction { private RecallWindow7Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 7); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"7"; public Descriptor() { super(7); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow7Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow8Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 8 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow8Action extends RecallWindowAction { private RecallWindow8Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 8); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"8"; public Descriptor() { super(8); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow8Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindow9Action.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import java.util.Map; /** * Recalls window number 9 (brings it to the front). * * @author Maxence Bernard */ public class RecallWindow9Action extends RecallWindowAction { private RecallWindow9Action(MainFrame mainFrame, Map properties) { super(mainFrame, properties, 9); } public static final class Descriptor extends RecallWindowAction.Descriptor { public static final String ACTION_ID = RecallWindowAction.Descriptor.ACTION_ID+"9"; public Descriptor() { super(9); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindow9Action(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RecallWindowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.List; import java.util.Map; /** * Brings a {@link MainFrame} window to the front. This action operates on a specific window number specified in the * constructor, either as a constructor parameter, or in the {@link #WINDOW_NUMBER_PROPERTY_KEY} property. * * @see com.mucommander.ui.main.WindowManager * @author Maxence Bernard */ public class RecallWindowAction extends TcAction { private static final Logger LOGGER = LoggerFactory.getLogger(RecallWindowAction.class); /** Window number this action operates on */ private int windowNumber; /** Key of the property that holds the window number */ public final static String WINDOW_NUMBER_PROPERTY_KEY = "window_number"; public RecallWindowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); Object windowNumberValue = getValue(WINDOW_NUMBER_PROPERTY_KEY); if (!(windowNumberValue instanceof String)) { throw new IllegalArgumentException(WINDOW_NUMBER_PROPERTY_KEY + " (" + windowNumberValue + ")"); } windowNumber = Integer.parseInt((String)windowNumberValue); if (windowNumber <= 0) { throw new IllegalArgumentException(WINDOW_NUMBER_PROPERTY_KEY + " (" + windowNumberValue + ")"); } } public RecallWindowAction(MainFrame mainFrame, Map properties, int windowNumber) { super(mainFrame, properties); this.windowNumber = windowNumber; if (windowNumber <= 0) { throw new IllegalArgumentException("windowNumber ("+windowNumber+")"); } } @Override public void performAction() { List mainFrames = WindowManager.getMainFrames(); // Checks that the window number currently exists if (windowNumber <= 0 || windowNumber > mainFrames.size()) { LOGGER.debug("Window number "+windowNumber+" does not exist"); return; } // Brings the MainFrame to front mainFrames.get(windowNumber-1).toFront(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(windowNumber); } public static class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "RecallWindow"; private int windowNumber; public Descriptor() { this(-1); } protected Descriptor(int windowNumber) { this.windowNumber = windowNumber; } public String getId() { return windowNumber < 0 ? ACTION_ID : ACTION_ID + windowNumber; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { if (windowNumber <= 0 || windowNumber > 10) { return null; } return KeyStroke.getKeyStroke(Character.forDigit(windowNumber == 10 ? 0 : windowNumber, 10), KeyEvent.CTRL_DOWN_MASK); } @Override public String getLabel() { return Translator.get(getLabelKey(), windowNumber < 0 ? "?" : ""+windowNumber); } @Override public boolean isParameterized() { return windowNumber == -1; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RecallWindowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RefreshAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action refreshes the currently active FolderPanel (refreshes the content of the folder). * * @author Maxence Bernard */ public class RefreshAction extends TcAction { private RefreshAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { // Refresh current folder in a separate thread mainFrame.getActivePanel().tryRefreshCurrentFolder(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Refresh"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F9, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RefreshAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RenameAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.commons.file.filter.OrFileFilter; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action triggers in-table renaming of the currently selected file, if no file is marked. * If files are marked, it simply invokes 'Move dialog' just like {@link CopyAction}. * * @author Maxence Bernard */ public class RenameAction extends SelectedFileAction { private RenameAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new OrFileFilter( new FileOperationFilter(FileOperation.RENAME), new AndFileFilter( new FileOperationFilter(FileOperation.READ_FILE), new FileOperationFilter(FileOperation.WRITE_FILE) ) )); } @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); AbstractFile selectedFile = activeTable.getSelectedFile(false); // Trigger in-table editing only if a file other than parent folder '..' is selected if (selectedFile != null) { // Trigger in-table renaming activeTable.editCurrentFilename(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Rename"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F6, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RenameAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ReportBugAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.util.Map; /** * This action opens the mucommander.com bug repository URL in the system's default browser. * * @author Maxence Bernard */ public class ReportBugAction extends OpenURLInBrowserAction { private ReportBugAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); putValue(URL_PROPERTY_KEY, com.mucommander.RuntimeConstants.BUG_REPOSITORY_URL); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ReportBug"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ReportBugAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RevealInDesktopAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractArchiveEntryFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.utils.text.Translator; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action reveals the currently selected file or folder in the native Desktop's file manager * (e.g. Finder for Mac OS X, Explorer for Windows, etc...). * * @author Maxence Bernard */ public class RevealInDesktopAction extends ParentFolderAction { private RevealInDesktopAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setEnabled(DesktopManager.canOpenInFileManager()); } @Override protected void toggleEnabledState() { AbstractFile currentFolder = mainFrame.getActivePanel().getCurrentFolder(); setEnabled(isLocalRegularFolder(currentFolder)); } private static boolean isLocalRegularFolder(AbstractFile currentFolder) { return currentFolder != null && currentFolder.isLocalFile() && !currentFolder.isArchive() && !currentFolder.hasAncestor(AbstractArchiveEntryFile.class); } @Override public void performAction() { try { DesktopManager.openInFileManager(getCurrentFolder()); } catch(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } private AbstractFile getCurrentFolder() { if (OsFamily.MAC_OS_X.isCurrent()) { AbstractFile currentFile = mainFrame.getActiveTable().getSelectedFile(); if (currentFile == null) { return mainFrame.getActivePanel().getCurrentFolder(); } return currentFile; } else { return mainFrame.getActivePanel().getCurrentFolder(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "RevealInDesktop"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_L, CTRL_OR_META_DOWN_MASK); } @Override public String getLabel() { return Translator.get(ActionProperties.getActionLabelKey(RevealInDesktopAction.Descriptor.ACTION_ID), DesktopManager.canOpenInFileManager() ? DesktopManager.getFileManagerName() : Translator.get("file_manager")); } @Override public ImageIcon getIcon() { if (OsFamily.MAC_OS_X.isCurrent()) { return getStandardIcon("Finder"); } return super.getIcon(); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RevealInDesktopAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ReverseSortOrderAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action reverses the sort order of the currently active FileTable. * * @author Maxence Bernard */ public class ReverseSortOrderAction extends TcAction { private ReverseSortOrderAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActiveTable().reverseSortOrder(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ReverseSortOrder"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ReverseSortOrderAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RightArrowAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.TableViewMode; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.HashMap; import java.util.Map; /** * @author Oleg Trifonov * Created on 27/10/16. */ public class RightArrowAction extends TcAction { public RightArrowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable table = mainFrame.getActiveTable(); if (table == null) { return; } if (table.getViewMode() != TableViewMode.FULL) { return; } int count = table.getFilesCount(); AbstractFile file = table.getSelectedFile(true, true); if (file != null && file.isDirectory() && table.getSelectedFileIndex() > 0) { new OpenAction(mainFrame, new HashMap<>()).performAction(); } else { table.selectFile(count-1); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "RightArrowAction"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RightArrowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/RunCommandAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.shell.RunDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the 'Run command' dialog that is used to execute a shell command. * * @author Maxence Bernard */ @InvokesDialog public class RunCommandAction extends TcAction { private RunCommandAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new RunDialog(mainFrame).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "RunCommand"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_R, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new RunCommandAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectBackwardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * Moves the current {@link FileTable}'s selection backward, {@link #getRowDecrement} rows at a time. * * @author Maxence Bernard */ public abstract class SelectBackwardAction extends TcAction { SelectBackwardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } ///////////////////////////// // MuAction implementation // ///////////////////////////// @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); int newFileIndex = Math.max(activeTable.getSelectedFileIndex() - getRowDecrement(), 0); activeTable.selectFile(newFileIndex); } ////////////////////// // Abstract methods // ////////////////////// /** * Returns the number of rows to decrease from the current selection. * * @return the number of rows to decrease from the current selection. */ protected abstract int getRowDecrement(); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectFirstRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action selects the first row/file in the current FileTable. * * @author Maxence Bernard */ public class SelectFirstRowAction extends TcAction { private SelectFirstRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getActiveTable().selectFile(0); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectFirstRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectFirstRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectForwardAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * Moves the current {@link FileTable}'s selection forward, {@link #getRowIncrement} rows at a time. * * @author Maxence Bernard */ public abstract class SelectForwardAction extends TcAction { SelectForwardAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } ///////////////////////////// // MuAction implementation // ///////////////////////////// @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); int newFileIndex = Math.min(activeTable.getSelectedFileIndex() + getRowIncrement(), activeTable.getFilesCount() - 1); activeTable.selectFile(newFileIndex); } ////////////////////// // Abstract methods // ////////////////////// /** * Returns the number of rows to increment the current selection. * * @return the number of rows to increment the current selection. */ protected abstract int getRowIncrement(); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectLastRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action selects the last row/file in the current FileTable. * * @author Maxence Bernard */ public class SelectLastRowAction extends TcAction { private SelectLastRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable fileTable = mainFrame.getActiveTable(); fileTable.selectFile(fileTable.getFilesCount()-1); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectLastRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_END, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectLastRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectNextBlockAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action moves the current {@link FileTable}'s selection to the next 'block'. * * @author Maxence Bernard */ public class SelectNextBlockAction extends SelectForwardAction { /** Number of file/rows a block represents */ // TODO: make this value configurable private static final int BLOCK_SIZE = 5; private SelectNextBlockAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { return BLOCK_SIZE; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectNextBlock"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, CTRL_OR_META_DOWN_MASK); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectNextBlockAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectNextPageAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action moves the current {@link FileTable}'s selection to the next page. * * @author Maxence Bernard */ public class SelectNextPageAction extends SelectForwardAction { private SelectNextPageAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { // Note: the page row increment varies with the file table's height return mainFrame.getActiveTable().getPageRowIncrement()+1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectNextPage"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectNextPageAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectNextRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action moves the current {@link FileTable}'s selection to the next row. * * @author Maxence Bernard */ public class SelectNextRowAction extends SelectForwardAction { private SelectNextRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowIncrement() { return 1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectNextRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectNextRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectPreviousBlockAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action moves the current {@link FileTable}'s selection to the previous 'block'. * * @author Maxence Bernard */ public class SelectPreviousBlockAction extends SelectBackwardAction { /** Number of file/rows a block represents */ // TODO: make this value configurable private static final int BLOCK_SIZE = 5; private SelectPreviousBlockAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { return BLOCK_SIZE; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectPreviousBlock"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_UP, CTRL_OR_META_DOWN_MASK); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectPreviousBlockAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectPreviousPageAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action moves the current {@link FileTable}'s selection to the previous page. * * @author Maxence Bernard */ public class SelectPreviousPageAction extends SelectBackwardAction { private SelectPreviousPageAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { // Note: the page row increment varies with the file table's height return mainFrame.getActiveTable().getPageRowIncrement()+1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectPreviousPage"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectPreviousPageAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectPreviousRowAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action moves the current {@link FileTable}'s selection to the previous row. * * @author Maxence Bernard */ public class SelectPreviousRowAction extends SelectBackwardAction { private SelectPreviousRowAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected int getRowDecrement() { return 1; } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SelectPreviousRow"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0); } public KeyStroke getDefaultAltKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SelectPreviousRowAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectedFileAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * SelectedFileAction is an abstract action that operates on the currently active FileTable, * and that is enabled only when a file other than the parent folder file '..' is selected. * *

    Optionally, a FileFilter can be specified using {@link #setSelectedFileFilter(com.mucommander.commons.file.filter.FileFilter) setSelectedFileFilter} * to further restrict the enabled condition to files that match the IMAGE_FILTER. * * @author Maxence Bernard */ public abstract class SelectedFileAction extends FileAction { SelectedFileAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } /** * Returns the IMAGE_FILTER that restricts the enabled condition to selected files that match the specified IMAGE_FILTER. * * @return the IMAGE_FILTER that restricts the enabled condition to selected files that match the specified IMAGE_FILTER. */ FileFilter getSelectedFileFilter() { return filter; } /** * Restricts the enabled condition to selected files that match the specified IMAGE_FILTER. * * @param filter FileFilter instance */ public void setSelectedFileFilter(FileFilter filter) { this.filter = filter; } @Override protected boolean getFileTableCondition(FileTable fileTable) { AbstractFile selectedFile = fileTable.getSelectedFile(false, true); boolean enable = selectedFile != null; if (enable && filter != null) { enable = filter.match(selectedFile); } return enable; } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SelectedFilesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import java.util.Map; /** * SelectedFilesAction is an abstract action that operates on the currently active FileTable, and is enabled only * when at least one file is marked, or when a file other than the parent folder file '..' is selected. * When none of those conditions is satisfied, this action is disabled. * *

    Optionally, a FileFilter can be specified using {@link #setSelectedFileFilter(com.mucommander.commons.file.filter.FileFilter) setSelectedFileFilter} * to further restrict the enabled condition to files that match the IMAGE_FILTER. * * @author Maxence Bernard */ public abstract class SelectedFilesAction extends SelectedFileAction { public SelectedFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected boolean getFileTableCondition(FileTable fileTable) { return fileTable.getFileTableModel().getNbMarkedFiles() > 0 || super.getFileTableCondition(fileTable); } ///////////////////////////// // MuAction implementation // ///////////////////////////// @Override public final void performAction() { FileSet files = mainFrame.getActiveTable().getSelectedFiles(); // Perform the action only if at least one file is selected/marked if (!files.isEmpty()) { performAction(files); } } ////////////////////// // Abstract methods // ////////////////////// /** * Performs the action on the files that were selected/marked by the user in the currently active table. * * @param files files that were selected/marked by the user in the currently active table */ public abstract void performAction(FileSet files); } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SetSameFolderAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action equalizes both FileTable's current folders: the 'inactive' FileTable's current folder becomes * the active FileTable's one. * * @author Maxence Bernard */ public class SetSameFolderAction extends TcAction implements ActivePanelListener { private SetSameFolderAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); mainFrame.addActivePanelListener(this); toggleEnabledState(); } /** * Enables or disables this action based on the tab in the other panel being not lock, * this action will be enabled, if not it will be disabled. */ private void toggleEnabledState() { setEnabled(!mainFrame.getInactivePanel().getTabs().getCurrentTab().isLocked()); } public void activePanelChanged(FolderPanel folderPanel) { toggleEnabledState(); } @Override public void performAction() { mainFrame.setSameFolder(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SetSameFolder"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_E, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SetSameFolderAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SetTabTitleAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.tab.TabTitleDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Change the title of the selected tab. * In case empty string is entered, the title of the tab will be based on its current location. * * @author Arik Hadas */ @InvokesDialog public class SetTabTitleAction extends TcAction { private SetTabTitleAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new TabTitleDialog(mainFrame, mainFrame.getActivePanel()).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SetTabTitle"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SetTabTitleAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowAboutAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.about.AboutDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action displays the 'About' dialog. * * @author Maxence Bernard */ public class ShowAboutAction extends TcAction { private ShowAboutAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new AboutDialog(mainFrame).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowAbout"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowAboutAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowBookmarksQLAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; public class ShowBookmarksQLAction extends ShowQuickListAction { private ShowBookmarksQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.BOOKMARKS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowBookmarksQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_4, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowBookmarksQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowDebugConsoleAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.debug.DebugConsoleDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * @author Maxence Bernard */ public class ShowDebugConsoleAction extends TcAction { private ShowDebugConsoleAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new DebugConsoleDialog(mainFrame).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowDebugConsole"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowDebugConsoleAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowEditorBookmarksQLAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; public class ShowEditorBookmarksQLAction extends ShowQuickListAction { private ShowEditorBookmarksQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.EDITOR_BOOKMARKS); } @Override public ActionDescriptor getDescriptor() { return new ShowRecentEditedFilesQLAction.Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowEditorBookmarksQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_9, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowEditorBookmarksQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowFilePopupMenuAction.java ================================================ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.TablePopupMenu; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This actions shows the popup menu for currently selected file * * @author Miroslav Oujesky */ public class ShowFilePopupMenuAction extends SelectedFilesAction { private ShowFilePopupMenuAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { FileTable table = mainFrame.getActiveTable(); int currentRow = table.getSelectedRow(); // Does the row correspond to the parent '..' folder ? boolean parentFolderClicked = currentRow == 0 && table.getFileTableModel().hasParentFolder(); TablePopupMenu popupMenu = new TablePopupMenu( mainFrame, table.getFolderPanel().getCurrentFolder(), parentFolderClicked ? null : table.getSelectedFile(), parentFolderClicked, files ); // find coordinates of current row and show popup menu bellow it Rectangle rect = table.getCellRect(currentRow, table.getSelectedColumn(), true); popupMenu.show(table, 5, rect.y + rect.height + 5); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowFilePopupMenu"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.ALT_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_CONTEXT_MENU, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowFilePopupMenuAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowFilePropertiesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.PropertiesDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the file Properties dialog. * * @author Maxence Bernard */ public class ShowFilePropertiesAction extends SelectedFilesAction { private ShowFilePropertiesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction(FileSet files) { new PropertiesDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowFileProperties"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowFilePropertiesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowFoldersSizeAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.full.FileTableModel; import javax.swing.KeyStroke; import java.util.Map; /** * @author Oleg Trifonov * Created on 24/03/15. */ public class ShowFoldersSizeAction extends ParentFolderAction { private ShowFoldersSizeAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected void toggleEnabledState() { } @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); FileTableModel fileTableModel = (FileTableModel)activeTable.getModel(); for (AbstractFile file : fileTableModel.getFiles()) { if (file.isDirectory()) { fileTableModel.startDirectorySizeCalculation(activeTable, file); } } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowFoldersSize"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowFoldersSizeAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowKeyboardShortcutsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.help.ShortcutsDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action displays the 'Keyboard shortcuts' dialog that lists all available keyboard shortcuts sorted by topic. * * @author Maxence Bernard */ public class ShowKeyboardShortcutsAction extends TcAction { private ShowKeyboardShortcutsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new ShortcutsDialog(mainFrame).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowKeyboardShortcuts"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowKeyboardShortcutsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowParentFoldersQLAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows ParentFoldersQL on the current active FileTable. * * @author Arik Hadas */ public class ShowParentFoldersQLAction extends ShowQuickListAction { private ShowParentFoldersQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.PARENT_FOLDERS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowParentFoldersQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_1, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowParentFoldersQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowPreferencesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.pref.general.GeneralPreferencesDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action shows up the preferences dialog. * * @author Maxence Bernard */ @InvokesDialog public class ShowPreferencesAction extends TcAction { private ShowPreferencesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { GeneralPreferencesDialog.getDialog().showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowPreferences"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowPreferencesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowQuickListAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import java.util.Map; /** * ShowFileTablePopupAction is an abstract action that shows pop up corresponding to the given * index on the currently active FileTable. * * @author Arik Hadas */ abstract class ShowQuickListAction extends TcAction { ShowQuickListAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } void openQuickList(QuickLists quickList) { mainFrame.getActivePanel().showQuickList(quickList.ordinal()); } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowRecentEditedFilesQLAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows RecentEditedFilesQL on the current active FileTable. * * @author Oleg Trifonov */ public class ShowRecentEditedFilesQLAction extends ShowQuickListAction { private ShowRecentEditedFilesQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.RECENT_EDITED_FILES); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowRecentEditedFilesQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_8, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowRecentEditedFilesQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowRecentExecutedFilesQLAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows RecentExecutedFilesQL on the current active FileTable. * * @author Arik Hadas */ public class ShowRecentExecutedFilesQLAction extends ShowQuickListAction { private ShowRecentExecutedFilesQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.RECENT_EXECUTED_FILES); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowRecentExecutedFilesQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_3, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowRecentExecutedFilesQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowRecentLocationsQLAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows RecentLocationsQL on the current active FileTable. * * @author Arik Hadas */ public class ShowRecentLocationsQLAction extends ShowQuickListAction { private ShowRecentLocationsQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.RECENT_ACCESSED_LOCATIONS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowRecentLocationsQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_2, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowRecentLocationsQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowRecentViewedFilesQLAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows RecentViewedFilesQL on the current active FileTable. * * @author Oleg Trifonov */ public class ShowRecentViewedFilesQLAction extends ShowQuickListAction { private ShowRecentViewedFilesQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.RECENT_VIEWED_FILES); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowRecentViewedFilesQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_7, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowRecentViewedFilesQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowRootFoldersQLAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows RootFoldersQL on the current active FileTable. * * @author Arik Hadas */ public class ShowRootFoldersQLAction extends ShowQuickListAction { private ShowRootFoldersQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.ROOTS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowRootFoldersQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_5, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowRootFoldersQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowServerConnectionsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.server.ShowServerConnectionsDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * Displays a dialog which shows a list of open server connections and allows the user to close them. * * @author Maxence Bernard */ @InvokesDialog public class ShowServerConnectionsAction extends TcAction { private ShowServerConnectionsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new ShowServerConnectionsDialog(mainFrame); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowServerConnections"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_K, KeyEvent.SHIFT_DOWN_MASK | CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowServerConnectionsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ShowTabsQLAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.QuickLists; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action shows TabsQL on the current active FileTable. * * @author Arik Hadas */ public class ShowTabsQLAction extends ShowQuickListAction { private ShowTabsQLAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { openQuickList(QuickLists.TABS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ShowTabsQL"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_6, KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ShowTabsQLAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import java.util.Map; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by a specified criterion. * If the table is already sorted by this particular criterion when the action is performed, the sort order will be * reversed. * *

    This action is enabled only if the corresponding column is currently visible. This prevents this action from being * performed when the column is not visible, which is an unsupported operation. * * @author Maxence Bernard */ public abstract class SortByAction extends TcAction implements ActivePanelListener, TableColumnModelListener { /** FileTable column this action operates on */ protected Column column; SortByAction(MainFrame mainFrame, Map properties, Column column) { super(mainFrame, properties); this.column = column; mainFrame.addActivePanelListener(this); mainFrame.getLeftPanel().getFileTable().getColumnModel().addColumnModelListener(this); mainFrame.getRightPanel().getFileTable().getColumnModel().addColumnModelListener(this); updateState(mainFrame.getActiveTable()); } /** * Updates this action's enable state, enabling this action if the corresponding column is visible and vice-versa. * * @param fileTable the FileTable this action currently operates on */ private void updateState(FileTable fileTable) { setEnabled(fileTable.isColumnVisible(column)); } ///////////////////////////// // MuAction implementation // ///////////////////////////// @Override public void performAction() { mainFrame.getActiveTable().sortBy(column); } //////////////////////////////////////// // ActivePanelListener implementation // //////////////////////////////////////// public void activePanelChanged(FolderPanel folderPanel) { // Update this action's enabled state when the active panel has changed updateState(folderPanel.getFileTable()); } ///////////////////////////////////////////// // TableColumnModelListener implementation // ///////////////////////////////////////////// public void columnAdded(TableColumnModelEvent event) { // Enable this action when the corresponding column has been made visible if(event.getFromIndex()==column.ordinal()) setEnabled(true); } public void columnRemoved(TableColumnModelEvent event) { // Disable this action when the corresponding column has been made invisible if(event.getFromIndex()==column.ordinal()) setEnabled(false); } public void columnMoved(TableColumnModelEvent event) { } public void columnMarginChanged(ChangeEvent event) { } public void columnSelectionChanged(ListSelectionEvent event) { } /////////////////// // Inner classes // /////////////////// public abstract static class Descriptor extends AbstractActionDescriptor { private Column column; private KeyStroke defaultKeyStroke; public Descriptor(Column column, KeyStroke defaultKeyStroke) { this.column = column; this.defaultKeyStroke = defaultKeyStroke; } public String getId() { return column.getSortByColumnActionId(); } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return defaultKeyStroke; } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByDateAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by date. * If the table is already sorted by date, the sort order will be reversed. * * @author Maxence Bernard */ public class SortByDateAction extends SortByAction { private SortByDateAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.DATE); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.DATE, KeyStroke.getKeyStroke(KeyEvent.VK_F6, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortByDateAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByExtensionAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by extension. * If the table is already sorted by extension, the sort order will be reversed. * * @author Maxence Bernard */ public class SortByExtensionAction extends SortByAction { private SortByExtensionAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.EXTENSION); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.EXTENSION, KeyStroke.getKeyStroke(KeyEvent.VK_F3, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortByExtensionAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByGroupAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by group. * If the table is already sorted by group, the sort order will be reversed. * * @author Maxence Bernard */ public class SortByGroupAction extends SortByAction { private SortByGroupAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.GROUP); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.GROUP, KeyStroke.getKeyStroke(KeyEvent.VK_F9, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortByGroupAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByNameAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by name. * If the table is already sorted by name, the sort order will be reversed. * * @author Maxence Bernard */ public class SortByNameAction extends SortByAction { private SortByNameAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.NAME); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.NAME, KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortByNameAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByOwnerAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by owner. * If the table is already sorted by owner, the sort order will be reversed. * * @author Maxence Bernard */ public class SortByOwnerAction extends SortByAction { private SortByOwnerAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.OWNER); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.OWNER, KeyStroke.getKeyStroke(KeyEvent.VK_F8, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortByOwnerAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortByPermissionsAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by permissions. * If the table is already sorted by permissions, the sort order will be reversed. * * @author Maxence Bernard */ public class SortByPermissionsAction extends SortByAction { private SortByPermissionsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.PERMISSIONS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.PERMISSIONS, KeyStroke.getKeyStroke(KeyEvent.VK_F7, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortByPermissionsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SortBySizeAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.awt.event.KeyEvent; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * This action sorts the currently active {@link com.mucommander.ui.main.table.FileTable} by size. * If the table is already sorted by size, the sort order will be reversed. * * @author Maxence Bernard */ public class SortBySizeAction extends SortByAction { private SortBySizeAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.SIZE); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends SortByAction.Descriptor { public Descriptor() { super(Column.SIZE, KeyStroke.getKeyStroke(KeyEvent.VK_F5, KeyEvent.CTRL_DOWN_MASK)); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SortBySizeAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SplitEquallyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Positions the split pane divider in the middle so that both folder panels have the same space. * * @author Maxence Bernard */ public class SplitEquallyAction extends TcAction { private SplitEquallyAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.getSplitPane().setSplitRatio(0.5f); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SplitEqually"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SplitEquallyAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SplitFileAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.FileOperationFilter; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.SplitFileDialog; import com.mucommander.ui.main.MainFrame; /** * This action invokes the split file dialog which allows to split the selected file into several parts. * * @author Mariusz Jakubowski */ @InvokesDialog public class SplitFileAction extends SelectedFileAction { private SplitFileAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); setSelectedFileFilter(new AndFileFilter( new AttributeFileFilter(FileAttribute.DIRECTORY, true), new FileOperationFilter(FileOperation.READ_FILE) )); } @Override public void performAction() { new SplitFileDialog(mainFrame, mainFrame.getActiveTable().getSelectedFile(), mainFrame.getInactivePanel().getCurrentFolder() ).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SplitFile"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return null; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SplitFileAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SplitHorizontallyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Splits the folder panels horizontally (left/right) within the MainFrame. * * @author Maxence Bernard */ public class SplitHorizontallyAction extends TcAction { private SplitHorizontallyAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.setSplitPaneOrientation(false); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SplitHorizontally"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SplitHorizontallyAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SplitVerticallyAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * Splits the folder panels vertically (top/bottom) within the MainFrame. * This is the default split orientation. * * @author Maxence Bernard */ public class SplitVerticallyAction extends TcAction { private SplitVerticallyAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.setSplitPaneOrientation(true); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SplitVertically"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SplitVerticallyAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/StopAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.core.LocationChanger.ChangeFolderThread; import com.mucommander.ui.action.*; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action is invoked to stop a running location change. * * @author Maxence Bernard */ public class StopAction extends TcAction implements LocationListener { private StopAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // This action is initially disabled and enabled only during a folder change setEnabled(false); // Listen to location change events mainFrame.getLeftPanel().getLocationManager().addLocationListener(this); mainFrame.getRightPanel().getLocationManager().addLocationListener(this); // This action must be available while in 'no events mode', that's the whole point setHonourNoEventsMode(false); } @Override public void performAction() { FolderPanel folderPanel = mainFrame.getActivePanel(); ChangeFolderThread changeFolderThread = folderPanel.getChangeFolderThread(); if (changeFolderThread != null) { changeFolderThread.tryKill(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } ////////////////////////////// // LocationListener methods // ////////////////////////////// public void locationChanged(LocationEvent e) { setEnabled(false); } public void locationChanging(LocationEvent e) { setEnabled(true); } public void locationCancelled(LocationEvent e) { setEnabled(false); } public void locationFailed(LocationEvent e) { setEnabled(false); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Stop"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new StopAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SwapFoldersAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action swaps both FileTable's current folders: the left table's current folder becomes the right table's one * and vice versa. * * @author Maxence Bernard */ public class SwapFoldersAction extends TcAction { private SwapFoldersAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.swapFolders(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SwapFolders"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_U, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SwapFoldersAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/SwitchActiveTableAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action switches the currently active FileTable, that is gives focus to the FileTable that currently doesn't * have it. * * @author Maxence Bernard */ public class SwitchActiveTableAction extends TcAction { private SwitchActiveTableAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); FileTable leftTable = mainFrame.getLeftPanel().getFileTable(); FileTable rightTable = mainFrame.getRightPanel().getFileTable(); if (activeTable == leftTable) { rightTable.requestFocus(); } else if(activeTable == rightTable) { leftTable.requestFocus(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "SwitchActiveTable"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.NAVIGATION; } public KeyStroke getDefaultAltKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_DOWN_MASK); } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new SwitchActiveTableAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/TerminalAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.process.ProcessRunner; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import lombok.extern.slf4j.Slf4j; import javax.swing.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.Map; import static com.mucommander.conf.TcPreferences.*; /** * @author Oleg Trifonov * Created on 17/12/13. */ @Slf4j public class TerminalAction extends ParentFolderAction { /** * Creates a new instance of InternalViewAction. * * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private TerminalAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { AbstractFile currentFolder = mainFrame.getActiveTable().getFileTableModel().getCurrentFolder(); if (OsFamily.LINUX.isCurrent()) { performOnLinux(currentFolder); } else { String cmd = getConsoleCommand(currentFolder); try { ProcessRunner.execute(cmd, currentFolder); } catch (Exception e) { log.error("Execution error", e); } } } private void performOnLinux(AbstractFile currentFolder) { String[] tokens = getTerminalCommand().split(" "); for (int i = 0; i < tokens.length; i++) { if (tokens[i].contains("$p")) { tokens[i] = tokens[i].replace("$p", currentFolder.getAbsolutePath()); } } try { ProcessRunner.execute(tokens, currentFolder); } catch (IOException e) { log.error("Execution error", e); } } private static String getConsoleCommand(AbstractFile folder) { String cmd = getTerminalCommand(); String path = folder.getAbsolutePath(); return cmd.replace("$p", path); } private static String getTerminalCommand() { return switch (useCustomExternalTerminal()) { case TERMINAL_DEFAULT -> DesktopManager.getDefaultTerminalAppCommand(); case TERMINAL_CUSTOM -> getCustomExternalTerminal(); case TERMINAL_ITERM -> "open -a iTerm ."; default -> { log.error("Unknown terminal type"); yield ""; } }; } private static String getCustomExternalTerminal() { return TcConfigurations.getPreferences().getVariable(TcPreference.CUSTOM_EXTERNAL_TERMINAL); } private static int useCustomExternalTerminal() { return TcConfigurations.getPreferences().getVariable(TcPreference.EXTERNAL_TERMINAL_TYPE, TcPreferences.DEFAULT_TERMINAL_TYPE); } @Override protected void toggleEnabledState() { } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Terminal"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F2, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new TerminalAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/TerminalAltAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.desktop.macos.OSXApplications; import com.mucommander.process.ProcessRunner; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import lombok.extern.slf4j.Slf4j; import javax.swing.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.Map; import static com.mucommander.conf.TcPreferences.*; @Slf4j public class TerminalAltAction extends ParentFolderAction { /** * Creates a new instance of InternalViewAction. * * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private TerminalAltAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { AbstractFile currentFolder = mainFrame.getActiveTable().getFileTableModel().getCurrentFolder(); if (OsFamily.MAC_OS_X.isCurrent()) { } if (OsFamily.LINUX.isCurrent()) { performOnLinux(currentFolder); } else { String cmd = getConsoleCommand(currentFolder); try { ProcessRunner.execute(cmd, currentFolder); } catch (Exception e) { log.error("Execution error", e); } } } private void performOnLinux(AbstractFile currentFolder) { String[] tokens = getTerminalCommand().split(" "); for (int i = 0; i < tokens.length; i++) { if (tokens[i].contains("$p")) { tokens[i] = tokens[i].replace("$p", currentFolder.getAbsolutePath()); } } try { ProcessRunner.execute(tokens, currentFolder); } catch (IOException e) { log.error("Execution error", e); } } private static String getConsoleCommand(AbstractFile folder) { String cmd = getTerminalCommand(); String path = folder.getAbsolutePath(); return cmd.replace("$p", path); } private static String getTerminalCommand() { int terminalType = useCustomExternalTerminal(); if (OsFamily.MAC_OS_X.isCurrent()) { if (terminalType == TERMINAL_ITERM || terminalType == TERMINAL_CUSTOM) { terminalType = TERMINAL_DEFAULT; } else if (terminalType == TERMINAL_DEFAULT) { if (OSXApplications.iTermInstalled()) { terminalType = TERMINAL_ITERM; } else if (!getCustomExternalTerminal().isBlank()) { terminalType = TERMINAL_CUSTOM; } } } return switch (terminalType) { case TERMINAL_DEFAULT -> DesktopManager.getDefaultTerminalAppCommand(); case TERMINAL_CUSTOM -> getCustomExternalTerminal(); case TERMINAL_ITERM -> "open -a iTerm ."; default -> { log.error("Unknown terminal type"); yield ""; } }; } private static String getCustomExternalTerminal() { return TcConfigurations.getPreferences().getVariable(TcPreference.CUSTOM_EXTERNAL_TERMINAL).trim(); } private static int useCustomExternalTerminal() { return TcConfigurations.getPreferences().getVariable(TcPreference.EXTERNAL_TERMINAL_TYPE, TcPreferences.DEFAULT_TERMINAL_TYPE); } @Override protected void toggleEnabledState() { } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "TerminalAlternative"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.MISC; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F2, KeyEvent.SHIFT_DOWN_MASK|KeyEvent.ALT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new TerminalAltAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/TerminalPanelAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * @author Oleg Trifonov * * Created on 24/10/14. */ public class TerminalPanelAction extends TcAction { private TerminalPanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { mainFrame.toggleTerminalPanel(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "TerminalPanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.ALL; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new TerminalPanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/TextEditorsListAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.quicklist.ViewedAndEditedFilesQL; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; public class TextEditorsListAction extends TcAction { private TextEditorsListAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { ViewedAndEditedFilesQL viewedAndEditedFilesQL = new ViewedAndEditedFilesQL(mainFrame.getActivePanel(), null); if (!viewedAndEditedFilesQL.isEmpty()) { viewedAndEditedFilesQL.show(); } } @Override public ActionDescriptor getDescriptor() { return new TextEditorsListAction.Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "TextEditorsList"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.WINDOW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { if (OsFamily.MAC_OS_X.isCurrent()) { return KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.ALT_DOWN_MASK); } else { return KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.CTRL_DOWN_MASK); } } public TcAction createAction(MainFrame mainFrame, Map properties) { return new TextEditorsListAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleAutoSizeAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.util.Map; /** * This action toggles the 'auto-size columns' option on the currently active FileTable, which automatically resizes * columns to fit the table's width. * * @author Maxence Bernard */ public class ToggleAutoSizeAction extends TcAction { private ToggleAutoSizeAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { boolean enabled; mainFrame.setAutoSizeColumnsEnabled(enabled = !mainFrame.isAutoSizeColumnsEnabled()); TcConfigurations.getPreferences().setVariable(TcPreference.AUTO_SIZE_COLUMNS, enabled); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleAutoSize"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleAutoSizeAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; import javax.swing.*; import java.util.Map; /** * Shows/hides a specified column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice-versa. * * @author Maxence Bernard */ public abstract class ToggleColumnAction extends TcAction { /** Index of the FileTable column this action operates on */ protected Column column; ToggleColumnAction(MainFrame mainFrame, Map properties, Column column) { super(mainFrame, properties); this.column = column; updateLabel(); } private boolean isColumnVisible() { return mainFrame.getActiveTable().isColumnVisible(column); } private void updateLabel() { setLabel(Translator.get(isColumnVisible() ? "ToggleColumn.hide" : "ToggleColumn.show", column.getLabel())); } @Override public void performAction() { boolean show = !isColumnVisible(); mainFrame.getActiveTable().setColumnEnabled(column, show); if (show) { mainFrame.getActivePanel().tryRefreshCurrentFolder(); } } public static abstract class Descriptor extends AbstractActionDescriptor { private final Column column; public Descriptor(Column column) { this.column = column; } public String getId() { return column.getToggleColumnActionId(); } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public String getLabel() { return Translator.get("ToggleColumn.show", column.getLabel()); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleCommandBarAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.commandbar.CommandBar; import javax.swing.KeyStroke; import java.util.Map; /** * This action shows/hides the current MainFrame's {@link com.mucommander.ui.main.commandbar.CommandBar} depending on its * current visible state: if it is visible, hides it, if not shows it. * *

    This action's label will be updated to reflect the current visible state. * *

    Each time this action is executed, the new current visible state is stored in the configuration so that * new MainFrame windows will use it to determine whether the CommandBar has to be made visible or not. * * @author Maxence Bernard */ public class ToggleCommandBarAction extends TcAction { private ToggleCommandBarAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); updateLabel(TcConfigurations.getPreferences().getVariable(TcPreference.COMMAND_BAR_VISIBLE, TcPreferences.DEFAULT_COMMAND_BAR_VISIBLE)); } private void updateLabel(boolean visible) { setLabel(Translator.get(visible ? Descriptor.ACTION_ID + ".hide" : Descriptor.ACTION_ID + ".show")); } @Override public void performAction() { CommandBar commandBar = mainFrame.getCommandBar(); boolean visible = !commandBar.isVisible(); // Save the last command bar visible state in the configuration, this will become the default for new MainFrame windows. TcConfigurations.getPreferences().setVariable(TcPreference.COMMAND_BAR_VISIBLE, visible); // Change the label to reflect the new command bar state updateLabel(visible); // Show/hide the command bar commandBar.setVisible(visible); mainFrame.getJFrame().validate(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleCommandBar"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public String getLabelKey() { return ACTION_ID + ".show"; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleCommandBarAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleDateColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * Shows/hides the 'Date' column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice versa. * * @author Maxence Bernard */ public class ToggleDateColumnAction extends ToggleColumnAction { private ToggleDateColumnAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.DATE); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends ToggleColumnAction.Descriptor { public Descriptor() { super(Column.DATE); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleDateColumnAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleExtensionColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; /** * Shows/hides the 'Extension' column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice-versa. * * @author Maxence Bernard */ public class ToggleExtensionColumnAction extends ToggleColumnAction { private ToggleExtensionColumnAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.EXTENSION); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends ToggleColumnAction.Descriptor { public Descriptor() { super(Column.EXTENSION); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleExtensionColumnAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleFoldersAlwaysAlphabeticalAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.*; import java.util.Map; /** * This action toggles the 'Show folders first' option, which controls whether folders are displayed first in the * FileTable or mixed with regular files. * * @author Maxence Bernard */ public class ToggleFoldersAlwaysAlphabeticalAction extends TcAction { private ToggleFoldersAlwaysAlphabeticalAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); boolean foldersAlwaysAlphabetical = !activeTable.getSortInfo().getFoldersAlwaysAlphabetical(); activeTable.setFoldersAlwaysAlphabetical(foldersAlwaysAlphabetical); TcConfigurations.getPreferences().setVariable(TcPreference.FOLDERS_ALWAYS_ALPHABETICAL, foldersAlwaysAlphabetical); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleFoldersAlwaysAlphabetical"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleFoldersAlwaysAlphabeticalAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleGroupColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; import java.util.Map; /** * Shows/hides the 'Group' column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice-versa. * * @author Maxence Bernard */ public class ToggleGroupColumnAction extends ToggleColumnAction { private ToggleGroupColumnAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.GROUP); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends ToggleColumnAction.Descriptor { public Descriptor() { super(Column.GROUP); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleGroupColumnAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleHiddenFilesAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * A simple action that toggles hidden files visibility on and off. * @author Nicolas Rinaudo */ public class ToggleHiddenFilesAction extends TcAction { // - Initialization ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Creates a new ToggleHiddenFilesAction. */ private ToggleHiddenFilesAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } // - Action code --------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Toggles hidden files display on and off and requests for all file tables to be repainted. */ @Override public void performAction() { boolean show = TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_HIDDEN_FILES, TcPreferences.DEFAULT_SHOW_HIDDEN_FILES); TcConfigurations.getPreferences().setVariable(TcPreference.SHOW_HIDDEN_FILES, !show); WindowManager.tryRefreshCurrentFolders(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleHiddenFiles"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleHiddenFilesAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleLockTabAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.tabs.FileTableTabs; import com.mucommander.utils.text.Translator; import javax.swing.*; import java.util.Map; /** * This action locks/unlocks the currently selected {@link com.mucommander.ui.main.tabs.FileTableTab} depending on its * current locking state: if it is locked, unlock it, if not lock it. * *

    This action's label will be updated to reflect the locking state of the currently selected tab. * * @author Arik Hadas */ public class ToggleLockTabAction extends ActiveTabAction { private ToggleLockTabAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } private void updateLabel(boolean locked) { setLabel(Translator.get(locked ? Descriptor.ACTION_ID + ".unlock" : Descriptor.ACTION_ID + ".lock")); } @Override public void performAction() { boolean lock = !mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked(); FileTableTabs tabs = mainFrame.getActivePanel().getTabs(); if (lock) { tabs.lock(); } else { tabs.unlock(); } // Change the label to reflect the new tab's locking state updateLabel(lock); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } @Override protected void toggleEnabledState() { updateLabel(mainFrame.getActivePanel().getTabs().getCurrentTab().isLocked()); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleLockTab"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.TAB; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleLockTabAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleOwnerColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; import java.util.Map; /** * Shows/hides the 'Owner' column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice-versa. * * @author Maxence Bernard */ public class ToggleOwnerColumnAction extends ToggleColumnAction { private ToggleOwnerColumnAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.OWNER); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends ToggleColumnAction.Descriptor { public Descriptor() { super(Column.OWNER); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleOwnerColumnAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/TogglePanelPreviewModeAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * @author Oleg Trifonov * * Created on 26/09/2016. */ public class TogglePanelPreviewModeAction extends TcAction { /** * Creates a new ToggleTableViewModeShortAction * * @param mainFrame the MainFrame to associate with this new MuAction * @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial */ private TogglePanelPreviewModeAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FolderPanel panel; if (getMainFrame().getActivePanel() == getMainFrame().getLeftPanel()) { panel = getMainFrame().getRightPanel(); } else { panel = getMainFrame().getLeftPanel(); } panel.setPreviewMode(!panel.isPreviewMode()); } @Override public ActionDescriptor getDescriptor() { return new TogglePanelPreviewModeAction.Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "TogglePanelPreviewMode"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { if (OsFamily.getCurrent() == OsFamily.MAC_OS_X) { return KeyStroke.getKeyStroke(KeyEvent.VK_Q, KeyEvent.CTRL_DOWN_MASK); } else { return KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK); } } public TcAction createAction(MainFrame mainFrame, Map properties) { return new TogglePanelPreviewModeAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/TogglePermissionsColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; import java.util.Map; /** * Shows/hides the 'Permissions' column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice-versa. * * @author Maxence Bernard */ public class TogglePermissionsColumnAction extends ToggleColumnAction { private TogglePermissionsColumnAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.PERMISSIONS); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends ToggleColumnAction.Descriptor { public Descriptor() { super(Column.PERMISSIONS); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new TogglePermissionsColumnAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleShowFoldersFirstAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import javax.swing.KeyStroke; import java.util.Map; /** * This action toggles the 'Show folders first' option, which controls whether folders are displayed first in the * FileTable or mixed with regular files. * * @author Maxence Bernard */ public class ToggleShowFoldersFirstAction extends TcAction { private ToggleShowFoldersFirstAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FileTable activeTable = mainFrame.getActiveTable(); boolean showFoldersFirst = !activeTable.getSortInfo().getFoldersFirst(); activeTable.setFoldersFirst(showFoldersFirst); TcConfigurations.getPreferences().setVariable(TcPreference.SHOW_FOLDERS_FIRST, showFoldersFirst); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleShowFoldersFirst"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleShowFoldersFirstAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleSinglePanelAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import java.util.Map; import javax.swing.KeyStroke; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; /** * Toggles isVisible state of the right panel, imitating single/two panel view switch. */ public class ToggleSinglePanelAction extends TcAction { private static final float TWO_PANELS_DEFAULT_RATIO = 0.5f; private float previousRatio = TWO_PANELS_DEFAULT_RATIO; private ToggleSinglePanelAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } private void hideInactivePanel() { mainFrame.getInactivePanel().getPanel().setVisible(false); } private void showInactivePanel() { mainFrame.getInactivePanel().getPanel().setVisible(true); } @Override public void performAction() { // we want to restore old two panel ratio boolean isSinglePanelViewNow = mainFrame.toggleSinglePanel(); if (isSinglePanelViewNow) { previousRatio = mainFrame.getSplitPane().getSplitRatio(); hideInactivePanel(); } else { mainFrame.getSplitPane().setSplitRatio(previousRatio); showInactivePanel(); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleSinglePanel"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleSinglePanelAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleSizeColumnAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.Column; import java.util.Map; /** * Shows/hides the 'Size' column of the currently active FileTable. If the column is currently visible, this action * will hide it and vice-versa. * * @author Maxence Bernard */ public class ToggleSizeColumnAction extends ToggleColumnAction { private ToggleSizeColumnAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, Column.SIZE); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends ToggleColumnAction.Descriptor { public Descriptor() { super(Column.SIZE); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleSizeColumnAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleStatusBarAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.statusbar.StatusBar; import javax.swing.KeyStroke; import java.util.Map; /** * This action shows/hides the current MainFrame's {@link com.mucommander.ui.main.statusbar.StatusBar} depending on its * current visible state: if it is visible, hides it, if not shows it. * *

    This action's label will be updated to reflect the current visible state. * *

    Each time this action is executed, the new current visible state is stored in the configuration so that * new MainFrame windows will use it to determine whether the StatusBar has to be made visible or not. * * @author Maxence Bernard */ public class ToggleStatusBarAction extends TcAction { private ToggleStatusBarAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); updateLabel(TcConfigurations.getPreferences().getVariable(TcPreference.STATUS_BAR_VISIBLE, TcPreferences.DEFAULT_STATUS_BAR_VISIBLE)); } private void updateLabel(boolean visible) { setLabel(Translator.get(visible?Descriptor.ACTION_ID+".hide":Descriptor.ACTION_ID+".show")); } @Override public void performAction() { StatusBar statusBar = mainFrame.getStatusBar(); boolean visible = !statusBar.isVisible(); // Save the last status bar visible state in the configuration, this will become the default for new MainFrame windows. TcConfigurations.getPreferences().setVariable(TcPreference.STATUS_BAR_VISIBLE, visible); // Change the label to reflect the new status bar state updateLabel(visible); // Show/hide the status bar statusBar.setVisible(visible); mainFrame.getJFrame().validate(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleStatusBar"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public String getLabelKey() { return ACTION_ID+ ".show"; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleStatusBarAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleTableViewModeCompactAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.views.TableViewMode; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * @author Oleg Trifonov * Created on 15/04/15. */ public class ToggleTableViewModeCompactAction extends TcAction { /** * Creates a new ToggleTableViewModeCompactAction * * @param mainFrame the MainFrame to associate with this new MuAction * @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial */ private ToggleTableViewModeCompactAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { getMainFrame().getActiveTable().setViewMode(TableViewMode.COMPACT); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleTableViewModeCompact"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_2, KeyEvent.CTRL_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleTableViewModeCompactAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleTableViewModeFullAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.views.TableViewMode; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * @author Oleg Trifonov * Created on 15/04/15. */ public class ToggleTableViewModeFullAction extends TcAction { /** * Creates a new ToggleTableViewModeFullAction * * @param mainFrame the MainFrame to associate with this new MuAction * @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial */ private ToggleTableViewModeFullAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { getMainFrame().getActiveTable().setViewMode(TableViewMode.FULL); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleTableViewModeFull"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_1, KeyEvent.CTRL_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleTableViewModeFullAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleTableViewModeShortAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.views.TableViewMode; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * @author Oleg Trifonov * Created on 15/04/15. */ public class ToggleTableViewModeShortAction extends TcAction { /** * Creates a new ToggleTableViewModeShortAction * * @param mainFrame the MainFrame to associate with this new MuAction * @param properties the initial properties to use in this action. The Hashtable may simply be empty if no initial */ private ToggleTableViewModeShortAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { getMainFrame().getActiveTable().setViewMode(TableViewMode.SHORT); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleTableViewModeShort"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_3, KeyEvent.CTRL_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleTableViewModeShortAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleToolBarAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.JPanel; import javax.swing.KeyStroke; import java.util.Map; /** * This action shows/hides the current MainFrame's {@link com.mucommander.ui.main.toolbar.ToolBar} depending on its * current visible state: if it is visible, hides it, if not shows it. * *

    This action's label will be updated to reflect the current visible state. * *

    Each time this action is executed, the new current visible state is stored in the configuration so that * new MainFrame windows will use it to determine whether the ToolBar has to be made visible or not. * * @author Maxence Bernard */ public class ToggleToolBarAction extends TcAction { private ToggleToolBarAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); updateLabel(TcConfigurations.getPreferences().getVariable(TcPreference.TOOLBAR_VISIBLE, TcPreferences.DEFAULT_TOOLBAR_VISIBLE)); } private void updateLabel(boolean visible) { setLabel(Translator.get(visible?Descriptor.ACTION_ID+".hide":Descriptor.ACTION_ID+".show")); } @Override public void performAction() { JPanel toolBarPanel = mainFrame.getToolBarPanel(); boolean visible = !toolBarPanel.isVisible(); // Save the last toolbar visible state in the configuration, this will become the default for new MainFrame windows. TcConfigurations.getPreferences().setVariable(TcPreference.TOOLBAR_VISIBLE, visible); // Change the label to reflect the new toolbar state updateLabel(visible); // Show/hide the toolbar toolBarPanel.setVisible(visible); mainFrame.getJFrame().validate(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleToolBar"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return null; } @Override public String getLabelKey() { return ACTION_ID + ".show"; } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleToolBarAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ToggleTreeAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.tree.FoldersTreePanel; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action toggles the visibility of a directory tree. * @see FoldersTreePanel * * @author Mariusz Jakubowski */ public class ToggleTreeAction extends TcAction { private ToggleTreeAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { FolderPanel folderPanel = mainFrame.getActiveTable().getFolderPanel(); folderPanel.setTreeVisible(!folderPanel.isTreeVisible()); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ToggleTree"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.VIEW; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_J, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ToggleTreeAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/UnmarkAllAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Map; /** * This action unmarks all files in the current file table. * * @author Maxence Bernard */ public class UnmarkAllAction extends MarkAllAction { private UnmarkAllAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties, false); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "UnmarkAll"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_D, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new UnmarkAllAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/UnmarkGroupAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.FileSelectionDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action brings up the 'File selection' dialog which allows to unmark a group of files that match a specified expression. * * @author Maxence Bernard */ @InvokesDialog public class UnmarkGroupAction extends TcAction { private UnmarkGroupAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override public void performAction() { new FileSelectionDialog(mainFrame, false).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "UnmarkGroup"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.SELECTION; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new UnmarkGroupAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/UnpackAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.OrFileFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.*; import com.mucommander.ui.dialog.file.UnpackDialog; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * This action pops up the 'Unpack files' dialog that allows to unpack the currently marked files. * * @author Maxence Bernard */ @InvokesDialog public class UnpackAction extends SelectedFilesAction { private UnpackAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); // Unpack job operates on archives and directories setSelectedFileFilter(new OrFileFilter( new AttributeFileFilter(FileAttribute.ARCHIVE), new AttributeFileFilter(FileAttribute.DIRECTORY) )); } @Override public void performAction(FileSet files) { new UnpackDialog(mainFrame, files).showDialog(); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "Unpack"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_P, CTRL_OR_META_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new UnpackAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/UserMenuAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.*; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.UserPopupMenu; import com.mucommander.ui.main.menu.usermenu.LoadUserMenuException; import com.mucommander.ui.main.menu.usermenu.UserPopupMenuLoader; import com.mucommander.ui.notifier.AbstractNotifier; import com.mucommander.ui.notifier.NotificationType; import com.mucommander.ui.viewer.EditorRegistrar; import com.mucommander.ui.viewer.text.TextEditor; import com.mucommander.ui.viewer.text.TextFilesHistory; import lombok.extern.slf4j.Slf4j; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.Map; @Slf4j public class UserMenuAction extends ParentFolderAction { private UserMenuAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } @Override protected void toggleEnabledState() {} @Override public void performAction() { UserPopupMenu menu = createMenu(mainFrame); if (menu != null) { menu.show(mainFrame.getJFrame()); } } public static UserPopupMenu createMenu(MainFrame mainFrame) { AbstractFile currentFolder = mainFrame.getActiveTable().getFileTableModel().getCurrentFolder(); AbstractFile localMenu = findLocalMenu(currentFolder); if (localMenu != null) { try { return UserPopupMenuLoader.loadMenu(mainFrame, localMenu); } catch (LoadUserMenuException ej) { openEditorAndShowError(mainFrame, localMenu, ej); } catch (IOException e) { // TODO status bar log.error("Load menu exception", e); AbstractNotifier notifier = AbstractNotifier.getNotifier(); if (notifier != null) { notifier.displayNotification(NotificationType.JOB_ERROR, "Error", e.getMessage()); } } catch (Throwable t) { log.error("Load menu error", t); } } return null; } private static AbstractFile findLocalMenu(AbstractFile folder) { if (folder == null) { return null; } AbstractFile menu = getMenuFile(folder); return menu != null ? menu : findLocalMenu(folder.getParent()); } private static AbstractFile getMenuFile(AbstractFile folder) { if (folder == null || !folder.exists()) { return null; } try { AbstractFile result = folder.getChild(".trolcommander-menu.yml"); if (result != null && result.exists()) { return result; } result = folder.getChild(".trolcommander-menu.yaml"); return result != null && result.exists() ? result : null; } catch (IOException e) { return null; } } private static void openEditorAndShowError(MainFrame mainFrame, AbstractFile localMenu, LoadUserMenuException e) { TextFilesHistory.FileRecord historyRecord = TextFilesHistory.getInstance().get(localMenu); historyRecord.update(e.getLine(), e.getLine(), e.getColumn(), historyRecord.getFileType(), historyRecord.getEncoding()); Image image = ActionProperties.getActionIcon(EditAction.Descriptor.ACTION_ID).getImage(); log.info("open frame {} {}:{}", e.getMessage(), e.getLine(), e.getColumn()); EditorRegistrar.createEditorFrame(mainFrame, localMenu, image, (fileFrame) -> { TextEditor textEditor = (TextEditor)fileFrame.getFilePresenter(); textEditor.getStatusBar().showMessage(e.getMessage(), 1000); }); } @Override public ActionDescriptor getDescriptor() { return new UserMenuAction.Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "UserMenu"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.COMMANDS; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new UserMenuAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ViewAction.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * User configurable variant of {@link InternalViewAction}. * @author Maxence Bernard, Nicolas Rinaudo */ public class ViewAction extends InternalViewAction { // - Initialization ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Creates a new instance of ViewAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ public ViewAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); } // - AbstractViewerAction implementation --------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- @Override protected Command getCustomCommand(AbstractFile file) { return CommandManager.getCommandForAlias(CommandManager.VIEWER_ALIAS, file); } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "View"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ViewAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/impl/ViewAsAction.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2019 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.action.impl; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.AbstractActionDescriptor; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.quicklist.ViewAsQL; import javax.swing.ImageIcon; import javax.swing.KeyStroke; import java.awt.event.KeyEvent; import java.util.Map; /** * * @author Oleg Trifonov */ public class ViewAsAction extends SelectedFilesAction { /** * Creates a new instance of ViewAsAction. * @param mainFrame frame to which the action is attached. * @param properties action's properties. */ private ViewAsAction(MainFrame mainFrame, Map properties) { super(mainFrame, properties); ImageIcon icon = getStandardIcon(ViewAction.class); if (icon != null) { setIcon(icon); } } @Override public ActionDescriptor getDescriptor() { return new Descriptor(); } @Override public void performAction(FileSet files) { AbstractFile file = mainFrame.getActiveTable().getSelectedFile(false, true); // At this stage, no assumption should be made on the type of file that is allowed to be viewed/edited: // viewer/editor implementations will decide whether they allow a particular file or not. if (file == null || file.isDirectory()) { return; } new ViewAsQL(mainFrame, file).show(); } public static final class Descriptor extends AbstractActionDescriptor { public static final String ACTION_ID = "ViewAs"; public String getId() { return ACTION_ID; } public ActionCategory getCategory() { return ActionCategory.FILES; } public KeyStroke getDefaultAltKeyStroke() { return null; } public KeyStroke getDefaultKeyStroke() { return KeyStroke.getKeyStroke(KeyEvent.VK_F3, KeyEvent.SHIFT_DOWN_MASK); } public TcAction createAction(MainFrame mainFrame, Map properties) { return new ViewAsAction(mainFrame, properties); } } } ================================================ FILE: src/main/java/com/mucommander/ui/action/package.html ================================================ Contains all actions used to interact with muCommander. ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/AutocompleterTextComponent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import com.mucommander.ui.combobox.EditableComboBox; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.ArrayList; import java.util.List; /** * AutocompleterTextComponent convert any text component to auto-completion supported text component. * In order to support auto-completion two abstract methods need to be implemented: *

      *
    • OnEnterPressed - What should the text component do when enter key is pressed and there is no item selected on the auto-completion popup list
    • *
    • OnEscPressed - What should the text component do when escape key is pressed and there is no item selected on the auto-completion popup list
    • *
    * * @author Arik Hadas */ public abstract class AutocompleterTextComponent { private JTextComponent textComponent; private EditableComboBox editableComboBox = null; public AutocompleterTextComponent(JTextComponent textComp) { this.textComponent = textComp; } protected AutocompleterTextComponent(EditableComboBox editableComboBox) { this.textComponent = editableComboBox.getTextField(); this.editableComboBox = editableComboBox; // Remove all key listeners which are defined for the EditableCombobox removeAllKeyListeners(); } // Abstract methods: /** * This function will be called when the text component has the focus and enter key is * pressed, while the auto-completion popup window is unvisible. */ public abstract void OnEnterPressed(KeyEvent keyEvent); /** * This function will be called when the text component has the focus and escape key is * pressed, while the auto-completion popup window is unvisible. */ public abstract void OnEscPressed(KeyEvent keyEvent); private void removeAllKeyListeners() { KeyListener[] l = editableComboBox.getTextField().getKeyListeners(); for (KeyListener aL : l) { editableComboBox.getTextField().removeKeyListener(aL); } } // Methods of the text component which are used by the auto-completion mechanism: public Document getDocument() { return textComponent.getDocument(); } public boolean isShowing() { return textComponent.isShowing(); } public void setText(String text) { textComponent.setText(text); } public String getText() { return textComponent.getText(); } public boolean hasFocus() { return textComponent.hasFocus(); } public boolean isEnabled() { return textComponent.isEnabled(); } public int getCaretPosition() { return textComponent.getCaretPosition(); } public void requestFocus() { textComponent.requestFocus(); } public int getHeight() { return textComponent.getHeight(); } Rectangle modelToView() throws BadLocationException { int pos = textComponent.getCaretPosition(); return textComponent.modelToView2D(pos).getBounds(); } void moveCaretToEndOfText() { textComponent.setCaretPosition(textComponent.getText().length()); } boolean isCaretAtEndOfTextAtInsertion() { return textComponent.getCaretPosition() == textComponent.getText().length() - 1; } boolean isCarentAtEndOfTextAtRemoval() { return textComponent.getCaretPosition() == textComponent.getText().length() + 1; } JTextComponent getTextComponent() { return textComponent; } public void addKeyListener(KeyAdapter adapter) { textComponent.addKeyListener(adapter); } public void addFocusListener(FocusListener listener) { textComponent.addFocusListener(listener); } /** * getItemsNames * * @return empty Vector if component is not an EditableComboBox, otherwise return Vector which contains the names of the combobox items. */ public List getItemNames() { List result = new ArrayList<>(); if (editableComboBox != null) { int nbItems = editableComboBox.getItemCount(); for (int i = 0; i < nbItems; i++) { result.add(editableComboBox.getItemAt(i).toString()); } } return result; } /** * isPopupVisible * * @return false if component is not an EditableComboBox, otherwise, true if the combo-box list of items is visible. */ public boolean isComponentsPopupVisible() { return editableComboBox != null && editableComboBox.isPopupVisible(); } /** * setPopupUnvisibe - make the combo-box list of items invisible. */ void setComponentsPopupInvisible() { if (editableComboBox != null) { editableComboBox.setPopupVisible(false); } } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/BasicAutocompleterTextComponent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import javax.swing.text.JTextComponent; import java.awt.event.KeyEvent; /** * This AutocompleterTextComponent implements {@link #OnEnterPressed(java.awt.event.KeyEvent)} * and {@link #OnEscPressed(java.awt.event.KeyEvent)} as no-ops. * * @author Maxence Bernard */ public class BasicAutocompleterTextComponent extends AutocompleterTextComponent { public BasicAutocompleterTextComponent(JTextComponent textComp) { super(textComp); } /////////////////////////////////////////////// // AutocompleterTextComponent implementation // /////////////////////////////////////////////// @Override public void OnEnterPressed(KeyEvent keyEvent) { } @Override public void OnEscPressed(KeyEvent keyEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/CompleterFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import com.mucommander.ui.autocomplete.completers.ComboboxOptionsCompleter; import com.mucommander.ui.autocomplete.completers.Completer; import com.mucommander.ui.autocomplete.completers.LocationCompleter; import com.mucommander.ui.autocomplete.completers.PathCompleter; /** * A factory class to produce completers. * * @author Arik Hadas */ public class CompleterFactory { public static Completer getComboboxOptionsCompleter() { return new ComboboxOptionsCompleter(); } public static Completer getPathCompleter() { return new PathCompleter(); } public static Completer getLocationCompleter() { return new LocationCompleter(); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/CompletionType.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import com.mucommander.ui.autocomplete.completers.Completer; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * AutoCompletionType defines the behavior of the auto-completion, such as: * - The key(s) that will open the popup window * - When should the documentListener be attached to the text component * - etc.. * This abstract class contains the common things to all CompleterType implementations. * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public abstract class CompletionType { private Completer completer; AutocompleterTextComponent autocompletedtextComp; DocumentListener documentListener; protected JList list = new JList<>(); protected final JPopupMenu popup = new JPopupMenu(); ShowingThread showingThread; // Constants: final int VISIBLE_ROW_COUNT = 10; private final int POPUP_DELAY_AT_TEXT_INSERTION = 1500; private final int POPUP_DELAY_AT_TEXT_DELETION = 500; private final int POPUP_DELAY_AFTER_ACCEPTING_LIST_ITEM = 1500; /** * ShowingThread is an abstract class for threads that show auto-completion popup * window after a given delay. * Each implementation of ShowingThread should implements an abstract function * "showPopup" that contains the popup opening. * * @author Arik Hadas */ protected abstract class ShowingThread extends Thread { boolean isStopped; int delayTime; ShowingThread(int delayTime) { isStopped = false; this.delayTime = delayTime; } @Override public void run() { // Hide the auto-completion popup window. hideAutocompletionPopup(); if (!autocompletedtextComp.isShowing() || !autocompletedtextComp.isEnabled()) return; // Sleep for delayTime milieconds. delay(delayTime); // If this thread should stop, finish its execution. if (isStopped) return; // Show auto-completion popup window. showAutocompletionPopup(); } /** * Stop this thread execution. */ public void done() { isStopped = true; } /** * Cause this thread sleep for the given time (in miliseconds). */ protected void delay(int miliseconds) { if (miliseconds > 0) { try { Thread.sleep(miliseconds); } catch (InterruptedException ignore) { } } } abstract void showAutocompletionPopup(); } CompletionType(AutocompleterTextComponent comp, Completer completer) { autocompletedtextComp = comp; this.completer = completer; // JScrollPane scroll = new JScrollPane(list); // Disable horizontal scrolling because they would sometimes appear under Mac OS X even though there was // plenty of horizontal space to display the list. JScrollPane scroll = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scroll.setBorder(null); list.setFocusable( false ); popup.setFocusable( false ); scroll.getVerticalScrollBar().setFocusable( false ); scroll.getHorizontalScrollBar().setFocusable( false ); popup.setBorder(BorderFactory.createLineBorder(Color.black)); popup.add(scroll); createDocumentListener(); addMouseListenerToList(); list.setRequestFocusEnabled(false); autocompletedtextComp.addFocusListener(new FocusListener() { public void focusGained(FocusEvent e) { } public void focusLost(FocusEvent e) { hideAutocompletionPopup(); } }); } // abstract methods: /** * Start a new thread that implement ShowingThread with the given delay. */ protected abstract void startNewShowingThread(int delay); /** * Hide the auto-completion popup window. */ protected abstract void hideAutocompletionPopup(); /** * update auto-completion popup's list model depending on the data in text component. * * @param list - Auto-completion popup's list. * @return true if the list was updated successfully, false otherwise. */ boolean updateListData(JList list) { return completer.updateListData(list, autocompletedtextComp); } /** * user has selected some item from the auto-completion popup list, * update text component accordingly. * * @param selected - The selected item from the auto-completion popup's list. */ private void updateTextComponent(String selected) { completer.updateTextComponent(selected, autocompletedtextComp); } /** * createNewShowingThread shows the auto-completion popup window after * a non-blocking delay. * * @param delay - The requested delay (in miliseconds) until the popup appear. */ void createNewShowingThread(int delay) { // stop current showing thread (if exist) if (showingThread != null) showingThread.done(); // start new showing thread startNewShowingThread(delay); } /** * The function handles the case when an item of the auto-copmpletion popup was selected. * it ask the text component to be updated according to the selected item and create * a new thread that will open auto-completion popup windos after delay of * POPUP_DELAY_AFTER_ACCEPTING_LIST_ITEM seconds. */ protected void acceptListItem(String selected) { updateTextComponent(selected); createNewShowingThread(POPUP_DELAY_AFTER_ACCEPTING_LIST_ITEM); } private void addMouseListenerToList() { list.addMouseListener(new MouseListener() { public void mouseClicked(MouseEvent e) { // If there was double click on item of the popup's list, // select it, and update the text component. if (e.getClickCount() == 2) { int index = list.locationToIndex(e.getPoint()); list.setSelectedIndex(index); acceptListItem(list.getSelectedValue()); } } public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public void mousePressed(MouseEvent e) {} }); } private void createDocumentListener() { documentListener = new DocumentListener(){ public void insertUpdate(DocumentEvent e){ // If text was inserted to the text component and carent is at the end of // the text, then start a showingThread to open auto-completion popup. if (autocompletedtextComp.isCaretAtEndOfTextAtInsertion()) createNewShowingThread(popup.isVisible() ? 0 : POPUP_DELAY_AT_TEXT_INSERTION); } public void removeUpdate(DocumentEvent e){ // If text was deleted from the text component and carent is at the end of // the text, then start a showingThread to open auto-completion popup. if (autocompletedtextComp.isCarentAtEndOfTextAtRemoval()) createNewShowingThread(popup.isVisible() ? 0 : POPUP_DELAY_AT_TEXT_DELETION); } public void changedUpdate(DocumentEvent e){ } }; } /** * Returns true if the auto-completion popup window is visible. */ boolean isPopupListShowing() { return popup.isShowing(); } /** * Returns true if there is a selected item at the auto-completion popup window. */ boolean isItemSelectedAtPopupList() { return popup.isShowing() && list.getSelectedIndex() >= 0; } /** * Selects the first item in the list. */ void selectFirstValue() { list.setSelectedIndex(0); list.ensureIndexIsVisible(0); } /** * Selects the last item in the list. */ void selectLastValue() { int lastIndex = list.getModel().getSize() - 1; list.setSelectedIndex(lastIndex); list.ensureIndexIsVisible(lastIndex); } /** * Selects the item at (VISIBLE_ROW_COUNT - 1) places after the currently * selected item in the list. */ void selectNextPage() { int si = list.getSelectedIndex(); int nextIndex = 0; if (si >= 0) { nextIndex = Math.min(si + VISIBLE_ROW_COUNT - 1, list.getModel().getSize() - 1); } list.setSelectedIndex(nextIndex); list.ensureIndexIsVisible(nextIndex); } /** * Selects the item at (VISIBLE_ROW_COUNT - 1) places before the currently * selected item in the list. */ void selectPreviousPage() { int si = list.getSelectedIndex(); if (si > 0){ int nextIndex = Math.max(si - (VISIBLE_ROW_COUNT - 1), 0); list.setSelectedIndex(nextIndex); list.ensureIndexIsVisible(nextIndex); } } /** * Selects the next item in the list. It won't change the selection if the * currently selected item is already the last item. */ void selectNextPossibleValue(){ int si = list.getSelectedIndex(); if (si < list.getModel().getSize() - 1) { int nextIndex = si + 1; list.setSelectedIndex(nextIndex); list.ensureIndexIsVisible(nextIndex); } } /** * Selects the previous item in the list. It won't change the selection if the * currently selected item is already the first item. */ void selectPreviousPossibleValue(){ int si = list.getSelectedIndex(); if (si > 0){ int nextIndex = si - 1; list.setSelectedIndex(nextIndex); list.ensureIndexIsVisible(nextIndex); } } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/EditableComboboxCompletion.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.text.BadLocationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.ui.autocomplete.completers.Completer; /** * EditableComboboxCompleter is a CompleterType which suite to editable combobox. * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public class EditableComboboxCompletion extends CompletionType { private static final Logger LOGGER = LoggerFactory.getLogger(EditableComboboxCompletion.class); private class ShowingThreadImp extends ShowingThread { ShowingThreadImp(int delay) { super(delay); } @Override void showAutocompletionPopup() { if (autocompletedtextComp.isShowing() && autocompletedtextComp.isEnabled() && updateListData(list)) { list.setVisibleRowCount(Math.min(list.getModel().getSize() ,VISIBLE_ROW_COUNT)); int x; try{ x = autocompletedtextComp.modelToView().x; } catch(BadLocationException e){ // this should never happen!!! LOGGER.debug("Caught exception", e); return; } if (autocompletedtextComp.hasFocus()) { if (!isStopped) { list.ensureIndexIsVisible(0); synchronized(popup) { popup.show(autocompletedtextComp.getTextComponent(), x, autocompletedtextComp.getHeight()); // probably because of swing's bug, sometimes the popup window looks // as a gray rectangle - repainting solves it. popup.repaint(); } } } } } } public EditableComboboxCompletion(AutocompleterTextComponent comp, Completer completer){ super(comp, completer); autocompletedtextComp.getDocument().addDocumentListener(documentListener); autocompletedtextComp.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent keyEvent) { switch(keyEvent.getKeyCode()) { case KeyEvent.VK_ENTER: if (isItemSelectedAtPopupList()) { hideAutocompletionPopup(); acceptListItem(list.getSelectedValue()); keyEvent.consume(); } else { // Stop the active showing-thread to prevent suggestions-popup // opening after the operation was initiated. if (showingThread != null) { showingThread.done(); } autocompletedtextComp.OnEnterPressed(keyEvent); } break; case KeyEvent.VK_ESCAPE: if (isPopupListShowing()) { if (autocompletedtextComp.isEnabled()) { hideAutocompletionPopup(); } keyEvent.consume(); } else autocompletedtextComp.OnEscPressed(keyEvent); break; case KeyEvent.VK_UP: if (autocompletedtextComp.isEnabled() && popup.isVisible()) { selectPreviousPossibleValue(); keyEvent.consume(); } break; case KeyEvent.VK_DOWN: if (autocompletedtextComp.isEnabled()){ if (popup.isVisible()) { selectNextPossibleValue(); keyEvent.consume(); } } break; case KeyEvent.VK_SPACE: // The combination of cntrl+space makes open the auto-complete popup without delay. if (keyEvent.isControlDown()) { if (!popup.isVisible()) { autocompletedtextComp.setComponentsPopupInvisible(); autocompletedtextComp.moveCaretToEndOfText(); createNewShowingThread(0); } } break; case KeyEvent.VK_PAGE_DOWN: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectNextPage(); keyEvent.consume(); } break; case KeyEvent.VK_PAGE_UP: if(autocompletedtextComp.isEnabled() && isItemSelectedAtPopupList()) { selectPreviousPage(); keyEvent.consume(); } break; case KeyEvent.VK_HOME: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectFirstValue(); keyEvent.consume(); } break; case KeyEvent.VK_END: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectLastValue(); keyEvent.consume(); } break; case KeyEvent.VK_LEFT: hideAutocompletionPopup(); break; default: } } }); } @Override protected void hideAutocompletionPopup() { synchronized (popup) { if (popup.isVisible()) popup.setVisible(false); } } @Override protected void startNewShowingThread(int delay) { (showingThread = new ShowingThreadImp(delay)).start(); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/OtherTextComponentCompletion.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.text.BadLocationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.ui.autocomplete.completers.Completer; /** * OtherTextComponentCompleter is a CompleterType which suite to text components * other than editable combobox and text-field (such as textArea for example). * * This class was not tested. * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public class OtherTextComponentCompletion extends CompletionType { private static final Logger LOGGER = LoggerFactory.getLogger(OtherTextComponentCompletion.class); private class ShowingThreadImp extends ShowingThread { ShowingThreadImp(int delay) { super(delay); } @Override void showAutocompletionPopup() { if (autocompletedtextComp.isShowing() && autocompletedtextComp.isEnabled() && updateListData(list)){ list.setVisibleRowCount(Math.min(list.getModel().getSize() ,VISIBLE_ROW_COUNT)); int x; try{ x = autocompletedtextComp.modelToView().x; } catch(BadLocationException e){ // this should never happen!!! LOGGER.debug("Caught exception", e); return; } if (autocompletedtextComp.hasFocus()) { if (!isStopped) { list.ensureIndexIsVisible(0); synchronized(popup) { popup.show(autocompletedtextComp.getTextComponent(), x, autocompletedtextComp.getHeight()); // probably because of swing's bug, sometimes the popup window looks // as a gray rectangle - repainting solves it. popup.repaint(); } autocompletedtextComp.getDocument().addDocumentListener(documentListener); } } } } } public OtherTextComponentCompletion(AutocompleterTextComponent comp, Completer completer){ super(comp, completer); autocompletedtextComp.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent keyEvent) { switch(keyEvent.getKeyCode()) { case KeyEvent.VK_ENTER: boolean itemSelected = isItemSelectedAtPopupList(); // Notify listeners that the text field has been validated if (itemSelected) { hideAutocompletionPopup(); acceptListItem(list.getSelectedValue()); keyEvent.consume(); } else autocompletedtextComp.OnEnterPressed(keyEvent); break; case KeyEvent.VK_ESCAPE: if (isPopupListShowing()) { if (autocompletedtextComp.isEnabled()) hideAutocompletionPopup(); keyEvent.consume(); } else autocompletedtextComp.OnEscPressed(keyEvent); break; case KeyEvent.VK_UP: if(autocompletedtextComp.isEnabled() && popup.isVisible()) { selectPreviousPossibleValue(); keyEvent.consume(); } break; case KeyEvent.VK_DOWN: if(autocompletedtextComp.isEnabled()){ if(popup.isVisible()) { selectNextPossibleValue(); keyEvent.consume(); } } break; case KeyEvent.VK_SPACE: if (keyEvent.isControlDown()) { if (!popup.isVisible()) { autocompletedtextComp.moveCaretToEndOfText(); createNewShowingThread(0); } } break; case KeyEvent.VK_PAGE_DOWN: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectNextPage(); keyEvent.consume(); } break; case KeyEvent.VK_PAGE_UP: if(autocompletedtextComp.isEnabled() && isItemSelectedAtPopupList()) { selectPreviousPage(); keyEvent.consume(); } break; case KeyEvent.VK_HOME: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectFirstValue(); keyEvent.consume(); } break; case KeyEvent.VK_END: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectLastValue(); keyEvent.consume(); } break; case KeyEvent.VK_LEFT: hideAutocompletionPopup(); break; default: } } }); } @Override protected void hideAutocompletionPopup() { synchronized (popup) { if (popup.isVisible()) popup.setVisible(false); autocompletedtextComp.getDocument().removeDocumentListener(documentListener); } } @Override protected void startNewShowingThread(int delay) { (showingThread = new ShowingThreadImp(delay)).start(); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/TextFieldCompletion.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.text.BadLocationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.ui.autocomplete.completers.Completer; /** * TextFieldCompleter is a CompleterType which suite to text-field. * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public class TextFieldCompletion extends CompletionType { private static final Logger LOGGER = LoggerFactory.getLogger(TextFieldCompletion.class); private class ShowingThreadImp extends ShowingThread { ShowingThreadImp(int delay) { super(delay); } @Override void showAutocompletionPopup() { if (autocompletedtextComp.isShowing() && autocompletedtextComp.isEnabled() && updateListData(list)){ list.setVisibleRowCount(Math.min(list.getModel().getSize() ,VISIBLE_ROW_COUNT)); int x; try{ x = autocompletedtextComp.modelToView().x; } catch(BadLocationException e){ // this should never happen!!! LOGGER.debug("Caught exception", e); return; } if (autocompletedtextComp.hasFocus()) { if (!isStopped) { list.ensureIndexIsVisible(0); synchronized(popup) { popup.show(autocompletedtextComp.getTextComponent(), x, autocompletedtextComp.getHeight()); // probably because of swing's bug, sometimes the popup window looks // as a gray rectangle - repainting solves it. popup.repaint(); } } } } } } public TextFieldCompletion(AutocompleterTextComponent comp, Completer completer){ super(comp, completer); autocompletedtextComp.getDocument().addDocumentListener(documentListener); autocompletedtextComp.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent keyEvent) { switch (keyEvent.getKeyCode()) { case KeyEvent.VK_ENTER: if (isItemSelectedAtPopupList()) { hideAutocompletionPopup(); acceptListItem(list.getSelectedValue()); keyEvent.consume(); } else autocompletedtextComp.OnEnterPressed(keyEvent); break; case KeyEvent.VK_ESCAPE: if (isPopupListShowing()) { if (autocompletedtextComp.isEnabled()) hideAutocompletionPopup(); keyEvent.consume(); } else autocompletedtextComp.OnEscPressed(keyEvent); break; case KeyEvent.VK_UP: if(autocompletedtextComp.isEnabled() && popup.isVisible()) { selectPreviousPossibleValue(); keyEvent.consume(); } break; case KeyEvent.VK_SPACE: // The combination of cntrl+space makes open the auto-complete popup without delay. if (keyEvent.isControlDown()) { if (!popup.isVisible()) { autocompletedtextComp.moveCaretToEndOfText(); createNewShowingThread(0); } } break; case KeyEvent.VK_DOWN: if(autocompletedtextComp.isEnabled()){ if(popup.isVisible()) { selectNextPossibleValue(); keyEvent.consume(); } else { autocompletedtextComp.moveCaretToEndOfText(); createNewShowingThread(0); } } break; case KeyEvent.VK_PAGE_DOWN: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectNextPage(); keyEvent.consume(); } break; case KeyEvent.VK_PAGE_UP: if(autocompletedtextComp.isEnabled() && isItemSelectedAtPopupList()) { selectPreviousPage(); keyEvent.consume(); } break; case KeyEvent.VK_HOME: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectFirstValue(); keyEvent.consume(); } break; case KeyEvent.VK_END: if(autocompletedtextComp.isEnabled() && isPopupListShowing()) { selectLastValue(); keyEvent.consume(); } break; case KeyEvent.VK_LEFT: hideAutocompletionPopup(); break; default: } } }); } @Override protected void startNewShowingThread(int delay) { (showingThread = new ShowingThreadImp(delay)).start(); } @Override protected void hideAutocompletionPopup() { synchronized (popup) { if (popup.isVisible()) popup.setVisible(false); } } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/TypicalAutocompleterEditableCombobox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete; import com.mucommander.ui.combobox.AutocompleteEditableCombobox; import java.awt.event.KeyEvent; /** * This AutocompleterTextComponent implements {@link #OnEnterPressed(java.awt.event.KeyEvent)} * and {@link #OnEscPressed(java.awt.event.KeyEvent)} as the typical AutocompleteEditableCombobox's ops. * * @author Arik Hadas */ public class TypicalAutocompleterEditableCombobox extends AutocompleterTextComponent { private AutocompleteEditableCombobox editableCombobox; public TypicalAutocompleterEditableCombobox(AutocompleteEditableCombobox editableCombobox) { super(editableCombobox); this.editableCombobox = editableCombobox; } @Override public void OnEnterPressed(KeyEvent keyEvent) { editableCombobox.respondToEnterKeyPressing(keyEvent); } @Override public void OnEscPressed(KeyEvent keyEvent) { editableCombobox.respondToEscapeKeyPressing(keyEvent); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/ComboboxOptionsCompleter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers; import com.mucommander.ui.autocomplete.AutocompleterTextComponent; import com.mucommander.ui.autocomplete.completers.services.PrefixFilter; import java.util.List; import java.util.Vector; /** * ComboboxOptionsCompleter is a Completer based on the items of combo-box. * * @author Arik Hadas */ public class ComboboxOptionsCompleter extends Completer { public ComboboxOptionsCompleter() { } @Override protected List getUpdatedSuggestions(AutocompleterTextComponent component) { return PrefixFilter.createPrefixFilter(component.getText()).filter(component.getItemNames()); } @Override public void updateTextComponent(final String selected, AutocompleterTextComponent comp) { comp.setText(selected); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/Completer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.autocomplete.AutocompleterTextComponent; import com.mucommander.ui.autocomplete.completers.services.CompletionService; import javax.swing.*; import java.net.MalformedURLException; import java.util.*; /** * Interface that each type of completion must implement. * It defines 2 methods: * *
      *
    • updateListData - Update what the auto-completion popup shows, depending on the data in text component
    • *
    • updateTextComponent - Update the text component's text according to the selected item from the auto-completion popup
    • *
    * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public abstract class Completer { private final Set services = new LinkedHashSet<>(); /** * This function gets an AutocompleterTextComponent and returns a Vector of suggestions for * completion, for the component's value. * * @param component - an AutocompleterTextComponent. * @return Vector of suggestions for completion. */ protected abstract List getUpdatedSuggestions(AutocompleterTextComponent component); /** * update list model depending on the data in text component * * @param list - auto-completion popup's list that should be updated. * @param comp - text component * @return true if an auto-completion popup with the updated list should be shown, false otherwise. */ public boolean updateListData(final JList list, AutocompleterTextComponent comp) { list.setListData(getUpdatedSuggestions(comp).toArray(String[]::new)); if (list.getModel().getSize() == 1) { try { String typedFilename = FileURL.getFileURL(comp.getText()).getFilename(); // in case the suggestions-list contains only one suggestion, and it // match the typed path - do not show an auto-completion popup. if (typedFilename == null || typedFilename.equalsIgnoreCase(list.getModel().getElementAt(0))) return false; } catch (MalformedURLException e) { e.printStackTrace(); } } return list.getModel().getSize() > 0; } /** * update text component according to the given string. * * @param selected - selected item from auto-completion popup list. * @param comp - text component. */ public abstract void updateTextComponent(final String selected, AutocompleterTextComponent comp); /** * Add service to this completer. * * The order in which the services is being registered is important, * see: tryToCompleteFromServices. * * @param service - Service to be added. */ protected void registerService(CompletionService service) { services.add(service); } /** * Gather the possible completions for the given path from * all the services registered to this completer. * * @param path - The path to be completed. * @return Vector that contain all the possible completions * which were retured from the registered services. */ protected Vector getPossibleCompletionsFromServices(String path) { Vector result = new Vector<>(); for (CompletionService service : services) result.addAll(service.getPossibleCompletions(path)); return result; } /** * Given the selected string (from the auto-completion's list), try to * get a completion from the registered services. * * The first completion that found will be returned, thus the order in which * the services are registered is significant. * * @param selectedString - selected string (from the auto-completion's list). * @return null if could not found any completion for the given string * from the registered services, otherwise the founded completion is returned. */ protected String tryToCompleteFromServices(String selectedString) { for (CompletionService service : services) { String location = (service).complete(selectedString); if (location != null) { return location; } } return null; } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/LocationCompleter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers; import com.mucommander.ui.autocomplete.AutocompleterTextComponent; import java.util.Vector; /** * LocationCompleter is a Completer based on locations, meaning root folders, * browsable file paths, bookmarks and system variables. * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public class LocationCompleter extends Completer { public LocationCompleter(){ registerService(ServiceFactory.getVolumesService()); registerService(ServiceFactory.getBrowsableFilesService()); registerService(ServiceFactory.getBookmarksService()); registerService(ServiceFactory.getSystemVariablesService()); } @Override protected Vector getUpdatedSuggestions(AutocompleterTextComponent component) { return getPossibleCompletionsFromServices(component.getText()); } @Override public void updateTextComponent(final String selected, AutocompleterTextComponent comp){ if (selected == null) { return; } String location = tryToCompleteFromServices(selected); if (comp.isEnabled() && location != null) { comp.setText(location); } } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/PathCompleter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.autocomplete.AutocompleterTextComponent; import java.util.Vector; /** * FileCompleter is a Completer based on root folders and file paths. * * @author Arik Hadas, based on the code of Santhosh Kumar: http://www.jroller.com/santhosh/entry/file_path_autocompletion */ public class PathCompleter extends Completer { private String currentLocation; public PathCompleter() { super(); registerService(ServiceFactory.getVolumesService()); registerService(ServiceFactory.getAllFilesService()); } @Override protected Vector getUpdatedSuggestions(AutocompleterTextComponent component) { String text = component.getText(); Vector result = getPossibleCompletionsFromServices(text); if (currentLocation != null) { result.addAll(getPossibleCompletionsFromServices(currentLocation + text)); } return result; } @Override public void updateTextComponent(final String selected, AutocompleterTextComponent comp){ if (selected == null) { return; } String location = tryToCompleteFromServices(selected); if (comp.isEnabled() && location != null) { comp.setText(location); } } public void setCurrentLocation(AbstractFile currentLocation) { this.currentLocation = currentLocation.getAbsolutePath(); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/ServiceFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.ui.autocomplete.completers.services.AllFilesService; import com.mucommander.ui.autocomplete.completers.services.BookmarksService; import com.mucommander.ui.autocomplete.completers.services.CompletionService; import com.mucommander.ui.autocomplete.completers.services.FilteredFilesService; import com.mucommander.ui.autocomplete.completers.services.SystemVariablesService; import com.mucommander.ui.autocomplete.completers.services.VolumesService; /** * A factory class to produce completion-services. * * @author Arik Hadas */ public class ServiceFactory { public static CompletionService getAllFilesService() { return new AllFilesService(); } public static CompletionService getBrowsableFilesService() { return new FilteredFilesService(new AttributeFileFilter(FileAttribute.BROWSABLE)); } public static CompletionService getVolumesService() { return new VolumesService(); } public static CompletionService getBookmarksService() { return new BookmarksService(); } public static CompletionService getSystemVariablesService() { return new SystemVariablesService(); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/services/AllFilesService.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers.services; import com.mucommander.commons.file.AbstractFile; import java.io.IOException; /** * This FilesService returns all the files in a given directory. * * @author Arik Hadas */ public class AllFilesService extends FilesService { @Override protected AbstractFile[] getFiles(AbstractFile directory) throws IOException { return directory.ls(); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/services/BookmarksService.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers.services; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkListener; import com.mucommander.bookmark.BookmarkManager; import java.util.Arrays; import java.util.Vector; /** * This CompletionService handles bookmarks completion. * * @author Arik Hadas */ public class BookmarksService implements CompletionService, BookmarkListener { private String[] sortedBookmarkNames; private String[] sortedBookmarkLocations; public BookmarksService() { fetchBookmarks(); // Register as a bookmark-listener, in order to be up-to-date with existing bookmarks. BookmarkManager.addBookmarkListener(this); } public Vector getPossibleCompletions(String path) { Vector result = new Vector<>(); PrefixFilter filter = PrefixFilter.createPrefixFilter(path); result.addAll(filter.filter(sortedBookmarkNames)); result.addAll(filter.filter(sortedBookmarkLocations)); return result; } public String complete(String selectedCompletion) { String result = null; int nbBookmarks = sortedBookmarkNames.length; int i; for (i = 0; i < nbBookmarks; i++) if (sortedBookmarkNames[i].equalsIgnoreCase(selectedCompletion)) { result = sortedBookmarkNames[i]; break; } for (i = 0; i < nbBookmarks; i++) if (sortedBookmarkLocations[i].equalsIgnoreCase(selectedCompletion)) { result = sortedBookmarkLocations[i]; break; } return result; } /** * Returns a sorted array of bookmarks names. * * @return a sorted array of bookmarks names */ private String[] getSortedBookmarkNames() { Vector bookmarks = BookmarkManager.getBookmarks(); int nbBookmarks = bookmarks.size(); String[] result = new String[nbBookmarks]; for (int i=0; i. */ package com.mucommander.ui.autocomplete.completers.services; import java.util.List; /** * CompletionService is used to handle completions according to certain criteria. * It defines 2 methods: * *
      *
    • getPossibleCompletions - return possible completions for a given path
    • *
    • complete - return a path corresponding to a given completion
    • *
    * * @author Arik Hadas */ public interface CompletionService { /** * Return a group of suggested completions corresponding to the given path, * according to this service's criteria. * * @param path - a path. * @return a Vector of possible completions. */ List getPossibleCompletions(String path); /** * If the given completion match one of my suggested completions, return * a corresponding path, null otherwise. * * @param selectedCompletion - string that represent a completion. * @return a path if the given completion was suggested by this service, null otherwise. */ String complete(String selectedCompletion); } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/services/FilesService.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers.services; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; /** * This CompletionService handles file paths completion. * * @author Arik Hadas */ public abstract class FilesService implements CompletionService { private static final Logger LOGGER = LoggerFactory.getLogger(FilesService.class); private String cachedDirectoryName; private String[] cachedDirectoryFileNames; private long cachedDirectoryDate; public FilesService() { cachedDirectoryFileNames = new String[0]; cachedDirectoryDate = -1; } /** * This abstract function gets a directory and should return it's children * files that match a certain criteria. * * @param directory - a directory. * @return subgroup of the given directory's children files. * @throws IOException */ protected abstract AbstractFile[] getFiles(AbstractFile directory) throws IOException; public List getPossibleCompletions(String path) { List result = new ArrayList<>(); int index = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/')); if (index != -1) { String currentDirectoryName = path.substring(0, index+1); AbstractFile currentDirectory = FileFactory.getFile(currentDirectoryName); if (currentDirectory != null && currentDirectory.exists()) { long currentDirectoryDate = currentDirectory.getLastModifiedDate(); if (cachedDirectoryName == null || !cachedDirectoryName.equals(currentDirectoryName) || currentDirectoryDate != cachedDirectoryDate) { AbstractFile[] currentDirectoryFiles; try { currentDirectoryFiles = getFiles(currentDirectory); } catch (IOException e) { LOGGER.debug("Caught exception", e); return new ArrayList<>(); } int nbCurrentDirectoryFiles = currentDirectoryFiles.length; cachedDirectoryFileNames = new String[nbCurrentDirectoryFiles]; for (int i=0; i. */ package com.mucommander.ui.autocomplete.completers.services; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; import java.io.IOException; /** * This FilesService returns filtered files in a given directory, * according to a certain FileFilter. * * @author Arik Hadas */ public class FilteredFilesService extends FilesService { private FileFilter fileFilter; public FilteredFilesService(FileFilter fileFilter) { this.fileFilter = fileFilter; } @Override protected AbstractFile[] getFiles(AbstractFile directory) throws IOException { return fileFilter.filter(directory.ls()); } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/services/PrefixFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers.services; import java.util.ArrayList; import java.util.List; /** * A PrefixFilter matches strings that start with certain prefix. * * @author Arik Hadas */ public class PrefixFilter { private final String prefix; private PrefixFilter(String prefix) { this.prefix = prefix!=null ? prefix.toLowerCase() : null; } /** * * @param prefix - The prefix that each string should start with in order to pass this IMAGE_FILTER. * @return A IMAGE_FILTER of the given prefix. */ public static PrefixFilter createPrefixFilter(String prefix) { return new PrefixFilter(prefix); } /** * * @param input - Some string. * @return true if the given input was accepted by this IMAGE_FILTER, false otherwise. */ public boolean accept(String input) { return prefix == null || input.toLowerCase().startsWith(prefix); } /** * Convenient method that filters out strings that do not start with this IMAGE_FILTER's prefix. * * @param strings - Array of strings. * @return Vector of strings which start with this IMAGE_FILTER's prefix. */ public List filter(String[] strings) { List result = new ArrayList<>(); for (String s : strings) { if (accept(s)) result.add(s); } return result; } /** * Convenient method that filters out strings that do not start with this IMAGE_FILTER's prefix. * * @param strings - Vector of strings. * @return Vector of strings which start with this IMAGE_FILTER's prefix. */ public List filter(List strings) { List result = new ArrayList<>(); for (String s : strings) { if (accept(s)) result.add(s); } return result; } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/services/SystemVariablesService.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers.services; import java.util.*; /** * This CompletionService handles system variables completion. * * @author Arik Hadas */ public class SystemVariablesService implements CompletionService { private final String[] cachedKeyNames; public SystemVariablesService() { Set keys = System.getenv().keySet(); int nbKeys = keys.size(); cachedKeyNames = new String[nbKeys]; Iterator iter = keys.iterator(); for (int i = 0; i < nbKeys; i++) cachedKeyNames[i] = "$" + iter.next(); Arrays.sort(cachedKeyNames, String.CASE_INSENSITIVE_ORDER); } public List getPossibleCompletions(String path) { return PrefixFilter.createPrefixFilter(path).filter(cachedKeyNames); } public String complete(String selectedCompletion) { for (String cachedKeyName : cachedKeyNames) if (cachedKeyName.equalsIgnoreCase(selectedCompletion)) { return cachedKeyName; } return null; } } ================================================ FILE: src/main/java/com/mucommander/ui/autocomplete/completers/services/VolumesService.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.autocomplete.completers.services; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import java.util.Arrays; import java.util.List; import java.util.Vector; /** * This CompletionService handles root folders completion. * * @author Arik Hadas */ public class VolumesService implements CompletionService { private List lastSuggestedCompletions = new Vector<>(); public VolumesService() {} /** * Resolves and returns a sorted array of root (top level) folder names. Those folders are purposively not cached * so that newly mounted folders will be returned. * * @return a sorted array of root folder names */ public List getPossibleCompletions(String path) { lastSuggestedCompletions.clear(); int index = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/')); if (index == -1) { AbstractFile[] fileRoots = LocalFile.getVolumes(); int nbFolders = fileRoots.length; String[] rootFolderNames = new String[nbFolders]; for (int i=0; i. */ package com.mucommander.ui.border; import javax.swing.border.LineBorder; import java.awt.*; /** * Implementation of LineBorder that allows applications to change the color after it's been instantiated. * @author Nicolas Rinaudo */ public class MutableLineBorder extends LineBorder { /** * Creates a line border with the specified color and a thickness = 1. * @param color the color of the border. */ public MutableLineBorder(Color color) {super(color);} /** * Creates a line border with the specified color and thickness. * @param color the color of the border * @param thickness the thickness of the border */ public MutableLineBorder(Color color, int thickness) {super(color, thickness);} /** * Creates a line border with the specified color, thickness, and corner shape. * @param color the color of the border * @param thickness the thickness of the border * @param roundedCorners whether border corners should be round */ public MutableLineBorder(Color color, int thickness, boolean roundedCorners) {super(color, thickness, roundedCorners);} /** * Sets this border's color. * @param color the color of the border. */ public void setLineColor(Color color) {lineColor = color;} /** * Sets this border's corner shape. * @param roundedCorners whether border corners should be round */ public void setRoundedCorners(boolean roundedCorners) {this.roundedCorners = roundedCorners;} /** * Sets this border's thickness. * @param thickness the thickness of the border. */ public void setThickness(int thickness) {this.thickness = thickness;} } ================================================ FILE: src/main/java/com/mucommander/ui/border/package.html ================================================ Provides various Border implementations. ================================================ FILE: src/main/java/com/mucommander/ui/button/ArrowButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import com.mucommander.ui.icon.IconManager; import javax.swing.*; /** * ArrowButton is a button displaying an arrow icon pointing to a specified direction (up/down/left/right). * The direction of the arrow can be changed at any time using {@link #setArrowDirection(Direction)}. * * @author Maxence Bernard */ public class ArrowButton extends JButton { public enum Direction { UP("arrow_up.png"), DOWN("arrow_down.png"), LEFT("arrow_left.png"), RIGHT("arrow_right.png"); private final String fileName; Direction(String fileName) { this.fileName = fileName; } } /** * Creates a new ArrowButton with no initial arrow icon. */ ArrowButton() { super(); } /** * Creates a new ArrowButton showing an arrow icon pointing to the specified direction. */ public ArrowButton(Direction direction) { setArrowDirection(direction); } /** * Creates a new ArrowButton using the specified Action and showing an arrow icon pointing to the specified direction. */ public ArrowButton(Action action, Direction direction) { super(action); setArrowDirection(direction); } /** * Changes the direction of the arrow icon to the specified one. * * @param direction can have one of the following values: {@link Direction#UP}, {@link Direction#DOWN}, * {@link Direction#LEFT} or {@link Direction#RIGHT} */ void setArrowDirection(Direction direction) { setIcon(IconManager.getIcon(IconManager.IconSet.COMMON, direction.fileName)); } } ================================================ FILE: src/main/java/com/mucommander/ui/button/ButtonChoicePanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import com.mucommander.ui.helper.MnemonicHelper; import javax.swing.*; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * ButtonChoicePanel lays out an array of buttons on a grid and provides an easy way * for the user to navigate and select buttons : *
      *
    • At any given time, the current active button (the first one initially) can be selected by pressing ENTER *
    • LEFT key goes back to the previous button, to the last button if current button is the first one *
    • RIGHT key goes forward one button, to the first button if current button is the last one *
    • UP key goes up one row, to the last row if current button is on the first row *
    • DOWN key goes down one row, to the first row if current button is on the last row *
    • Buttons can directly be selected by pressing the Mnemonic associated with the button (if any) *
    * This does not interfere with regular focus management where TAB (resp. Shift+TAB) goes to the next (resp. previous) * focusable component. * * @author Maxence Bernard */ public class ButtonChoicePanel extends JPanel implements KeyListener, FocusListener { /** Provided JButton instances */ private final JButton[] buttons; /** RootPane associated with this ButtonChoicePanel */ private final JRootPane rootPane; /** Number of columns of the buttons grid */ private final int nbCols; /** Number of row of the buttons grid */ private final int nbRows; /** Current button, i.e. the one that currently has focus */ private int currentButton; /** Total number of buttons */ private final int nbButtons; /** * Creates a new ButtonChoicePanel and lays out the given buttons on a grid * according to the provided number of columns. * *

    Initial focus will be given to the first button. * * @param buttons the JButton instances to layout * @param nbCols number of columns for the buttons grid, if <= 0 all buttons will be put on a single row * @param rootPane associated with this ButtonChoicePanel */ public ButtonChoicePanel(JButton[] buttons, int nbCols, JRootPane rootPane) { this.buttons = buttons; this.nbButtons = buttons.length; this.nbCols = nbCols <= 0 ? nbButtons : nbCols; this.rootPane = rootPane; this.nbRows = nbCols <= 0 ? 1 : nbButtons/nbCols+(nbButtons % nbCols == 0 ? 0 : 1); // If the provided number of columns is <= 0, lay out all buttons on a single row // and use FlowLayout to do that if (nbCols <= 0) { setLayout(new FlowLayout(FlowLayout.RIGHT)); } // Use GridLayout to lay out buttons on 2-dimensional grid else { setLayout(new GridLayout(0, nbCols)); } for(int i = 0; i < nbButtons; i++) { JButton button = buttons[i]; // Listener to key events to transfer focus button.addKeyListener(this); // Allow buttons to be made 'default buttons' when enter is pressed button.setDefaultCapable(true); // Listen to focus events to make the focused button the default button button.addFocusListener(this); add(button); } // Set mnemonics to buttons updateMnemonics(); // First button is made 'default button' rootPane.setDefaultButton(buttons[0]); this.currentButton = 0; } /** * Updates the buttons mnemonics. This method should be called when at least one of the button's label has changed. */ public void updateMnemonics() { MnemonicHelper mnemonicHelper = new MnemonicHelper(); char mnemonic; JButton button; for(int i=0; inbButtons-1) this.currentButton -= nbCols; } else this.currentButton -= nbCols; } // DOWN key goes down one row, to the first row if current button is on the last row else if (keyCode==KeyEvent.VK_DOWN) { if(nbButtons-currentButton>0 && nbButtons-currentButton<=nbCols) // If current button is on the last row this.currentButton = currentButton%nbCols; else this.currentButton += nbCols; } // Click button when a key that corresponds to one of the buttons' mnemonic has been pressed else if(!e.isAltDown()) { for(int i=0; i. */ package com.mucommander.ui.button; import com.mucommander.ui.dialog.FocusDialog; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * CollapseExpandButton provides an expand/collapse functionality to a component: clicking the button expands/collapses * the associated component making it visible/unvisible, and resizes the window that contains it so that it properly * fits. * *

    This button shows a down/right arrow icon to reflect the current expanded/collapsed state. * * @author Maxence Bernard */ public class CollapseExpandButton extends ArrowButton implements ActionListener, KeyListener { /** The component to collapse/expand */ private final Component comp; /** True if this button is in the 'expanded' state and the associated component is being displayed */ private boolean expandedState; /** * Creates a new CollapseExpandButton that renders the specified component visible/invisible. * * @param label the label to use for this button * @param component the component that is to be expanded/collapsed * @param expanded initial expanded/collapsed state */ public CollapseExpandButton(String label, Component component, boolean expanded) { this.comp = component; setText(label); setExpandedState(expanded, false); addActionListener(this); addKeyListener(this); } /** * Expands/collapses the associated component, resizes the window that contains it, and updates the button's icon * to reflect the new state. * * @param expanded if true, the component will be expanded, collapsed if false. */ public void setExpandedState(boolean expanded) { setExpandedState(expanded, true); } /** * Returns the current expanded state of the component: true for expanded, false for * collapsed. * * @return the current expanded state of the component: true for expanded, false for collapsed. */ public boolean getExpandedState() { return expandedState; } /** * Sets the new expanded state for the component: true for expanded, false for collapsed. * If specified, the window which contains the expanded/collapsed component will be repacked so that the component * fits properly. * * @param expanded the new expanded state: true for expanded, false for collapsed. * @param packWindow If true, the window which contains the expanded/collapsed component will be repacked so that * the component fits properly. */ private void setExpandedState(boolean expanded, boolean packWindow) { comp.setVisible(expanded); setArrowDirection(expanded ? Direction.DOWN : Direction.RIGHT); this.expandedState = expanded; final Container tla = getTopLevelAncestor(); if (packWindow) { if (tla instanceof Window) { ((Window) tla).pack(); } } if (tla instanceof FocusDialog) { ((FocusDialog) tla).setStorageSuffix(expanded ? "expanded" : null); } } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent actionEvent) { setExpandedState(!expandedState, true); } //////////////////////////////// // KeyListener implementation // //////////////////////////////// public void keyPressed(KeyEvent keyEvent) { int keyCode = keyEvent.getKeyCode(); if (keyCode == KeyEvent.VK_RIGHT && !expandedState) { setExpandedState(true, true); } else if (keyCode == KeyEvent.VK_LEFT && expandedState) { setExpandedState(false, true); } } public void keyReleased(KeyEvent keyEvent) { } public void keyTyped(KeyEvent keyEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/button/HelpButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import java.util.HashMap; import java.util.Map; import javax.swing.JButton; import javax.swing.border.EmptyBorder; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.ui.action.impl.GoToDocumentationAction; import com.mucommander.ui.main.MainFrame; /** * This is a contextual 'Help' button to be used wherever help is available or needed. When clicked, it opens * muCommander's online documentation in the system's default browser. If a specified help topic is passed to the * constructor, the browser will open the said topic. If no topic is specified, the base documentation URL will be opened. * *

    Unless explicitly set, this button has a standard help icon but no text label. A tooltip is displayed when * hovering over the button. * * @see com.mucommander.ui.action.impl.GoToDocumentationAction * @author Maxence Bernard */ public class HelpButton extends JButton { /** * Creates a new HelpButton with no initial topic. * * @param mainFrame the MainFrame this button is associated with */ public HelpButton(MainFrame mainFrame) { this(mainFrame, null); } /** * Creates a new HelpButton with the specified topic. * * @param mainFrame the MainFrame this button is associated with * @param helpTopic the help topic this button will open when clicked, null to open the base documentation URL */ public HelpButton(MainFrame mainFrame, String helpTopic) { Map properties = new HashMap<>(); GoToDocumentationAction action = new GoToDocumentationAction(mainFrame, properties); setAction(action); if (helpTopic != null) { setHelpTopic(helpTopic); } // Note: the button's text and icon must be set after the action otherwise they'll be replaced by the action's // Remove the action's label from this button's text setText(null); // Use the action's label as a tooltip setToolTipText(action.getLabel()); if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { // If running Mac OS X 10.5 (and up), use the special client property to have a standard help button. putClientProperty("JButton.buttonType", "help"); // Remove the action's icon setIcon(null); } else { setContentAreaFilled(false); setBorderPainted(false); setBorder(new EmptyBorder(0, 0, 0, 0)); } setFocusable(false); } /** * Returns the help topic this button will open when clicked, null if there is none. * * @return the help topic this button will open when clicked, null if there is none */ public String getHelpTopic() { return (String)getAction().getValue(GoToDocumentationAction.TOPIC_PROPERTY_KEY); } /** * Sets the help topic this button will open when clicked, null to open the base documentation URL. * * @param helpTopic the help topic this button will open when clicked, null to open the base documentation URL */ public void setHelpTopic(String helpTopic) { getAction().putValue(GoToDocumentationAction.TOPIC_PROPERTY_KEY, helpTopic); } } ================================================ FILE: src/main/java/com/mucommander/ui/button/HelpButtonPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import javax.swing.*; import java.awt.*; /** * A simple panel that lays out a {@link HelpButton}, aligning to the right. * * @author Maxence Bernard */ public class HelpButtonPanel extends JPanel { public HelpButtonPanel(HelpButton helpButton) { super(new BorderLayout()); add(helpButton, BorderLayout.EAST); } } ================================================ FILE: src/main/java/com/mucommander/ui/button/NonFocusableButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.UIManager; import com.mucommander.commons.runtime.OsFamily; /** * NonFocusableButton is a JButton which is non focusable, i.e. that cannot hold keyboard focus. * * @author Maxence Bernard */ public class NonFocusableButton extends JButton { public NonFocusableButton() { setLookAndFeelProperties(); } public NonFocusableButton(Action a) { super(a); setLookAndFeelProperties(); } public NonFocusableButton(Icon icon) { super(icon); setLookAndFeelProperties(); } public NonFocusableButton(String text) { super(text); setLookAndFeelProperties(); } public NonFocusableButton(String text, Icon icon) { super(text, icon); setLookAndFeelProperties(); } private void setLookAndFeelProperties() { // Fill the content area under the Windows L&F only, required for the borders to be painted. // Note: filing the content area under Metal L&F looks like absolute crap. setContentAreaFilled(OsFamily.WINDOWS.isCurrent() && "Windows".equals(UIManager.getLookAndFeel().getName())); } @Override public boolean isFocusable() { return false; } @Override public void updateUI() { super.updateUI(); // Update L&F properties setLookAndFeelProperties(); } } ================================================ FILE: src/main/java/com/mucommander/ui/button/PopupButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.impl.MuteProxyAction; import lombok.Getter; import lombok.Setter; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * PopupButton is a compound component that combines a JButton with a JPopupMenu. * *

    When the mouse is held down on the button, a popup menu is displayed below the button. When the button is clicked, * if a specific action was specified at creation time or using {@link #setAction(Action)}, this action is performed. * If not, a popup menu is displayed below the button. * *

    This class is abstract. Derived classes must implement {@link #getPopupMenu()} to return a JPopupMenu instance * to be displayed. * * @author Maxence Bernard */ public abstract class PopupButton extends NonFocusableButton { /** Custom action performed when the button is clicked. If null, popup menu will be displayed when mouse is clicked */ private Action buttonClickedAction; /** Timestamp when popup menu was closed */ private long popupMenuClosedTime; /** Non-null while popup menu is visible */ private JPopupMenu popupMenu; /** Location of the popup menu, relative to the button */ @Setter @Getter private int popupMenuLocation = BOTTOM; /** Controls the number of milliseconds to hold down the mouse button on the button to display the popup menu */ private final static int POPUP_DELAY = 300; /** * Box-orientation constant used to specify the buttom-left oriented side of a box. */ private static final int BOTTOM_LEFT_ORIENTED = 5; /** * Creates a new PopupButton with no custom action for when this button is clicked. * When this button is clicked, the popup menu as returned by {@link #getPopupMenu()} will be displayed. */ protected PopupButton() { this(null); } /** * Creates a new PopupButton with a custom action to performed when this button is clicked. * * @param buttonClickedAction custom action to performed when this button is clicked */ public PopupButton(Action buttonClickedAction) { setAction(buttonClickedAction); // Listen to mouse events on this button addMouseListener(new PopupMenuHandler()); } /** * Sets the action to be performed when this button is clicked. If null is passed, a popup menu will * be displayed when this button is clicked. */ @Override public void setAction(Action buttonClickedAction) { if (buttonClickedAction == null) { super.setAction(null); } else { // Pass a MuteProxyAction to JButton that does nothing when the action is performed. // We need this to keep the use the Action's properties but handle action events ourself. super.setAction(new MuteProxyAction(buttonClickedAction)); } this.buttonClickedAction = buttonClickedAction; } /** * Returns true if a popup menu is currently being displayed. */ private boolean isPopupMenuVisible() { return popupMenu!=null; } /** * Displays the popup menu as returned by {@link #getPopupMenu()}. */ public synchronized void popupMenu() { this.popupMenu = getPopupMenu(); popupMenu.addPopupMenuListener(new PopupMenuListener() { public void popupMenuWillBecomeVisible(PopupMenuEvent popupMenuEvent) { } public void popupMenuWillBecomeInvisible(PopupMenuEvent popupMenuEvent) { popupMenuClosed(); } public void popupMenuCanceled(PopupMenuEvent popupMenuEvent) { popupMenuClosed(); } private void popupMenuClosed() { // Set popup menu reference to null once it has been made invisible so that it can be garbage collected, // and remember the time at which the popup was closed, so that a mouse press on the button closes // the popup menu and does not bring it back up. popupMenuClosedTime = System.currentTimeMillis(); popupMenu = null; setSelected(false); } }); // Popup up the menu underneath under this button. This has to be executed by the event thread, otherwise some // weird repaint issue will arise under Windows at least (Note: this method can be executed by a thread other // than the event thread). SwingUtilities.invokeLater(() -> { Dimension popupMenuSize = popupMenu.getPreferredSize(); popupMenu.show(PopupButton.this, popupMenuLocation==RIGHT?getWidth():popupMenuLocation==LEFT?-(int)popupMenuSize.getWidth():popupMenuLocation== BOTTOM_LEFT_ORIENTED ?getWidth()-(int)popupMenuSize.getWidth():0, popupMenuLocation==BOTTOM?getHeight():popupMenuLocation==TOP?-(int)popupMenuSize.getHeight():popupMenuLocation== BOTTOM_LEFT_ORIENTED ?getHeight():0 ); }); // Leave the button selected (shows that button has focus) while the popup menu is visible setSelected(true); // Note: focus MUST NOT be requested on the popup menu because: // a/ it's not necessary, focus is automatically transfered to the popup menu // b/ it creates a weird bug under Windows which prevents enter key from selecting any menu item } /** * This method is invoked when the popup menu is about to be displayed. This method must return the JPopupMenu * instance to display. */ public abstract JPopupMenu getPopupMenu(); /////////////////// // Inner classes // /////////////////// /** * This inner class controls this button's behavior and actions when the mouse button is clicked or held down. */ private class PopupMenuHandler implements MouseListener, Runnable { /** Contains the time at which a mouse button was pressed, 0 when a mouse button is not currently being pressed */ private long pressedTime; /** Returns true to indicate that a mouse event should currently be ignored because the popup menu * is visible, or was closed recently (less than POPUP_DELAY ms ago) */ private boolean shouldIgnoreMouseEvent() { return isPopupMenuVisible() || System.currentTimeMillis() - popupMenuClosedTime= POPUP_DELAY) { popupMenu(); } } } } } ================================================ FILE: src/main/java/com/mucommander/ui/button/RolloverButtonAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import javax.swing.*; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * This class allows to add a rollover effect to a JButton. Rollover-enabled buttons have no borders * by default. It is only when the mouse cursor is over the button that the button's borders get painted. * This rollover effect gives the user a visual indication that the button can be pressed (in other words, that the * button is indeed a button), while not cluttering the interface with button borders. * Such buttons are particularly effective for toolbars, where a large number of buttons are usually present. * *

    * To 'rollover-enable' a button, the {@link #decorateButton(javax.swing.JButton)} method must first be called to * set decoration properties. Then, the button must register an instance of RolloverButtonAdapter as a * mouse listener. Note that a single RolloverButtonAdapter instance can be registered with several buttons. * * @author Maxence Bernard */ public class RolloverButtonAdapter implements MouseListener { private static final RolloverButtonAdapter INSTANCE = new RolloverButtonAdapter(); /** * Creates a new RolloverButtonAdapter. */ public RolloverButtonAdapter() { } /** * Sets the decoration properties required to give the specified button a 'rollover' look and feel. * * @param button the button to 'rollover-enable' */ public static void decorateButton(JButton button) { // Set button decorations and rollover behavior button.setRolloverEnabled(true); button.setFocusPainted(false); button.setBorderPainted(false); button.setContentAreaFilled(false); // if (OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { // button.putClientProperty("JButton.buttonType", "textured"); // } button.setRolloverEnabled(true); button.addMouseListener(INSTANCE); } @Override public void mouseEntered(MouseEvent e) { ((JButton)e.getSource()).setBorderPainted(true); } @Override public void mouseExited(MouseEvent e) { ((JButton)e.getSource()).setBorderPainted(false); } @Override public void mouseClicked(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/ui/button/ToolbarMoreButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.button; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.AbstractButton; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.ui.icon.IconManager; /* * MySwing: Advanced Swing Utilites * Copyright (C) 2005 Santhosh Kumar T *

    * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. *

    * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ /** * Provides a way for toolbar buttons to be displayed when the toolbar itself doesn't have enough space for all buttons. * The buttons that are cropped are displayed in a secondary toolbar, using a vertical layout. *

    * To use this Feature replace: * * frame.getContentPane().add(toolbar, BorderLayout.NORTH); * * with * * frame.getContentPane().add(MoreButton.wrapToolBar(toolBar), BorderLayout.NORTH); * *

    *

    * This class is based on the code of Santhosh Kumar T, see * this link for more information. * * @author Santhosh Kumar T, Leo Welsch */ public class ToolbarMoreButton extends JToggleButton implements ActionListener { private static JToolBar moreToolbar; JToolBar toolbar; private ToolbarMoreButton(final JToolBar toolbar) { super(IconManager.getIcon(IconManager.IconSet.COMMON, "more.png")); this.toolbar = toolbar; addActionListener(this); setFocusPainted(false); setMargin(new Insets(0, 0, 0, 0)); setContentAreaFilled(false); setBorderPainted(false); // Use new JButton decorations introduced in Mac OS X 10.5 (Leopard) if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { putClientProperty("JComponent.sizeVariant", "small"); putClientProperty("JButton.buttonType", "textured"); } // paint border only when necessary addMouseListener(new MouseAdapter() { @Override public void mouseExited(MouseEvent e) { setBorderPainted(false); } @Override public void mouseEntered(MouseEvent e) { setBorderPainted(true); } }); // hide & seek toolbar.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { int nbToolbarComponents = toolbar.getComponentCount(); final boolean aFlag = nbToolbarComponents > 0 && !isVisible(toolbar.getComponent(nbToolbarComponents - 1), null); setVisible(aFlag); moreToolbar.setVisible(aFlag); } }); } // check visibility // partially visible is treated as not visible private boolean isVisible(Component comp, Rectangle rect) { if (rect == null) { rect = toolbar.getVisibleRect(); } return comp.getLocation().x + comp.getWidth() <= rect.getWidth(); } public void actionPerformed(ActionEvent e) { Component[] comp = toolbar.getComponents(); Rectangle visibleRect = toolbar.getVisibleRect(); for (int i = 0; i < comp.length; i++) { if (!isVisible(comp[i], visibleRect)) { JPopupMenu popup = new JPopupMenu(); for (; i < comp.length; i++) { if (comp[i] instanceof AbstractButton button) { if (button.getAction() != null) { popup.add(button.getAction()); } } else if (comp[i] instanceof JSeparator) { popup.addSeparator(); } } // on popup close make more-button unselected popup.addPopupMenuListener(new PopupMenuListener() { public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { setSelected(false); } public void popupMenuCanceled(PopupMenuEvent e) { } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } }); popup.show(this, 0, getHeight()); } } } public static JPanel wrapToolBar(JToolBar toolbar) { moreToolbar = new JToolBar(); moreToolbar.setRollover(true); moreToolbar.setFloatable(false); moreToolbar.add(new ToolbarMoreButton(toolbar)); moreToolbar.setBorderPainted(false); JPanel panel = new JPanel(new BorderLayout()); panel.add(toolbar, BorderLayout.CENTER); panel.add(moreToolbar, BorderLayout.EAST); return panel; } } ================================================ FILE: src/main/java/com/mucommander/ui/button/package.html ================================================ Provides various JButton implementations. ================================================ FILE: src/main/java/com/mucommander/ui/chooser/ColorChangeEvent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import javax.swing.*; import java.awt.*; /** * @author Maxence Bernard */ public class ColorChangeEvent { private final JComponent source; private final Color color; public ColorChangeEvent(JComponent source, Color color) { this.source = source; this.color = color; } public JComponent getSource() { return source; } public Color getColor() { return color; } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/ColorChangeListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; /** * @author Maxence Bernard */ public interface ColorChangeListener { void colorChanged(ColorChangeEvent event); } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/ColorChooser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * Component used to let users pick a color. *

    * The main reason for this component's existence is Swing's JColorChooser does not offer * alpha transparency edition, and that its localisation is incomplete. This is a wrapper that fixes both * of these shortcomings. *

    * This component can be used either as a regular widget and be added to a container or as * a dialog box through the {@link #createDialog(Frame,ColorChooser) createDialog} method. * * @author Nicolas Rinaudo, Maxence Bernard */ public class ColorChooser extends YBoxPanel implements ChangeListener { // - UI components ---------------------------------------------------------- // -------------------------------------------------------------------------- /** Component that displays a preview of the current color */ private JComponent previewComponent; /** Color chooser. */ private JColorChooser chooser; /** Alpha transparency chooser. */ private IntegerChooser alpha; // - Color editing ---------------------------------------------------------- // -------------------------------------------------------------------------- /** Currently selected color. */ private Color currentColor; /** Color on which the dialog was initialized. */ private final Color initialColor; /** Property to change in the preview component when the current color changes */ private String previewColorPropertyName; // - Localisation ----------------------------------------------------------- // -------------------------------------------------------------------------- static { String buffer; // Tab labels. UIManager.put("ColorChooser.rgbNameText", Translator.get("color_chooser.rgb")); UIManager.put("ColorChooser.hsbNameText", Translator.get("color_chooser.hsb")); UIManager.put("ColorChooser.swatchesNameText", Translator.get("color_chooser.swatches")); // Red color name. UIManager.put("ColorChooser.rgbRedText", buffer = Translator.get("color_chooser.red")); UIManager.put("ColorChooser.hsbRedText", buffer); // Green color name. UIManager.put("ColorChooser.rgbGreenText", buffer = Translator.get("color_chooser.green")); UIManager.put("ColorChooser.hsbGreenText", buffer); // Blue color name. UIManager.put("ColorChooser.rgbBlueText", buffer = Translator.get("color_chooser.blue")); UIManager.put("ColorChooser.hsbBlueText", buffer); // HSB tab specific strings. UIManager.put("ColorChooser.hsbHueText", Translator.get("color_chooser.hue")); UIManager.put("ColorChooser.hsbSaturationText", Translator.get("color_chooser.saturation")); UIManager.put("ColorChooser.hsbBrightnessText", Translator.get("color_chooser.brightness")); // Swatches tab specific strings. UIManager.put("ColorChooser.swatchesRecentText", Translator.get("color_chooser.recent")); } public ColorChooser() { this(Color.WHITE, null, null); } public ColorChooser(Color initialColor) { this(initialColor, null, null); } public ColorChooser(Color initialColor, JComponent previewComponent, String previewColorPropertyName) { this.currentColor = initialColor; this.initialColor = initialColor; // Initializes the UI. add(createChooserPanel()); add(createTransparencyPanel()); alpha.setValue(initialColor.getAlpha()); chooser.setColor(initialColor); if(previewComponent!=null && previewColorPropertyName!=null) { this.previewComponent = previewComponent; this.previewColorPropertyName = previewColorPropertyName; add(createPreviewPanel(previewComponent)); updatePreview(); } } /** * Creates a dialog containing the specified color chooser. * @param parent component on which to center the dialog. * @param chooser chooser to use within the dialog. * @return a dialog containing the specified chooser. */ public static FocusDialog createDialog(Dialog parent, ColorChooser chooser) { return new ChooserDialog(parent, chooser); } /** * Creates a dialog containing the specified color chooser. * @param parent component on which to center the dialog. * @param chooser chooser to use within the dialog. * @return a dialog containing the specified chooser. */ public static FocusDialog createDialog(Frame parent, ColorChooser chooser) { return new ChooserDialog(parent, chooser); } /** * Creates the preview panel. */ private JPanel createPreviewPanel(JComponent previewComponent) { // Sets the label's preferred size (same width as the chooser, twice the normal label height). Dimension size = previewComponent.getPreferredSize(); size.width = chooser.getPreferredSize().width; size.height *= 2; previewComponent.setPreferredSize(size); // Sets the preview label appearance. previewComponent.setOpaque(true); // Creates the preview panel. JPanel panel = new JPanel(); panel.add(previewComponent); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); return panel; } /** * Creates the color chooser panel. */ private JColorChooser createChooserPanel() { // Creates the color chooser. chooser = new JColorChooser(); chooser.setPreviewPanel(new JPanel()); chooser.getSelectionModel().addChangeListener(this); return chooser; } /** * Creates the transparency selection panel. */ private JPanel createTransparencyPanel() { // Creates and initializes the transparency selector. alpha = new IntegerChooser(0, 255, 255); alpha.setMajorTickSpacing(85); alpha.setMinorTickSpacing(17); alpha.setPaintTicks(true); alpha.setPaintLabels(true); alpha.setBorder(BorderFactory.createTitledBorder(Translator.get("color_chooser.alpha"))); alpha.addChangeListener(this); return alpha; } /** * Returns the color selected by the user. * @return the color selected by the user. */ public Color getColor() { return currentColor; } /** * Resets the dialog to the initial color. */ public void reset() { reset(true); } /** * Resets the dialog to the initial color. * @param updateUI if set to false, the component's UI won't be updated. */ private void reset(boolean updateUI) { currentColor = initialColor; /// Propagates the color to the choosers. if (updateUI) { alpha.setValue(currentColor.getAlpha()); chooser.setColor(currentColor); currentColor = initialColor; // Need to set it again as the value is changed by stateChanged() updatePreview(); } } /** * Update the preview panel to the current color. */ private void updatePreview() { if (previewComponent != null) { previewComponent.putClientProperty(previewColorPropertyName, currentColor); } } /** * This method is public as an implementation side effect and should not be called directly. */ public void stateChanged(ChangeEvent e) { // Creates the new current color. Color buffer = chooser.getColor(); currentColor = new Color(buffer.getRed(), buffer.getGreen(), buffer.getBlue(), alpha.getValue()); updatePreview(); } /** * Component used to present a ColorChooser from within a modal dialog. * @author Nicolas Rinaudo */ private static class ChooserDialog extends FocusDialog implements ActionListener { /** Resets the color to the original one. */ private JButton resetButton; /** Closes the dialog without applying the color selection. */ private JButton cancelButton; /** Color chooser contained by this dialog. */ private ColorChooser chooser; /** * Creates a new dialog containing the specified color chooser. */ ChooserDialog(Frame parent, ColorChooser chooser) { super(parent, i18n("color_chooser.title"), parent); initUI(chooser); } /** * Creates a new dialog containing the specified color chooser. */ ChooserDialog(Dialog parent, ColorChooser chooser) { super(parent, i18n("color_chooser.title"), parent); initUI(chooser); } /** * Initializes the dialog's UI. */ private void initUI(ColorChooser chooser) { this.chooser = chooser; // Initializes the dialog and its content pane. Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); // Creates the content of the dialog. contentPane.add(chooser, BorderLayout.CENTER); contentPane.add(createButtonsPanel(), BorderLayout.SOUTH); } /** * Creates the panel that contains the dialog's buttons. */ private JPanel createButtonsPanel() { // Creates the panel and buttons. XBoxPanel buttonsPanel = new XBoxPanel(); buttonsPanel.add(resetButton = new JButton(i18n("reset"))); buttonsPanel.addSpace(20); JButton okButton = new JButton(i18n("ok")); buttonsPanel.add(okButton); buttonsPanel.add(cancelButton = new JButton(i18n("cancel"))); // Tracks events. resetButton.addActionListener(this); okButton.addActionListener(this); cancelButton.addActionListener(this); // OK will be selected when the user presses enter. getRootPane().setDefaultButton(okButton); // Aligns the buttons to the right of the panel. JPanel panel = new JPanel(); panel.setLayout(new FlowLayout(FlowLayout.RIGHT)); panel.add(buttonsPanel); return panel; } /** * In case the dialog was cancelled, resets the color before closing it. */ @Override public void cancel() { chooser.reset(false); super.cancel(); } /** * This method is public as an implementation side effect and should not be called directly. */ public void actionPerformed(ActionEvent e) { // Resets the current color. if (e.getSource() == resetButton) { chooser.reset(true); }// Closes the dialog, applying modifications if necessary. else { if (e.getSource() == cancelButton) { chooser.reset(false); } dispose(); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/ColorPicker.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import com.mucommander.ui.icon.IconManager; import lombok.Getter; import javax.swing.*; import java.awt.*; import java.awt.event.AWTEventListener; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.WeakHashMap; /** * @author Maxence Bernard */ public class ColorPicker extends JButton implements ActionListener, AWTEventListener { private Robot robot; @Getter private boolean isActive; private final WeakHashMap listeners = new WeakHashMap<>(); /** * True if this component is supported (java.awt.Robot can be used) */ private static boolean isSupported; static { try { new Robot(); isSupported = true; } catch (Exception e) { // java.awt.Robot constructor throws an AWTException "if the platform configuration does not allow low-level input control." // In this case, isSupported will be false } } public ColorPicker() { super(IconManager.getIcon(IconManager.IconSet.COMMON, "picker.png")); addActionListener(this); } public static boolean isSupported() { return isSupported; } public void setActive(boolean active) { if (active == isActive) { return; } final Toolkit toolkit = Toolkit.getDefaultToolkit(); if (active) { if (!isVisible()) return; try { // create a java.awt.Robot operating on the screen device that contains the window this component is in. // Not sure what happens if the window spawns across 2 screens... robot = new Robot(getTopLevelAncestor().getGraphicsConfiguration().getDevice()); } catch (Exception e) { // If Robot is not available, ColorPicker will simply be ineffective, clicking it won't do anything return; } // Change the cursor to the 'eye dropper' icon filled with white setPickerCursor(Color.WHITE); // These are invoked after all pending events are processed SwingUtilities.invokeLater(() -> { // Listen to all mouse events on the window that contains this button toolkit.addAWTEventListener(ColorPicker.this, AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK); // Leave this button selected until a color is picked or this button is pressed again (to cancel) setSelected(true); }); } else { // Stop listening to mouse events toolkit.removeAWTEventListener(this); // Restore default cursor setCustomCursor(Cursor.getDefaultCursor()); // Make the button unselected setSelected(false); } this.isActive = active; } public void addColorChangeListener(ColorChangeListener listener) { listeners.put(listener, null); } public void removeColorChangeListener(ColorChangeListener listener) { listeners.remove(listener); } private void setPickerCursor(Color fillColor) { ImageIcon cursorIcon = (ImageIcon) getIcon(); int iconWidth = cursorIcon.getIconWidth(); int iconHeight = cursorIcon.getIconHeight(); int colorRGB = fillColor.getRGB(); // Retrieve the cursor icon fill mask as an alpha-enabled BufferedImage BufferedImage iconMaskBi = new BufferedImage(iconWidth, iconHeight, BufferedImage.TYPE_INT_ARGB); Graphics g = iconMaskBi.getGraphics(); g.drawImage(IconManager.getIcon(IconManager.IconSet.COMMON, "picker_mask.png").getImage(), 0, 0, null); // Replace solid (non-transparent) pixels with specified fill color for (int y = 0; y < iconHeight; y++) { for (int x = 0; x < iconWidth; x++) { int rgba = iconMaskBi.getRGB(x, y); if ((rgba >> 24) != 0) iconMaskBi.setRGB(x, y, colorRGB); } } // Retrieve the cursor icon as an alpha-enabled BufferedImage and paint the fill mask on it BufferedImage iconBi = new BufferedImage(cursorIcon.getIconWidth(), cursorIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); g = iconBi.getGraphics(); g.drawImage(cursorIcon.getImage(), 0, 0, null); g.drawImage(iconMaskBi, 0, 0, null); setCustomCursor(Toolkit.getDefaultToolkit().createCustomCursor(iconBi, new Point(0, 15), getClass().getName())); } private void setCustomCursor(Cursor cursor) { getTopLevelAncestor().setCursor(cursor); } private void fireColorPicked(Color color) { // Iterate on all listeners for (ColorChangeListener listener : listeners.keySet()) listener.colorChanged(new ColorChangeEvent(this, color)); } @Override public void actionPerformed(ActionEvent actionEvent) { setActive(!isActive); } @Override public void eventDispatched(AWTEvent awtEvent) { if (awtEvent instanceof MouseEvent mouseEvent) { Point mousePoint = mouseEvent.getPoint(); Component source = (Component) mouseEvent.getSource(); // Convert the mouse X/Y into screen coordinates SwingUtilities.convertPointToScreen(mousePoint, source); int x = (int) mousePoint.getX(); int y = (int) mousePoint.getY(); // Retrieve the color of the pixel the mouse is currently over Color color = robot.getPixelColor(x, y); int button = mouseEvent.getButton(); if (button != MouseEvent.NOBUTTON) { // If left button was clicked (not released) if (button == MouseEvent.BUTTON1 && (mouseEvent.getModifiersEx() & MouseEvent.MOUSE_CLICKED) != 0) { // If this color picker was clicked, cancel the color picking without firing an event if (source != this) fireColorPicked(color); // Consume the event so that it doesn't get caught by a clicked component mouseEvent.consume(); // We're done setActive(false); } // If any other button was clicked or if this is not a MOUSE_CLICKED event, do nothing return; } setPickerCursor(color); } } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/FontChooser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import com.mucommander.utils.text.Translator; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.ui.layout.YBoxPanel; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.WeakHashMap; /** * Component used to let users choose a font. * @author Nicolas Rinaudo */ public class FontChooser extends YBoxPanel implements ActionListener { /** Legal font sizes. */ private final static int[] FONT_SIZES = {8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 28, 32, 36, 40}; /** Lists all the available font families. */ private JComboBox families; /** Lists all the legal font sizes. */ private JComboBox sizes; /** Whether, or not the font should be italic. */ private JCheckBox italic; /** Whether, or not the font should be bold. */ private JCheckBox bold; /** Used to display a preview of the current font. */ private JLabel preview; /** Currently selected font. */ private Font font; /** List of all registered state change listeners. */ private final WeakHashMap listeners = new WeakHashMap<>(); /** * Creates a new FontChooser with the specified selection. * @param selection font that should be pre-selected. */ public FontChooser(Font selection) { super(); initUI(selection); } /** * Initializes the font chooser's UI. * @param selection default font selection (ignored if null). */ private void initUI(Font selection) { setAlignmentX(LEFT_ALIGNMENT); // Font families. families = new TcComboBox<>(); String[] familyNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); int selectedIndex = 0; for (int i = 0; i < familyNames.length; i++) { families.addItem(familyNames[i]); if (selection.getFamily().equalsIgnoreCase(familyNames[i])) { selectedIndex = i; } } families.setSelectedIndex(selectedIndex); families.addActionListener(this); // Adds the font families to the component. JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); panel.add(families); add(panel); // Font sizes. sizes = new TcComboBox<>(); for (int fontSize : FONT_SIZES) { sizes.addItem(Integer.toString(fontSize)); } sizes.setSelectedItem(Integer.toString(selection.getSize())); sizes.addActionListener(this); // Font styles. bold = new JCheckBox(Translator.get("font_chooser.font_bold")); italic = new JCheckBox(Translator.get("font_chooser.font_italic")); bold.setSelected(selection.isBold()); bold.addActionListener(this); italic.setSelected(selection.isItalic()); italic.addActionListener(this); // Adds the font size and styles to the component. panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); panel.add(new JLabel(Translator.get("font_chooser.font_size")+": ")); panel.add(sizes); panel.add(Box.createRigidArea(new Dimension(10, 0))); panel.add(bold); panel.add(italic); add(panel); // Initializes the current font. font = selection; // Creates the preview panel. panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); panel.add(new JLabel(Translator.get("preview")+": ")); preview = new JLabel("aBcDeFgHiJkLmNoPqRsTuVwXyZ"); updatePreview(); panel.add(preview); add(panel); } // - Content access --------------------------------------------------------- // -------------------------------------------------------------------------- /** * Creates a font from the current selection. * @return the font described by the current selection. */ private Font createFont() { int style = (bold.isSelected() ? Font.BOLD : 0) | (italic.isSelected() ? Font.ITALIC : 0); int size = Integer.parseInt((String)sizes.getSelectedItem()); return new Font((String)families.getSelectedItem(), style, size); } /** * Returns the font currently selected in the chooser. * @return the font currently selected in the chooser. */ public Font getCurrentFont() { return font; } // - Listener code ---------------------------------------------------------- // -------------------------------------------------------------------------- /** * Updates the preview panel. */ private void updatePreview() { preview.setFont(font); preview.repaint(); } /** * Called when the font description has been changed. */ public void actionPerformed(ActionEvent e) { font = createFont(); updatePreview(); // Notifies listeners. ChangeEvent event = new ChangeEvent(this); for (ChangeListener listener : listeners.keySet()) { listener.stateChanged(event); } } // - State changing code ---------------------------------------------------- // -------------------------------------------------------------------------- /** * Registers the specified object as a change listener. */ public void addChangeListener(ChangeListener listener) { listeners.put(listener, null); } /** * Un-registers the specified object as a change listener. */ public void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/IntegerChooser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.util.WeakHashMap; /** * Component meant to let users choose an integer value from a specific range. *

    * An IntegerChooser is an compound-component created by associating a JSpinner and a JSlider. *

    * In order to track the chooser's state, listeners can be registered through {@link #addChangeListener(ChangeListener)}. * * @author Nicolas Rinaudo */ public class IntegerChooser extends JPanel implements ChangeListener { /** List of all registered state change listeners. */ private final WeakHashMap listeners; /** Integer slider. */ private final JSlider slider; /** Integer spinner. */ private final JSpinner spinner; /** * Creates a new integer chooser. * @param min chooser's minimum value. * @param max chooser's maximum value. * @param initialValue chooser's initial value. */ public IntegerChooser(int min, int max, int initialValue) { super(); // Initializes the listeners. listeners = new WeakHashMap<>(); // Creates the components. slider = new JSlider(JSlider.HORIZONTAL, min, max, initialValue); spinner = new JSpinner(new SpinnerNumberModel(initialValue, min, max, 1)); // Registers listeners. slider.addChangeListener(this); spinner.addChangeListener(this); // Creates the panel. this.add(slider); this.add(spinner, BorderLayout.EAST); } // - Slider modifications --------------------------------------------------- // -------------------------------------------------------------------------- /** * This method sets the major tick spacing. * The number that is passed-in represents the distance, measured in values, between each major tick mark. If you have a * slider with a range from 0 to 50 and the major tick spacing is set to 10, you will get major ticks next to the following * values: 0, 10, 20, 30, 40, 50. * @param spacing major tick spacing */ public void setMajorTickSpacing(int spacing) { slider.setMajorTickSpacing(spacing); } /** * This method sets the minor tick spacing. * The number that is passed-in represents the distance, measured in values, between each minor tick mark. If you have a * slider with a range from 0 to 50 and the minor tick spacing is set to 10, you will get minor ticks next to the following * values: 0, 10, 20, 30, 40, 50. * @param spacing minor ticks spacing */ public void setMinorTickSpacing(int spacing) { slider.setMinorTickSpacing(spacing); } /** * Determines whether tick marks are painted on the slider * @param b true if tick marks are painted on the slider */ public void setPaintTicks(boolean b) { slider.setPaintTicks(b); } /** * Determines whether labels are painted on the slider * @param b true if labels marks are painted on the slider */ public void setPaintLabels(boolean b) { slider.setPaintLabels(b); } /** * Determines whether labels are painted on the slider. */ public void setValue(int value) { slider.setValue(value); spinner.setValue(value); } // - Value retrieval -------------------------------------------------------- // -------------------------------------------------------------------------- /** * Returns the chooser's current value. * @return the chooser's current value. */ public int getValue() {return slider.getValue();} /** * Registers the specified object as a change listener. */ public void addChangeListener(ChangeListener listener) { listeners.put(listener, null); } /** * Un-registers the specified object as a change listener. */ public void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } /** * This method is public as an implementation side effect and should never be called directly. */ public void stateChanged(ChangeEvent e) { // Updates the chooser's value. if (e.getSource() == spinner) { slider.setValue((Integer) spinner.getValue()); } else if(e.getSource() == slider) { spinner.setValue(slider.getValue()); } // Notifies listeners. ChangeEvent event = new ChangeEvent(this); for (ChangeListener listener : listeners.keySet()) { listener.stateChanged(event); } } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/KeyboardShortcutChooser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import com.mucommander.ui.combobox.ComboBoxListener; import com.mucommander.ui.combobox.SaneComboBox; import com.mucommander.ui.text.KeyStrokeUtils; import com.mucommander.utils.text.Translator; import org.intellij.lang.annotations.MagicConstant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.PlainDocument; import java.awt.*; import java.awt.event.*; /** * @author Maxence Bernard */ public class KeyboardShortcutChooser extends JPanel implements ItemListener, ComboBoxListener, FocusListener, KeyListener { private static final Logger LOGGER = LoggerFactory.getLogger(KeyboardShortcutChooser.class); private final JTextField textField; private final JCheckBox[] modifierCheckBoxes; private final SaneComboBox keyComboBox; private KeyStroke currentKeyStroke; private boolean updatingTextField; private boolean updatingComboBox; private boolean updatingCheckBoxes; private final String noneString = "<"+Translator.get("none")+">"; private final static int[] KEY_CHOICES = new int[] { KeyEvent.VK_ESCAPE, KeyEvent.VK_TAB, KeyEvent.VK_DELETE, KeyEvent.VK_BACK_SPACE, KeyEvent.VK_ENTER, KeyEvent.VK_BACK_QUOTE, KeyEvent.VK_MINUS, KeyEvent.VK_EQUALS, KeyEvent.VK_OPEN_BRACKET, KeyEvent.VK_CLOSE_BRACKET, KeyEvent.VK_BACK_SLASH, KeyEvent.VK_SEMICOLON, KeyEvent.VK_QUOTE, KeyEvent.VK_COMMA, KeyEvent.VK_PERIOD, KeyEvent.VK_SLASH, KeyEvent.VK_UP, KeyEvent.VK_DOWN, KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_PAGE_UP, KeyEvent.VK_PAGE_DOWN, KeyEvent.VK_HOME, KeyEvent.VK_END, KeyEvent.VK_INSERT, KeyEvent.VK_PRINTSCREEN, KeyEvent.VK_A, KeyEvent.VK_B, KeyEvent.VK_C, KeyEvent.VK_D, KeyEvent.VK_E, KeyEvent.VK_F, KeyEvent.VK_G, KeyEvent.VK_H, KeyEvent.VK_I, KeyEvent.VK_J, KeyEvent.VK_K, KeyEvent.VK_L, KeyEvent.VK_M, KeyEvent.VK_N, KeyEvent.VK_O, KeyEvent.VK_P, KeyEvent.VK_Q, KeyEvent.VK_R, KeyEvent.VK_S, KeyEvent.VK_T, KeyEvent.VK_U, KeyEvent.VK_V, KeyEvent.VK_W, KeyEvent.VK_X, KeyEvent.VK_Y, KeyEvent.VK_Z, KeyEvent.VK_0, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3, KeyEvent.VK_4, KeyEvent.VK_5, KeyEvent.VK_6, KeyEvent.VK_7, KeyEvent.VK_8, KeyEvent.VK_9, KeyEvent.VK_F1, KeyEvent.VK_F2, KeyEvent.VK_F3, KeyEvent.VK_F4, KeyEvent.VK_F5, KeyEvent.VK_F6, KeyEvent.VK_F7, KeyEvent.VK_F8, KeyEvent.VK_F9, KeyEvent.VK_F10, KeyEvent.VK_F11, KeyEvent.VK_F12, KeyEvent.VK_ADD, KeyEvent.VK_SUBTRACT, KeyEvent.VK_MULTIPLY, KeyEvent.VK_DIVIDE, KeyEvent.VK_NUM_LOCK, KeyEvent.VK_NUMPAD0, KeyEvent.VK_NUMPAD1, KeyEvent.VK_NUMPAD2, KeyEvent.VK_NUMPAD3, KeyEvent.VK_NUMPAD4, KeyEvent.VK_NUMPAD5, KeyEvent.VK_NUMPAD6, KeyEvent.VK_NUMPAD7, KeyEvent.VK_NUMPAD8, KeyEvent.VK_NUMPAD9 }; private final static int[] MODIFIER_TABLE = { KeyEvent.SHIFT_DOWN_MASK, KeyEvent.CTRL_DOWN_MASK, KeyEvent.ALT_DOWN_MASK, KeyEvent.META_DOWN_MASK }; private final static Color FOCUSED_TEXT_FIELD_FOREGROUND = Color.BLACK; private final static Color UNFOCUSED_TEXT_FIELD_FOREGROUND = Color.DARK_GRAY; public KeyboardShortcutChooser() { this(null); } public KeyboardShortcutChooser(KeyStroke keyStroke) { super(new BorderLayout()); JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); this.currentKeyStroke = keyStroke; textField = new JTextField(30) { @Override protected boolean processKeyBinding(KeyStroke keyStroke, KeyEvent keyEvent, int i, boolean b) { return true; } }; textField.setDocument(new PlainDocument() { @Override public void insertString(int i, String string, AttributeSet attributeSet) throws BadLocationException { if (updatingTextField) { super.insertString(i, string, attributeSet); } } @Override public void remove(int i, int i1) throws BadLocationException { if (updatingTextField) { super.remove(i, i1); } } }); flowPanel.add(textField); modifierCheckBoxes = new JCheckBox[MODIFIER_TABLE.length]; for (int i = 0; i < MODIFIER_TABLE.length; i++) { @MagicConstant(flagsFromClass = java.awt.event.InputEvent.class) int modifier = MODIFIER_TABLE[i]; modifierCheckBoxes[i] = new JCheckBox(InputEvent.getModifiersExText(modifier)); flowPanel.add(modifierCheckBoxes[i]); modifierCheckBoxes[i].addItemListener(this); } keyComboBox = new SaneComboBox<>(); keyComboBox.addItem(new KeyChoice(0, noneString)); for (int keyChoice : KEY_CHOICES) { addKeyChoice(keyChoice); } flowPanel.add(keyComboBox); add(flowPanel, BorderLayout.NORTH); updateKeyComboBox(); updateCheckBoxes(); updateTextField(); keyComboBox.addComboBoxListener(this); textField.addKeyListener(this); textField.addFocusListener(this); textField.setForeground(UNFOCUSED_TEXT_FIELD_FOREGROUND); } private void addKeyChoice(int keyValue) { keyComboBox.addItem(new KeyChoice(keyValue, KeyEvent.getKeyText(keyValue))); } private void updateTextField() { LOGGER.trace("currentKeyStroke="+currentKeyStroke+" keyCode="+ (currentKeyStroke==null?"null":""+currentKeyStroke.getKeyCode())); updatingTextField = true; if (currentKeyStroke == null || currentKeyStroke.getKeyCode() == 0) { textField.setText(noneString); } else { textField.setText(KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(currentKeyStroke)); } updatingTextField = false; } private void updateKeyComboBox() { updatingComboBox = true; int keyCode = currentKeyStroke==null?0:currentKeyStroke.getKeyCode(); LOGGER.trace("keyCode="+ keyCode); int nbChoices = keyComboBox.getItemCount(); for (int i=1; i. */ package com.mucommander.ui.chooser; import com.mucommander.utils.text.Translator; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.LineBorder; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * PreviewLabel is a component used to preview a color selection that will eventually be used on a label. * This component is used by {@link ColorChooser} to preview the current color selection. * * @author Nicolas Rinaudo, Maxence Bernard */ public class PreviewLabel extends JLabel implements PropertyChangeListener, Cloneable { /** Color painted on top of the label. */ private Color overlayColor; /** Label's border, if necessary. */ private Border border; /** Controls whether the overlay should be painted over or under the text. */ private boolean overlayUnderText; public final static String FOREGROUND_COLOR_PROPERTY_NAME = "PreviewLabel.ForegroundColor"; public final static String BACKGROUND_COLOR_PROPERTY_NAME = "PreviewLabel.BackgroundColor"; public final static String OVERLAY_COLOR_PROPERTY_NAME = "PreviewLabel.OverlayColor"; public final static String BORDER_COLOR_PROPERTY_NAME = "PreviewLabel.BorderColor"; /** * Creates a new preview label. */ public PreviewLabel() { super(" "); addPropertyChangeListener(this); } /** * Sets the label's overlay color. */ public void setOverlay(Color color) { putClientProperty(OVERLAY_COLOR_PROPERTY_NAME, color); } public void setTextPainted(boolean b) { setText(b ? Translator.get("sample_text") : " "); } public void setOverlayUnderText(boolean b) { overlayUnderText = b; repaint(); } public void setBorderColor(Color color) { putClientProperty(BORDER_COLOR_PROPERTY_NAME, color); } private void paintText(Graphics g) { g.setColor(getForeground()); g.setFont(getFont()); FontMetrics metrics = getFontMetrics(getFont()); g.drawString(getText(), (getWidth() - metrics.stringWidth(getText())) / 2, (getHeight() - metrics.getHeight()) / 2 + metrics.getAscent()); } @Override public void setForeground(Color color) { putClientProperty(FOREGROUND_COLOR_PROPERTY_NAME, color); } @Override public void setBackground(Color color) { putClientProperty(BACKGROUND_COLOR_PROPERTY_NAME, color); } @Override public Object clone() throws CloneNotSupportedException { PreviewLabel label = (PreviewLabel)super.clone(); label.addPropertyChangeListener(label); return label; } /** * Paints the preview label. */ @Override public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); g.setColor(getBackground()); g.fillRect(0, 0, width, height); if (!overlayUnderText) { paintText(g); } if (overlayColor != null) { g.setColor(overlayColor); g.fillRect(0, 0, width/2, height); } if (overlayUnderText) { paintText(g); } if (border != null) { border.paintBorder(this, g, 0, 0, width, height); } } @Override public Dimension getPreferredSize() { Dimension dimension = super.getPreferredSize(); dimension.setSize(dimension.getWidth()+8, dimension.getHeight()+6); return dimension; } @Override public void propertyChange(PropertyChangeEvent event) { String name = event.getPropertyName(); Object value = event.getNewValue(); if (FOREGROUND_COLOR_PROPERTY_NAME.equals(name)) { super.setForeground((Color)value); } else if (BACKGROUND_COLOR_PROPERTY_NAME.equals(name)) { super.setBackground((Color)value); } else if (OVERLAY_COLOR_PROPERTY_NAME.equals(name)) { overlayColor = (Color)value; repaint(); } else if (BORDER_COLOR_PROPERTY_NAME.equals(name)) { border = new LineBorder((Color)value, 1); repaint(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/SizeChooser.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.chooser; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.combobox.TcComboBox; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.util.WeakHashMap; /** * SizeChooser is a compound component made of a JComboBox and a JSpinner that * allows the user to enter a size in multiple of a selectable unit: byte, kilobyte, megabyte, ... * Each time the value changes, a ChangeEvent is fired to registered listeners. * *

    This component can also serve to enter a speed in byte/kilobyte/megabyte/... per second. This only affects the * units displayed, this component works in the exact same way otherwise. * * @author Maxence Bernard */ public class SizeChooser extends JPanel { /** Allows to enter a value in multiple of the current unit */ private final JSpinner valueSpinner; /** Allows to select the size/speed unit */ private final JComboBox unitComboBox; /** Contains all registered listeners, stored as weak references */ private final WeakHashMap listeners = new WeakHashMap<>(); /** Maximum value allowed by the spinner */ private final static int MAX_SPINNER_VALUE = Integer.MAX_VALUE; /** Value increase/decrease when clicking the spinner's up/down buttons */ private final static int SPINNER_STEP = 100; /** Maximum number of columns that the spinner's text field can have */ private final static int MAX_SPINNER_COLUMNS = 7; /** * Creates a new SizeChooser. * * @param speedUnits if true, speed units will be displayed (B/s, KB/s, MB/s, ...) instead of size unit (B, KB, MB, ...). */ public SizeChooser(boolean speedUnits) { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); valueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, MAX_SPINNER_VALUE, SPINNER_STEP)); valueSpinner.addChangeListener(e -> fireChangeEvent()); // Limit the number of columns of the spinner's JTextField to a reasonable amount. // By default, the text field has as many columns as needed to fit the spinner maximum value. // If this maximum value is Integer.MAX_VALUE, the text field has 13 columns which makes it enormous. JComponent editor = valueSpinner.getEditor(); if (editor instanceof JSpinner.DefaultEditor) { JTextField textField = ((JSpinner.DefaultEditor)editor).getTextField(); int nbColumns = textField.getColumns(); if (nbColumns > MAX_SPINNER_COLUMNS ) { textField.setColumns(MAX_SPINNER_COLUMNS); } textField.setMaximumSize(textField.getPreferredSize()); } add(valueSpinner); unitComboBox = new TcComboBox<>(); for (int i = SizeFormat.BYTE_UNIT; i <= SizeFormat.GIGABYTE_UNIT; i++) { unitComboBox.addItem(SizeFormat.getUnitString(i, speedUnits)); } unitComboBox.setSelectedIndex(SizeFormat.KILOBYTE_UNIT); unitComboBox.addItemListener(e -> fireChangeEvent()); add(unitComboBox); } @Override public Dimension getSize() { // the panel height can't be greater than the text editor height Dimension d = super.getSize(); final int maxHeight = (int)valueSpinner.getEditor().getPreferredSize().getHeight(); if (d.getHeight() > maxHeight) { d.height = maxHeight; } return d; } /** * Returns the current value expressed in bytes. * * @return the current value expressed in bytes */ public long getValue() { return SizeFormat.getUnitBytes(unitComboBox.getSelectedIndex())* (Integer) valueSpinner.getValue(); } /** * Adds the specified ChangedListener to the list of registered listeners. * *

    Listeners are stored as weak references so {@link #remove} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the ChangeListener to add to the list of registered listeners. */ public synchronized void addChangeListener(ChangeListener listener) { listeners.put(listener, null); } /** * Removes the specified ChangeListener from the list of registered listeners. * * @param listener the ChangeListener to remove from the list of registered listeners. */ public synchronized void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } /** * Notifies all registered ChangeListener that the current value has changed. This method is called as the result * of a change in the spinner or the combo box. */ public synchronized void fireChangeEvent() { for (ChangeListener listener : listeners.keySet()) listener.stateChanged(new ChangeEvent(this)); } @Override public void setEnabled(boolean enabled) { valueSpinner.setEnabled(enabled); unitComboBox.setEnabled(enabled); } } ================================================ FILE: src/main/java/com/mucommander/ui/chooser/package.html ================================================ Provides various UI components used to let users choose between sets of values. ================================================ FILE: src/main/java/com/mucommander/ui/combobox/AutocompleteEditableCombobox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; import com.mucommander.ui.autocomplete.EditableComboboxCompletion; import com.mucommander.ui.autocomplete.TypicalAutocompleterEditableCombobox; import com.mucommander.ui.autocomplete.completers.Completer; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Vector; /** * AutocompleteEditableCombobox is an editable combo-box that provides * auto-completion capabilities based on the given Completer. * * @author Arik Hadas */ public class AutocompleteEditableCombobox extends EditableComboBox { /** * Creates a new editable combo box and a JTextField to be used as the editor. * Has the same effect as calling {@link EditableComboBox#EditableComboBox(javax.swing.JTextField)} with a null value. */ public AutocompleteEditableCombobox(Completer completer) { super(); enableAutoCompletion(completer); } /** * Creates a new editable combo box using the given text field as the editor. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. */ public AutocompleteEditableCombobox(JTextField textField, Completer completer) { super(textField); enableAutoCompletion(completer); } /** * Creates a new editable combo box using the given text field as the editor and ComboBoxModel. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. * @param comboBoxModel the ComboBoxModel to use for this combo box * @param completer Completer object */ public AutocompleteEditableCombobox(JTextField textField, ComboBoxModel comboBoxModel, Completer completer) { super(textField, comboBoxModel); enableAutoCompletion(completer); } /** * Creates a new editable combo box using the given text field as the editor and items to populate the initial items list. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. * @param items items used to populate the initial items list. * @param completer Completer object */ public AutocompleteEditableCombobox(JTextField textField, E[] items, Completer completer) { super(textField, items); enableAutoCompletion(completer); } /** * Creates a new editable combo box using the given text field as the editor and items to populate the initial items list. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. * @param items items used to populate the initial items list. * @param completer Completer object */ public AutocompleteEditableCombobox(JTextField textField, Vector items, Completer completer) { super(textField, items); enableAutoCompletion(completer); } private void enableAutoCompletion(Completer completer) { new EditableComboboxCompletion(new TypicalAutocompleterEditableCombobox(this), completer); } /** * The desired behavior of this editable combo-box when enter key is pressed. * * @param keyEvent - the KeyEvent that occurred. */ public void respondToEnterKeyPressing(KeyEvent keyEvent) { // Combo popup menu is visible if (isPopupVisible()) { // Note that since the event is not consumed, JComboBox will catch it and fire } // Combo popup menu is not visible, these events really belong to the text field else { // Notify listeners that the text field has been validated fireComboFieldValidated(); // /!\ Consume the event so to prevent JComboBox from firing an ActionEvent (default JComboBox behavior) keyEvent.consume(); } } /** * The desired behavior of this editable combo-box when escape key is pressed. * * @param keyEvent - the KeyEvent that occurred. */ public void respondToEscapeKeyPressing(KeyEvent keyEvent) { // Combo popup menu is visible if (isPopupVisible()) { // Explicitely hide popup menu, JComboBox does not seem do it automatically (at least under Mac OS X + Java 1.5 and Java 1.4) hidePopup(); // Consume the event so that it is not propagated, since dialogs catch this event to close the window keyEvent.consume(); } else { // Combo popup menu is not visible, these events really belong to the text field // Notify listeners that the text field has been cancelled fireComboFieldCancelled(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/ComboBoxCellRenderer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; import lombok.Setter; import javax.swing.*; import java.awt.*; /** * @author Nicolas Rinaudo */ public class ComboBoxCellRenderer implements ListCellRenderer { private Color textColor; private Color backgroundColor; private Color selectedTextColor; private Color selectedBackgroundColor; @Setter private Font font; private final JLabel label; public ComboBoxCellRenderer() { label = new JLabel(); label.setOpaque(true); } public void setForeground(Color color) {textColor = color;} public void setBackground(Color color) {backgroundColor = color;} public void setSelectionForeground(Color color) {selectedTextColor = color;} public void setSelectionBackground(Color color) {selectedBackgroundColor = color;} public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean hasFocus) { if(value!=null) label.setText(value.toString()); if(font == null) label.setFont(list.getFont()); else label.setFont(font); if(isSelected) { if(selectedBackgroundColor == null) label.setBackground(list.getSelectionBackground()); else label.setBackground(selectedBackgroundColor); if(selectedTextColor == null) label.setForeground(list.getSelectionForeground()); else label.setForeground(selectedTextColor); } else { if(backgroundColor == null) label.setBackground(list.getBackground()); else label.setBackground(backgroundColor); if(textColor == null) label.setForeground(list.getForeground()); else label.setForeground(textColor); } return label; } } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/ComboBoxListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; /** * Interface to be implemented by classes that wish to be notified of selections occuring on a {@link SaneComboBox}. * @author Maxence Bernard */ public interface ComboBoxListener { /** * This method is called when an item has been selected from the specified combo box popup menu. * The item may have been selected either with the 'Enter' key, or by clicking on the item. * * @param source the SaneComboBox on which the event was triggered */ void comboBoxSelectionChanged(SaneComboBox source); } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/EditableComboBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; import lombok.Getter; import lombok.Setter; import javax.swing.*; import javax.swing.plaf.basic.BasicComboBoxEditor; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.Vector; import java.util.WeakHashMap; /** * EditableComboBox is an editable combo box (really!) that can use a specified JTextField to be used as the editor. * *

    EditableComboBox also extends JComboBox to make it much easier to use, instead of having to work around its * numerous bugs and weird behavior (understatement). Registering a {@link EditableComboBoxListener} makes it * easy to know for sure when an item has been selected from the combo popup menu, or when the text field has been * validated ('Enter' key pressed) or cancelled ('Escape' key pressed). It is strongly recommanded to use this interface * instead of ActionListener / ItemListener, their already erratic behavior could be further aggravated by the tweakings * used in this class. * *

    The {@link #setComboSelectionUpdatesTextField(boolean)} method allows to automatically replace the text field's * contents when an item is selected from the associated combo box, replacing its value by the selected item's * string representation. This feature is disabled by default. * * @author Maxence Bernard * @see EditableComboBoxListener */ public class EditableComboBox extends SaneComboBox { /** * Used to render the content of the combo box. */ private ComboBoxCellRenderer renderer; /** * The text field used as the combo box's editor * -- GETTER -- * Returns the text field used as the combo box's editor. */ @Getter private JTextField textField; /** * Contains all registered EditableComboBoxListener instances, stored as weak references */ private final WeakHashMap listeners = new WeakHashMap<>(); /** * Specifies whether the text field's contents is updated when an item is selected in the associated combo box * -- SETTER -- * If true is specified, when an item is selected in this combo box, the text field's contents * will be automatically replaced by the selected item's string representation. */ @Setter private boolean comboSelectionUpdatesTextField; /** * Creates a new editable combo box and a JTextField to be used as the editor. * Has the same effect as calling {@link #EditableComboBox(javax.swing.JTextField)} with a null value. */ public EditableComboBox() { init(null); } /** * Creates a new editable combo box using the given text field as the editor. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. */ public EditableComboBox(JTextField textField) { init(textField); } /** * Creates a new editable combo box using the given text field as the editor and ComboBoxModel. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. * @param comboBoxModel the ComboBoxModel to use for this combo box */ public EditableComboBox(JTextField textField, ComboBoxModel comboBoxModel) { super(comboBoxModel); init(textField); } /** * Creates a new editable combo box using the given text field as the editor and items to populate the initial items list. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. * @param items items used to populate the initial items list. */ public EditableComboBox(JTextField textField, E[] items) { super(items); init(textField); } /** * Creates a new editable combo box using the given text field as the editor and items to populate the initial items list. * * @param textField the text field to be used as the combo box's editor. If null, a new JTextField instance * will be created and used. * @param items items used to populate the initial items list. */ public EditableComboBox(JTextField textField, Vector items) { super(items); init(textField); } /** * If true is returned, when an item is selected in this combo box, the text field's contents * will be automatically replaced by the selected item's string representation. * This feature is disabled by default (false is returned). */ public boolean getComboSelectionUpdatesTextField() { return comboSelectionUpdatesTextField; } /** * Initializes the combo box to make it editable and use the given text field. * * @param textField the text field to be used as the combo box's editor. If null, a JTextField instance will be created and used. */ private void init(JTextField textField) { setRenderer(renderer = new ComboBoxCellRenderer<>()); // create a new JTextField if no text field was specified if (textField == null) { this.textField = new JTextField(); } // Use the specified text field else this.textField = textField; // Use a custom editor that uses the text field setEditor(new BasicComboBoxEditor() { @Override public Component getEditorComponent() { return EditableComboBox.this.textField; } }); // Make this combo box editable setEditable(true); // Note: the default JComboBox behavior is to also fire an ActionEvent when enter is pressed on the text field // and the popup menu is not visible, making the ActionEvent indistinguishable from a combo item selection. // This awful behavior is overridden by keyPressed() so that ActionEvent is only fired for item selections. // Listen to the text field's key events. These are fired regardless of the combo box popup menu being visible or not. // The following KeyListener is added as an anonymous inner class so that any class overridding EditableComboBox // can safely implement KeyListener without risking to override those methods by accident. this.textField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent keyEvent) { int keyCode = keyEvent.getKeyCode(); // Combo popup menu is visible if (isPopupVisible()) { if (keyCode == KeyEvent.VK_ENTER) { // Note that since the event is not consumed, JComboBox will catch it and fire } else if (keyCode == KeyEvent.VK_ESCAPE) { // Explicitly hide popup menu, JComboBox does not seem do it automatically (at least under Mac OS X + Java 1.5 and Java 1.4) hidePopup(); // Consume the event so that it is not propagated, since dialogs catch this event to close the window keyEvent.consume(); } } // Combo popup menu is not visible, these events really belong to the text field else { if (keyCode == KeyEvent.VK_ENTER) { // Notify listeners that the text field has been validated fireComboFieldValidated(); // /!\ Consume the event so to prevent JComboBox from firing an ActionEvent (default JComboBox behavior) keyEvent.consume(); } else if (keyCode == KeyEvent.VK_ESCAPE) { // Notify listeners that the text field has been cancelled fireComboFieldCancelled(); } } } }); } ////////////////////////////////////////////// // EditableComboBoxListener support methods // ////////////////////////////////////////////// /** * Adds the specified EditableComboBoxListener to the list of registered listeners. * *

    Listeners are stored as weak references so {@link #removeEditableComboBoxListener(EditableComboBoxListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the EditableComboBoxListener to add to the list of registered listeners. */ public void addEditableComboBoxListener(EditableComboBoxListener listener) { addComboBoxListener(listener); listeners.put(listener, null); } /** * Removes the specified EditableComboBoxListener from the list of registered listeners. * * @param listener the EditableComboBoxListener to remove from the list of registered listeners. */ public void removeEditableComboBoxListener(EditableComboBoxListener listener) { removeComboBoxListener(listener); listeners.remove(listener); } /** * Overrides {@link SaneComboBox#fireComboBoxSelectionChanged()} to set the text field's contents to the item that * has been selected, if {@link #setComboSelectionUpdatesTextField(boolean)} has been enabled. */ @Override protected void fireComboBoxSelectionChanged() { if (comboSelectionUpdatesTextField) { // Replace the text field's contents by the selected item's string representation, // only if this feature has been enabled if (getSelectedIndex() != -1) textField.setText(getSelectedItem().toString()); } super.fireComboBoxSelectionChanged(); } /** * Notifies all registered EditableComboBoxListener instances that the text field has been validated, that is * the 'Enter' key has been pressed in the text field, without the popup menu being visible. * *

    Note: Unlike JComboBox's weird ActionEvent handling, this event is *not* fired when 'Enter' is pressed * in the combo popup menu. */ protected void fireComboFieldValidated() { // Iterate on all listeners for (EditableComboBoxListener listener : listeners.keySet()) listener.textFieldValidated(this); } /** * Notifies all registered EditableComboBoxListener instances that the text field has been cancelled, that is * the 'Escape' key has been pressed in the text field, without the popup menu being visible. * *

    Note: This event is *not* fired when 'Escape' is pressed in the combo popup menu. */ protected void fireComboFieldCancelled() { // Iterate on all listeners for (EditableComboBoxListener listener : listeners.keySet()) listener.textFieldCancelled(this); } // - Aspect managenement ------------------------------------------------------------- // ----------------------------------------------------------------------------------- @Override public void setForeground(Color color) { if (renderer == null) super.setForeground(color); else { renderer.setForeground(color); textField.setForeground(color); } } @Override public void setBackground(Color color) { if (renderer == null) super.setBackground(color); else { renderer.setBackground(color); textField.setBackground(color); } } public void setSelectionForeground(Color color) { if (renderer != null) { renderer.setSelectionForeground(color); textField.setSelectedTextColor(color); } } public void setSelectionBackground(Color color) { if (renderer != null) { renderer.setSelectionBackground(color); textField.setSelectionColor(color); } } @Override public void setFont(Font font) { super.setFont(font); if (renderer != null) { renderer.setFont(font); textField.setFont(font); } } } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/EditableComboBoxListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; /** * Interface to be implemented by classes that wish to be notified of actions occuring on a {@link EditableComboBox}. * Those classes need to be registered to receive those events, this can be done by calling * {@link EditableComboBox#addEditableComboBoxListener(EditableComboBoxListener)}. * * @author Maxence Bernard */ public interface EditableComboBoxListener extends ComboBoxListener { /** * This method is called when the text field has been validated, that is the 'Enter' key has been pressed * in the text field, without the popup menu being visible. * *

    Note: Unlike JComboBox's weird ActionEvent handling, this method is *not* called when 'Enter' is pressed * in the combo popup menu. * * @param source the EditableComboBox containing the JTextField on which the event was triggered */ void textFieldValidated(EditableComboBox source); /** * Notifies all registered EditableComboBoxListener instances that the text field has been cancelled, that is * the 'Escape' key has been pressed in the text field, without the popup menu being visible. * *

    Note: This method is *not* called when 'Escape' is pressed in the combo popup menu. * * @param source the EditableComboBox containing the JTextField on which the event was triggered */ void textFieldCancelled(EditableComboBox source); } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/SaneComboBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; import com.mucommander.ui.dialog.FocusDialog; import javax.swing.*; import java.awt.Container; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.Vector; import java.util.WeakHashMap; /** * SaneComboBox is a JComboBox which does not have the awful default JComboBox behavior of firing ActionEvents * when navigating with the arrow keys between choices of the popup menu. * This page describes the problem in details: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4199622 * *

    Also when using {@link ComboBoxListener}, action events that are normally triggered by JComboBox * when the add/insert/remove item methods are called are filtered out, only actual selection changes performed * by the user are fired. * * @author Maxence Bernard */ public class SaneComboBox extends JComboBox { private WeakHashMap listeners = new WeakHashMap<>(); private boolean ignoreActionEvent; public SaneComboBox() { super(); init(); } public SaneComboBox(ComboBoxModel comboBoxModel) { super(comboBoxModel); init(); } public SaneComboBox(E[] items) { super(items); init(); } public SaneComboBox(Vector items) { super(items); init(); } private void init() { // Prevent up/down keys from firing ActionEvents // for Java 1.3 putClientProperty("JComboBox.lightweightKeyboardNavigation","Lightweight"); // Commented as it causes rendering issues under Mac OS X Leopard (does not render like a native combo box) // // for Java 1.4 and up // putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE); // Listen to combo box action events, these are fired each time an item is selected when the popup menu // is visible, either by pressing 'Enter' on an item or by clicking on it. addActionListener(new AbstractAction() { public void actionPerformed(ActionEvent actionEvent) { // Filter out action events triggered by the add/insert/remove item methods if (!ignoreActionEvent) fireComboBoxSelectionChanged(); } }); } ////////////////////////////////////// // ComboBoxListener support methods // ////////////////////////////////////// /** * Adds the specified ComboBoxListener to the list of registered listeners. * *

    Listeners are stored as weak references so {@link #removeComboBoxListener(ComboBoxListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the ComboBoxListener to add to the list of registered listeners. */ public void addComboBoxListener(ComboBoxListener listener) { listeners.put(listener, null); } /** * Removes the specified ComboBoxListener from the list of registered listeners. * * @param listener the ComboBoxListener to remove from the list of registered listeners. */ public void removeComboBoxListener(ComboBoxListener listener) { listeners.remove(listener); } /** * Notifies all registered ComboBoxListener instances that an item has been selected from the * combo box popup menu. The item may have been selected either with the 'Enter' key, or by clicking on the item. * *

    Unlike JComboBox ActionListener behavior, calls to the add/insert/remove item methods do *not* trigger * a selection event. */ protected void fireComboBoxSelectionChanged() { // Iterate on all listeners for (ComboBoxListener listener : listeners.keySet()) listener.comboBoxSelectionChanged(this); } //////////////////////// // Overridden methods // //////////////////////// @Override public void addItem(E object) { ignoreActionEvent = true; super.addItem(object); ignoreActionEvent = false; } @Override public void insertItemAt(E object, int i) { ignoreActionEvent = true; super.insertItemAt(object, i); ignoreActionEvent = false; } @Override public void removeItem(Object object) { ignoreActionEvent = true; super.removeItem(object); ignoreActionEvent = false; } @Override public void removeItemAt(int i) { ignoreActionEvent = true; super.removeItemAt(i); ignoreActionEvent = false; } @Override public void removeAllItems() { ignoreActionEvent = true; super.removeAllItems(); ignoreActionEvent = false; } @Override public void processKeyEvent(KeyEvent e) { boolean popupVisible = isPopupVisible(); super.processKeyEvent(e); // Close parent FocusDialog if ESC pressed if (!popupVisible && e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ESCAPE) { Container container = getParent(); while (container != null) { if (container instanceof FocusDialog) { ((FocusDialog) container).dispose(); break; } container = container.getParent(); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/TcComboBox.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.combobox; import com.mucommander.ui.dialog.FocusDialog; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; /** * ComboBox with correct Esc handling. * * Created on 20/10/14. * @author Oleg Trifonov */ public class TcComboBox extends JComboBox { public TcComboBox() { super(); } public TcComboBox(java.util.List items) { super(); for (E item : items) { addItem(item); } } public TcComboBox(E[] items) { super(); for (E item : items) { addItem(item); } } @Override public void processKeyEvent(KeyEvent e) { boolean popupVisible = isPopupVisible(); super.processKeyEvent(e); // Close parent FocusDialog if ESC pressed if (!popupVisible && e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ESCAPE) { disposeParentFocusDialog(); } } private void disposeParentFocusDialog() { Container container = getParent(); while (container != null) { if (container instanceof FocusDialog) { ((FocusDialog) container).dispose(); break; } container = container.getParent(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/combobox/package.html ================================================ Provides various classes meant to work around the many JComboBox issues. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/DialogOwner.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog; import java.awt.*; /** * This class wraps a dialog owner, which can either be a {@link Frame} or a {@link Dialog}. * Its purpose is to avoid the duplication of constructors of {@link Dialog} subclasses. * * @author Maxence Bernard */ public class DialogOwner { protected Window owner; /** * Creates a new DialogOwner wrapping the given {@link Frame}. * * @param frame the dialog owner */ public DialogOwner(Frame frame) { this.owner = frame; } /** * Creates a new DialogOwner wrapping the given {@link Dialog}. * * @param dialog the dialog owner */ public DialogOwner(Dialog dialog) { this.owner = dialog; } /** * Returns the owner {@link Window} which was passed to the constructor. * * @return the owner {@link Window} which was passed to the constructor. */ public Window getOwner() { return owner; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/DialogToolkit.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog; import java.awt.Component; import java.awt.Dialog; import java.awt.Dimension; import java.awt.Frame; import java.awt.HeadlessException; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRootPane; import com.mucommander.conf.TcSnapshot; import com.mucommander.ui.button.ButtonChoicePanel; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.helper.ScreenServices; /** * Placeholder for convenience methods that ease dialog creation. * * @author Maxence Bernard */ public class DialogToolkit { public static boolean fitToMinDimension(Window window, Dimension minD) { return fitToDimension(window, minD, true); } static boolean fitToMaxDimension(Window window, Dimension maxD) { return fitToDimension(window, maxD, false); } public static boolean fitToScreen(Window window) { Rectangle screenBounds = ScreenServices.getFullScreenBounds(window); return fitToMaxDimension(window, new Dimension((int)screenBounds.getWidth(), (int)screenBounds.getHeight())); } private static boolean fitToDimension(Window window, Dimension d, boolean min) { int maxWidth = (int)d.getWidth(); int maxHeight = (int)d.getHeight(); int windowWidth = window.getWidth(); int windowHeight = window.getHeight(); boolean changeSize = false; // Minimum dimension if (min) { if (windowWidth < maxWidth) { windowWidth = maxWidth; changeSize = true; } if (windowHeight < maxHeight) { windowHeight = maxHeight; changeSize = true; } } // Maximum dimension else { if (windowWidth > maxWidth) { windowWidth = maxWidth; changeSize = true; } if (windowHeight > maxHeight) { windowHeight = maxHeight; changeSize = true; } } // Dimension needs to be changed if (changeSize) { window.setSize(windowWidth, windowHeight); } // Return true if dimension was changed return changeSize; } /** * Sets the given component's (JFrame, JDialog...) location to be centered on screen. * @param c component to center on screen */ public static void centerOnScreen(Component c) { Dimension screenSize = TcSnapshot.getScreenSize(); c.setLocation(screenSize.width/2 - c.getWidth()/2, screenSize.height/2 - c.getHeight()/2); } /** * Centers the specified component on the specified window. *

    * Note that this method assumes c's dimension to be at most that of the screen. * This can be ensured through {@link #fitToScreen(Window)}. If this constraint is not respected, * behavior is unpredictable. * * @param c component to center. * @param window window to center on. */ public static void centerOnWindow(Component c, Window window) { int x = Math.max(0, window.getX() + (window.getWidth() - c.getWidth()) / 2); int y = Math.max(0, window.getY() + (window.getHeight() - c.getHeight()) / 2); Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); int buffer; if ((buffer = screenSize.width - (c.getWidth() + x)) < 0) { x += buffer; } if ((buffer = screenSize.height - (c.getHeight() + y)) < 0) { y += buffer; } c.setLocation(x, y); } /** * Creates an OK/Cancel panel using the given buttons, and register the given listener for button actions. * @param okButton OK button * @param cancelButton Cancel button * @param actionListener action listener * @param rootPane root panel */ public static JPanel createOKCancelPanel(JButton okButton, JButton cancelButton, JRootPane rootPane, ActionListener actionListener) { return createButtonPanel(rootPane, actionListener, okButton, cancelButton); } /** * Creates an OK panel using the given button, and register the given listener for button actions. * @param okButton OK button * @param actionListener action listener * @param rootPane root panel */ public static JPanel createOKPanel(JButton okButton, JRootPane rootPane, ActionListener actionListener) { return createButtonPanel(rootPane, actionListener, okButton); } /** * Creates a button panel using the given buttons, and register the given listener for button actions. * Buttons are disposed horizontally, aligned to the right. * @param rootPane root panel * @param actionListener action listener * @param buttons buttons */ public static JPanel createButtonPanel(JRootPane rootPane, ActionListener actionListener, JButton ... buttons) { JPanel panel = new ButtonChoicePanel(buttons, 0, rootPane); MnemonicHelper mnemonicHelper = new MnemonicHelper(); for (JButton button : buttons) { button.setMnemonic(mnemonicHelper.getMnemonic(button.getText())); button.addActionListener(actionListener); panel.add(button); } return panel; } /** * Returns the specified component's top level Frame or * Dialog. * * @param parentComponent the Component to check for a * Frame or Dialog * @return the Frame or Dialog that * contains the component, or the default * frame if the component is null, * or does not have a valid * Frame or Dialog parent * @exception HeadlessException if * GraphicsEnvironment.isHeadless returns * true * @see java.awt.GraphicsEnvironment#isHeadless */ static Window getWindowForComponent(Component parentComponent) throws HeadlessException { // Note: this method is a shameless rip from javax.swing.JOptionPane if (parentComponent == null) { return JOptionPane.getRootFrame(); } if (parentComponent instanceof Frame || parentComponent instanceof Dialog) { return (Window) parentComponent; } return getWindowForComponent(parentComponent.getParent()); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/FocusDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.EmptyBorder; import com.mucommander.cache.WindowsStorage; import com.mucommander.ui.macosx.IMacOsWindow; import com.mucommander.ui.main.MainFrame; import com.mucommander.utils.text.Translator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.helper.FocusRequester; /** * FocusDialog is a modal dialog which extends JDialog to provide the following additional functionalities : *

      *
    • focus can be requested on a specified JComponent once the dialog has been made visible
    • *
    • the screen location of the window can be set relatively to a Component specified in the constructor
    • *
    • a minimum and/or maximum size can be specified and will be used by {@link #pack()} to calculate the effective dialog size
    • *
    • by default, the 'Escape' key disposes the dialog, this can be disabled using {@link #setKeyboardDisposalEnabled(boolean)}
    • *
    * @author Maxence Bernard */ public class FocusDialog extends JDialog implements WindowListener, IMacOsWindow { private static final Logger LOGGER = LoggerFactory.getLogger(FocusDialog.class); private static final EmptyBorder BORDER = new EmptyBorder(6, 8, 6, 8); /** Minimum dimensions of this dialog, may be null */ private Dimension minimumDimension; /** Maximum dimensions of this dialog, may be null */ private Dimension maximumDimension; /** Has this window been activated yet ? */ private boolean firstTimeActivated; /** The component that will receive the focus when this window is activated for the first time, may be null */ private JComponent initialFocusComponent; private Component locationRelativeComp; private boolean keyboardDisposalEnabled = true; /** * Suffix of keyname for position/size storage */ private String storageSuffix; private boolean storeSizes = true; private final static String CUSTOM_DISPOSE_EVENT = "CUSTOM_DISPOSE_EVENT"; private static long lastCreateTime; private static String lastCreateTitle; private static Class lastCreateClass; /** * Saved to restore focus */ private Component ownerFocusedComponent; public FocusDialog(Frame owner, String title, Component locationRelativeComp) { super(owner, title, true); init(locationRelativeComp); if (owner != null) { ownerFocusedComponent = owner.getFocusOwner(); } boolean kill = false; if (title != null && title.equals(lastCreateTitle)) { long dt = System.currentTimeMillis() - lastCreateTime; // sometimes EventDispatchThread duplicates events that caused double windows if (dt < 250 && lastCreateClass != null && lastCreateClass.equals(getClass())) { kill = true; } } lastCreateTime = System.currentTimeMillis(); lastCreateTitle = title; lastCreateClass = getClass(); if (kill) { dispose(); throw new RuntimeException("EventDispatchThread error"); } // if (owner != null) { // showOnScreen(owner); // } } public FocusDialog(Dialog owner, String title, Component locationRelativeComp) { super(owner, title, true); init(locationRelativeComp); if (owner != null) { ownerFocusedComponent = owner.getFocusOwner(); } boolean kill = false; if (title != null && title.equals(lastCreateTitle)) { long dt = System.currentTimeMillis() - lastCreateTime; // sometimes EventDispatchThread duplicates events that caused double windows if (dt < 250 && lastCreateClass != null && lastCreateClass.equals(getClass())) { kill = true; } } lastCreateTime = System.currentTimeMillis(); lastCreateTitle = title; lastCreateClass = getClass(); if (kill) { dispose(); throw new RuntimeException("EventDispatchThread error"); } // if (owner != null) { // showOnScreen(owner); // } } public void showOnScreen(Window parent) { if (getWidth() == 0) { return; } //System.out.println("SIZE " + getWidth() + "x" + getHeight() + " " + getLocation()); GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice[] devices = env.getScreenDevices(); final GraphicsDevice frameDevice = parent.getGraphicsConfiguration().getDevice(); for (GraphicsDevice graphicsDevice : devices) { //System.out.println(frameDevice.getIDstring() + " ' " + graphicsDevice.getIDstring()); if (frameDevice.equals(graphicsDevice) || frameDevice.getIDstring().equals(graphicsDevice.getIDstring())) { final Rectangle monitorBounds = graphicsDevice.getDefaultConfiguration().getBounds(); final int x = monitorBounds.x + (monitorBounds.width - getWidth()) /2; final int y = monitorBounds.y + (monitorBounds.height - getHeight()) / 2; //System.out.println(" " + monitorBounds.width + 'x' + monitorBounds.height + " " + monitorBounds.x + ' ' + monitorBounds.y); setLocation(x, y); } } } private void init(Component locationRelativeComp) { this.locationRelativeComp = locationRelativeComp; setLocationRelativeTo(locationRelativeComp); initLookAndFeel(); JPanel contentPane = (JPanel)getContentPane(); contentPane.setBorder(BORDER); setResizable(true); // Important: dispose (release resources) window on close, default is HIDE_ON_CLOSE setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // Catch escape key presses and have them close the dialog by mapping the escape keystroke to a custom dispose Action InputMap inputMap = contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap = contentPane.getActionMap(); AbstractAction disposeAction = new AbstractAction() { public void actionPerformed(ActionEvent e){ if (keyboardDisposalEnabled) { cancel(); } } }; // Maps the dispose action to the 'Escape' keystroke inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), CUSTOM_DISPOSE_EVENT); actionMap.put(CUSTOM_DISPOSE_EVENT, disposeAction); // Maps the dispose action to the 'Apple+W' keystroke under Mac OS X if (OsFamily.MAC_OS_X.isCurrent()) { inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_DOWN_MASK), CUSTOM_DISPOSE_EVENT); } // Under Windows, Alt+F4 automatically disposes the dialog, nothing to do } /** * Method called when the user has canceled through the escape key. *

    * This method is equivalent to a call to {@link #dispose()}. It's meant to be * overridden by those implementations of FocusDialog that need to init * code before canceling the dialog. */ public void cancel() { dispose(); } @Override public void dispose() { try { WindowsStorage.getInstance().put(this, storageSuffix); saveState(); } catch (Throwable t) { t.printStackTrace(); } super.dispose(); // fixed issue: return to main frame form FocusDialog if ((ownerFocusedComponent instanceof JRootPane && getOwner() instanceof JFrame)) { ownerFocusedComponent = null; } FocusRequester.requestFocus(ownerFocusedComponent != null ? ownerFocusedComponent : getOwner()); } /** * Sets the component that will receive focus once this dialog has been made visible. * * @param initialFocusComponent the component that will receive focus once this dialog has been made visible, if * null, the first component in the dialog will receive focus. */ public void setInitialFocusComponent(JComponent initialFocusComponent) { this.initialFocusComponent = initialFocusComponent; if (initialFocusComponent == null) { removeWindowListener(this); } else { addWindowListener(this); } } /** * Sets a maximum width and height for this dialog. */ @Override public void setMaximumSize(Dimension dimension) { this.maximumDimension = dimension; } /** * Sets a minimum width and height for this dialog. */ @Override public void setMinimumSize(Dimension dimension) { this.minimumDimension = dimension; } /** * Specifies whether this dialog can be automatically disposed using the 'Escape' key and 'Apple+W' under Mac OS X. * If enabled, {@link #dispose()} will be called when one of those keystrokes is pressed from any component * within this dialog. * * @param enabled true to enable automatic keyboard disposal, false to disable it */ protected void setKeyboardDisposalEnabled(boolean enabled) { this.keyboardDisposalEnabled = enabled; } /** * Overrides Window.pack() to take into account minimum and maximum dialog size (if specified). */ @Override public void pack() { super.pack(); if (maximumDimension != null) { DialogToolkit.fitToMaxDimension(this, maximumDimension); } else { super.setMaximumSize(getSize()); DialogToolkit.fitToScreen(this); } if (minimumDimension != null) { DialogToolkit.fitToMinDimension(this, minimumDimension); } else { super.setMinimumSize(getSize()); } } protected void packDialog() { super.pack(); } protected void setMinimumSizeDialog(Dimension d) { minimumDimension = d; super.setMinimumSize(d); } protected void setMaximumSizeDialog(Dimension d) { maximumDimension = d; super.setMaximumSize(d); } /** * Packs this dialog, makes it non-resizable and visible. */ public void showDialog() { if (!WindowsStorage.getInstance().init(this, storageSuffix, storeSizes)) { pack(); if (locationRelativeComp == null) { DialogToolkit.centerOnScreen(this); } else { final int x = locationRelativeComp.getX() + (locationRelativeComp.getWidth() - getWidth()) / 2; final int y = locationRelativeComp.getY() + (locationRelativeComp.getHeight() - getHeight()) / 2; setLocation(x, y); } } SwingUtilities.invokeLater(this::toFront); setVisible(true); } /** * Return true if the dialog has been activated (see WindowListener.windowActivated()). * * @return true if the dialog has been activated */ public boolean isActivated() { return firstTimeActivated; } @Override public void windowOpened(WindowEvent e) { } @Override public void windowActivated(WindowEvent e) { // (this method is called each time the dialog is activated) if (!firstTimeActivated && initialFocusComponent != null) { LOGGER.trace("requesting focus on initial focus component"); // First try using requestFocusInWindow() which is preferred over requestFocus(). If it fails // (returns false), call requestFocus: // "The focus behavior of this method can be implemented uniformly across platforms, and thus developers are // strongly encouraged to use this method over requestFocus when possible. Code which relies on requestFocus // may exhibit different focus behavior on different platforms." if (!initialFocusComponent.requestFocusInWindow()) { LOGGER.trace("requestFocusInWindow failed, calling requestFocus"); FocusRequester.requestFocus(initialFocusComponent); } firstTimeActivated = true; } } @Override public void windowClosing(WindowEvent e) { } @Override public void windowClosed(WindowEvent e) { } @Override public void windowDeactivated(WindowEvent e) { } @Override public void windowIconified(WindowEvent e) { } @Override public void windowDeiconified(WindowEvent e) { } protected void saveState() { } public void setStorageSuffix(String storageSuffix) { this.storageSuffix = storageSuffix; } public void setStoreSizes(boolean storeSizes) { this.storeSizes = storeSizes; } public FocusDialog returnFocusTo(Component c) { this.ownerFocusedComponent = c; return this; } public Component getReturnFocusTo() { return ownerFocusedComponent; } protected void fixHeight() { addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { Dimension preferredSize = getPreferredSize(); int width = getWidth(); int minWidth = minimumDimension != null ? minimumDimension.width : preferredSize.width; setSize(new Dimension(Math.max(width, minWidth), preferredSize.height)); super.componentResized(e); } }); } protected static String i18n(String key, String... params) { return Translator.get(key, params); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/InformationDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog; import com.mucommander.utils.text.Translator; import com.mucommander.ui.button.CollapseExpandButton; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.text.FontUtils; import javax.swing.*; import java.awt.*; import java.io.PrintWriter; import java.io.StringWriter; /** * This class provides static methods to display 'information' dialogs of different kinds: * {@link #ERROR_DIALOG_TYPE error}, {@link #INFORMATION_DIALOG_TYPE information}, {@link #WARNING_DIALOG_TYPE warning} * or {@link #QUESTION_DIALOG_TYPE question}. *

    * While this class is very similar to {@link JOptionPane}, it extends the functionality by adding optional caption * message and exception details to the dialog. It also allows to use generic title and messages for certain * dialog types. *

    * This class uses {@link InformationPane} to display the icon and the main and caption messages. * * @see InformationPane * @author Maxence Bernard */ public class InformationDialog { /** Minimum dialog size */ private static final Dimension MIN_DIALOG_SIZE = new Dimension(360, 0); /** Maximum dialog size */ private static final Dimension MAX_DIALOG_SIZE = new Dimension(480, 10000); /** Error dialog type */ public static final int ERROR_DIALOG_TYPE = 1; /** Information dialog type */ public static final int INFORMATION_DIALOG_TYPE = 2; /** Warning dialog type */ public static final int WARNING_DIALOG_TYPE = 3; /** Question dialog type */ public static final int QUESTION_DIALOG_TYPE = 4; /** * Brings up an error dialog with a generic localized title and message. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, */ public static void showErrorDialog(Component parentComponent) { showErrorDialog(parentComponent, null, null, null, null); } /** * Brings up an error dialog with the specified message and a generic localized title. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param message the error message to display in the dialog */ public static void showErrorDialog(Component parentComponent, String message) { showErrorDialog(parentComponent, null, message, null, null); } /** * Brings up an error dialog with the specified title and message. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param title the dialog's title, null for a generic localized title. * @param message the error message to display in the dialog, null for a generic localized message. */ public static void showErrorDialog(Component parentComponent, String title, String message) { showErrorDialog(parentComponent, title, message, null, null); } /** * Brings up an error dialog with the specified title, main and caption messages. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param title the dialog's title, null for a generic localized title. * @param message the error message to display in the dialog, null for a generic localized message. * @param captionMessage the caption message to display underneath the error message, null for none. */ public static void showErrorDialog(Component parentComponent, String title, String message, String captionMessage) { showErrorDialog(parentComponent, title, message, captionMessage, null); } /** * Brings up an error dialog with the specified title, main and caption messages, and stack trace of the specified * exception inside an expandable panel. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param title the dialog's title, null for a generic localized title. * @param message the error message to display in the dialog, null for a generic localized message. * @param captionMessage the caption message to display underneath the error message, null for none. * @param throwable exception for which to show the stack trace, null for none. */ public static void showErrorDialog(Component parentComponent, String title, String message, String captionMessage, Throwable throwable) { showDialog(ERROR_DIALOG_TYPE, parentComponent, title==null?Translator.get("error"):title, message==null?Translator.get("generic_error"):message, captionMessage, throwable); } /** * Brings up a warning dialog with the specified message and a generic localized title. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param message the main message to display in the dialog */ public static void showWarningDialog(Component parentComponent, String message) { showWarningDialog(parentComponent, null, message, null); } /** * Brings up a warning dialog with the specified title and message. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param title the dialog's title, null for a generic localized title. * @param message the main message to display in the dialog. */ public static void showWarningDialog(Component parentComponent, String title, String message) { showWarningDialog(parentComponent, title, message, null); } /** * Brings up a warning dialog with the specified title, main and caption messages. * * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param title the dialog's title, null for a generic localized title. * @param message the main message to display in the dialog, null for a generic localized message. * @param captionMessage the caption message to display underneath the main message, null for none. */ public static void showWarningDialog(Component parentComponent, String title, String message, String captionMessage) { showDialog(WARNING_DIALOG_TYPE, parentComponent, title==null?Translator.get("warning"):title, message, captionMessage, null); } /** * Brings up a dialog of the specified type and with the specified title, main and caption messages, and stack trace * of the specified exception inside an expandable panel. * * @param dialogType type of dialog, see constant fields for allow values. * @param parentComponent determines the Frame in which the dialog is displayed; if null, * or if the parentComponent has no Frame, a default Frame is used * @param title the dialog's title, null for a generic localized title, if one exists for the * dialog type. * @param message the main message to display in the dialog, null for a generic localized message, if * one exists for the dialog type. * @param captionMessage the caption message to display underneath the main message, null for none. * @param throwable exception for which to show the stack trace, null for none. */ public static void showDialog(int dialogType, Component parentComponent, String title, String message, String captionMessage, Throwable throwable) { Window owner = DialogToolkit.getWindowForComponent(parentComponent); final FocusDialog dialog; if(owner instanceof Frame) dialog = new FocusDialog((Frame)owner, title, parentComponent); else dialog = new FocusDialog((Dialog)owner, title, parentComponent); dialog.setMinimumSize(MIN_DIALOG_SIZE); dialog.setMaximumSize(MAX_DIALOG_SIZE); YBoxPanel mainPanel = new YBoxPanel(); InformationPane informationPane = new InformationPane(message, captionMessage, captionMessage==null?Font.PLAIN:Font.BOLD, getInformationPaneIconId(dialogType)); mainPanel.add(informationPane); mainPanel.addSpace(10); JButton okButton = new JButton(Translator.get("ok")); JPanel okPanel = DialogToolkit.createOKPanel(okButton, dialog.getRootPane(), e -> dialog.dispose()); JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); mainPanel.add(buttonPanel); // Show the exception's stack trace in an expandable/collapsible panel if(throwable !=null) { JTextArea detailsArea = new JTextArea(); detailsArea.setEditable(false); // Get the stack trace as a string StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw, true); throwable.printStackTrace(pw); pw.close(); // Fill the area with the stack trace. // Tabs by space characters to reduce the text's width detailsArea.setText(sw.toString().replace('\t', ' ')); FontUtils.makeMini(detailsArea); JScrollPane scrollPane = new JScrollPane(detailsArea, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); buttonPanel.add(new CollapseExpandButton(Translator.get("details"), scrollPane, false)); mainPanel.add(scrollPane); } buttonPanel.add(Box.createVerticalGlue()); buttonPanel.add(okPanel); dialog.getContentPane().add(mainPanel); // Give initial keyboard focus to the 'OK' button dialog.setInitialFocusComponent(okButton); // Call dispose() when dialog is closed dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.showDialog(); } /** * Returns an {@link InformationPane} icon id corresponding to the given dialog type. * * @param dialogType type of dialog, see constant fields for allow values. * @return an {@link InformationPane} icon id corresponding to the given dialog type. */ private static int getInformationPaneIconId(int dialogType) { int iconId; switch(dialogType) { case ERROR_DIALOG_TYPE: iconId = InformationPane.ERROR_ICON; break; case INFORMATION_DIALOG_TYPE: iconId = InformationPane.INFORMATION_ICON; break; case WARNING_DIALOG_TYPE: iconId = InformationPane.WARNING_ICON; break; case QUESTION_DIALOG_TYPE: iconId = InformationPane.QUESTION_ICON; break; default: iconId = InformationPane.ERROR_ICON; break; } return iconId; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/PasswordDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog; import com.mucommander.ui.button.ButtonChoicePanel; import com.mucommander.ui.layout.YBoxPanel; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * @author Oleg Trifonov * Created on 28/10/16. */ public class PasswordDialog extends FocusDialog implements ActionListener { private JPasswordField edtPassword; private JButton btnOk; private JButton btnCancel; private volatile Boolean canceled; /** Minimum dialog size */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(160, 100); /** Maximum dialog size */ private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(1024, 500); private PasswordDialog(Frame owner, String title, Component locationRelativeComp) { super(owner, title, locationRelativeComp); init(); } public PasswordDialog(String title) { this(null,title, null); } private void init() { // Sets minimum and maximum dimensions for this dialog setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); YBoxPanel mainPanel = new YBoxPanel(); edtPassword = new JPasswordField(10); mainPanel.add(new JLabel(i18n("password"))); mainPanel.addSpace(5); mainPanel.add(edtPassword); mainPanel.addSpace(10); btnOk = new JButton(i18n("ok")); btnOk.addActionListener(this); btnCancel = new JButton(i18n("cancel")); btnCancel.addActionListener(this); JButton[] buttons = new JButton[]{btnOk, btnCancel}; setInitialFocusComponent(edtPassword); mainPanel.add(new ButtonChoicePanel(buttons, 2, getRootPane())); getContentPane().add(mainPanel, BorderLayout.NORTH); requestFocus(); edtPassword.requestFocus(); pack(); fixHeight(); } public String getPassword() { SwingUtilities.invokeLater(this::showDialog); while (canceled == null) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } SwingUtilities.invokeLater(this::dispose); return canceled ? null : new String(edtPassword.getPassword()); } @Override public void cancel() { canceled = true; super.cancel(); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == btnOk) { canceled = false; dispose(); } else if (e.getSource() == btnCancel) { canceled = true; dispose(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/QuestionDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog; import com.mucommander.job.ui.DialogResult; import com.mucommander.ui.button.ButtonChoicePanel; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.layout.YBoxPanel; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * * * @author Maxence Bernard */ public class QuestionDialog extends FocusDialog implements ActionListener, DialogResult { /** Dialog owner */ private JButton[] buttons; private int[] actionValues; private int retValue = DIALOG_DISPOSED_ACTION; private YBoxPanel mainPanel; /** This value is returned by {@link #getActionValue()} when the dialog has been disposed without the user * selecting a custom action */ private final static int DIALOG_DISPOSED_ACTION = -1; /** Minimum dialog size */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(360, 0); /** Maximum dialog size */ private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(1024, 10000); /** * * @param actionValues values for actions, each of them must be >= 0 */ public QuestionDialog(Frame owner, String title, String msg, Component locationRelative, String[] actionText, int[] actionValues, int maxNbCols) { super(owner, title, locationRelative); init(new InformationPane(msg, null, Font.PLAIN, InformationPane.QUESTION_ICON), actionText, actionValues, maxNbCols); } /** * * @param actionValues values for actions, each of them must be >= 0 */ public QuestionDialog(Dialog owner, String title, String msg, Component locationRelative, String[] actionText, int[] actionValues, int maxNbCols) { super(owner, title, locationRelative); init(new InformationPane(msg, null, Font.PLAIN, InformationPane.QUESTION_ICON), actionText, actionValues, maxNbCols); } /** * * @param actionValues values for actions, each of them must be >= 0 */ public QuestionDialog(Frame owner, String title, Component msgComp, Component locationRelative, String[] actionText, int[] actionValues, int maxNbCols) { super(owner, title, locationRelative); init(msgComp, actionText, actionValues, maxNbCols); } /** * * @param actionValues values for actions, each of them must be >= 0 */ public QuestionDialog(Dialog owner, String title, Component msgComp, Component locationRelative, String[] actionText, int[] actionValues, int maxNbCols) { super(owner, title, locationRelative); init(msgComp, actionText, actionValues, maxNbCols); } protected QuestionDialog(Frame owner, String title, Component locationRelative) { super(owner, title, locationRelative); } protected QuestionDialog(Dialog owner, String title, Component locationRelative) { super(owner, title, locationRelative); } protected void init(Component comp, String[] actionText, int[] actionValues, int maxNbCols) { this.actionValues = actionValues; // Sets minimum and maximum dimensions for this dialog setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); mainPanel = new YBoxPanel(); if (comp != null) { mainPanel.addSpace(5); mainPanel.add(comp); mainPanel.addSpace(10); } int nbButtons = actionText.length; buttons = new JButton[nbButtons]; for (int i = 0; i < nbButtons; i++) { String text = actionText[i]; buttons[i] = new JButton(text); buttons[i].addActionListener(this); } setInitialFocusComponent(buttons[0]); mainPanel.add(new ButtonChoicePanel(buttons, maxNbCols, getRootPane())); getContentPane().add(mainPanel, BorderLayout.NORTH); } /** * Adds a component to this dialog, under the buttons panel. * * @param comp the component to add */ protected void addComponent(JComponent comp) { mainPanel.add(comp); } /** * Shows this dialog, waits for an action/button to be selected and returns the selected action's value. * The dialog may be closed without the user selecting a custom action. In this case, * {@link #DIALOG_DISPOSED_ACTION} (-1) will be returned. * @return action value */ public int getActionValue() { // // Beep ! // Toolkit.getDefaultToolkit().beep(); // Returns only when this dialog has been disposed // by actionPerformed or if window has been closed (-1) super.showDialog(); return retValue; } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); for (int i = 0; i < buttons.length; i++) if (buttons[i] == source) { retValue = actionValues[i]; break; } dispose(); } @Override public Object getUserInput() { super.showDialog(); return retValue; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/about/AboutDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.about; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.Locale; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyleContext; import javax.swing.text.StyledDocument; import com.mucommander.RuntimeConstants; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.GoToWebsiteAction; import com.mucommander.ui.action.impl.ShowAboutAction; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.FluentPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; /** * Dialog displaying information about muCommander. * @author Maxence Bernard, Nicolas Rinaudo */ public class AboutDialog extends FocusDialog implements ActionListener { /** Style for normal text. */ private static final String STYLE_NORMAL = "normal"; /** Style for headers. */ private static final String STYLE_HEADER = "header"; /** Style for URLs. */ private static final String STYLE_URL = "url"; /** Style for an item's details. */ private static final String STYLE_DETAILS = "details"; /** Style for a section title. */ private static final String STYLE_TITLE = "title"; /** Line break string. */ private static final String LINE_BREAK = System.lineSeparator(); /** Button that closes the dialog. */ private JButton btnOk; /** Button that opens trolCommander's homepage in a browser. */ private JButton btnHome; /** Button that opens the trolCommander's license. */ private JButton btnLicense; /** Panel in which all the textual information is displayed. */ private JScrollPane textPanel; /** * Creates a new AboutDialog. * @param mainFrame frame this dialog is relative to. */ public AboutDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(ShowAboutAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); // Initializes the dialog's content. Container contentPane = getContentPane(); contentPane.add(createIconPanel(), BorderLayout.WEST); contentPane.add(createCreditsPanel(), BorderLayout.EAST); setResizable(false); // Makes sure the scroll pane is properly initialized. SwingUtilities.invokeLater(() -> textPanel.getViewport().setViewPosition(new Point(0, 0))); pack(); // Makes OK the default action. setInitialFocusComponent(btnOk); getRootPane().setDefaultButton(btnOk); } /** * Creates the panel that contains all the about box's text. */ private JScrollPane createCreditsPanel() { JTextPane text = new JTextPane(); StyledDocument doc = text.getStyledDocument(); text.setBackground(ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR)); setStyles(doc); text.setEditable(false); try { // Team. insertTitle(doc, "The trolCommander team"); // Core developers. insertHeader(doc, "Developers and contributors"); insertNormalString(doc, "Maxence Bernard"); insertNormalString(doc, "Arik Hadas"); insertNormalString(doc, "Mariusz Jakubowski"); insertNormalString(doc, "Nicolas Rinaudo"); insertNormalString(doc, "Oleg Trifonov"); insertNormalString(doc, "Ondřej Zima"); insertNormalString(doc, "Martin Kortkamp"); // insertLineBreak(doc); // Contributors. // insertHeader(doc, "Contributors"); insertNormalString(doc, "Ivan Baidakov"); insertNormalString(doc, "Vassil Dichev"); insertNormalString(doc, "Karel Klic"); insertNormalString(doc, "David Kovar"); insertNormalString(doc, "Joshua Lebo"); insertNormalString(doc, "LeO"); insertNormalString(doc, "Xavier Martin"); insertNormalString(doc, "Alejandro Scandroli"); insertNormalString(doc, "Alexander Yerenkow"); insertNormalString(doc, "Johann Schmitz"); insertLineBreak(doc); // QA insertHeader(doc, "QA"); insertNormalString(doc, "Joshua Lebo"); insertLineBreak(doc); // Translators. insertHeader(doc, "Translators"); insertDetailedString(doc, "4X_Pro", "Russian"); insertDetailedString(doc, "Roberto Angeletti", "Italian"); insertDetailedString(doc, "Tamás Balogh-Walder", "Hungarian"); insertDetailedString(doc, "Mykola Bilovus", "Ukrainian"); insertDetailedString(doc, "ChArLoK_16", "Arabic"); insertDetailedString(doc, "György Varga", "Hungarian"); insertDetailedString(doc, "Frank Berger", "German"); insertDetailedString(doc, "Tony Klüver", "German"); insertDetailedString(doc, "Marcos Cobeña", "Spanish"); insertDetailedString(doc, "Cristiano Duarte", "Brazilian Portuguese"); insertDetailedString(doc, "Jakob Ekström", "Swedish"); insertDetailedString(doc, "Catalin Hritcu", "Romanian"); insertDetailedString(doc, "Kent Hsu", "Traditional Chinese"); insertDetailedString(doc, "Jioh L. Jung", "Korean"); insertDetailedString(doc, "Andrzej Kosiński", "Polish"); insertDetailedString(doc, "Joze Kovacic", "Slovenian"); insertDetailedString(doc, "Oleksandr Kovalchuk", "Ukrainian"); insertDetailedString(doc, "Pieter Kristensen", "Dutch"); insertDetailedString(doc, "Ján Ľudvík", "Slovak"); insertDetailedString(doc, "Jaromír Mára", "Czech"); insertDetailedString(doc, "Xavi Miró", "Spanish"); insertDetailedString(doc, "Evgeny Morozov", "Russian"); insertDetailedString(doc, "Jonathan Murphy", "British English"); insertDetailedString(doc, "Nardog", "Japanese"); insertDetailedString(doc, "Jordi Plantalech", "Catalan"); insertDetailedString(doc, "Alexey Sirotov", "Russian"); insertDetailedString(doc, "Jeppe Toustrup", "Danish"); insertDetailedString(doc, "Peter Vasko", "Czech"); insertDetailedString(doc, "vboo", "Belarusian"); insertDetailedString(doc, "whiteriver", "Simplified Chinese"); insertLineBreak(doc); // Special thanks. insertHeader(doc, "Special thanks"); insertDetailedString(doc, "Semyon Filippov", "muCommander icon"); insertDetailedString(doc, "Stefano Perelli", "Former muCommander icon"); insertLineBreak(doc); insertLineBreak(doc); // Powered by. insertTitle(doc, "Powered by"); // External Libraries. insertHeader(doc, "Libraries"); insertDetailedUrl(doc, "Apache Commons", "Apache License", "http://commons.apache.org"); insertDetailedUrl(doc, "Apache Hadoop", "Apache License", "http://hadoop.apache.org"); insertDetailedUrl(doc, "Furbelow", "LGPL", "http://sourceforge.net/projects/furbelow"); insertDetailedUrl(doc, "ICU4J", "ICU License", "http://www.icu-project.org"); insertDetailedUrl(doc, "J2SSH", "LGPL", "http://sourceforge.net/projects/sshtools"); insertDetailedUrl(doc, "J7Zip", "LGPL", "http://sourceforge.net/projects/p7zip/"); insertDetailedUrl(doc, "jCIFS", "LGPL", "http://jcifs.samba.org"); insertDetailedUrl(doc, "JetS3t", "Apache License", "http://jets3t.s3.amazonaws.com/index.html"); insertDetailedUrl(doc, "JmDNS", "LGPL", "http://jmdns.sourceforge.net"); insertDetailedUrl(doc, "JNA", "LGPL", "http://jna.dev.java.net"); insertDetailedUrl(doc, "JUnRar", "Freeware", "https://github.com/edmund-wagner/junrar"); insertDetailedUrl(doc, "Yanfs", "BSD", "http://yanfs.dev.java.net"); insertDetailedUrl(doc, "RSyntaxTextArea", "Modified BSD", "http://fifesoft.com/rsyntaxtextarea"); insertDetailedUrl(doc, "ICEpdf", "Apache License", "http://www.icesoft.org/java/home.jsf"); insertDetailedUrl(doc, "image4j", "LGPL", "http://image4j.sourceforge.net"); insertDetailedUrl(doc, "JediTerm", "LGPL", "https://github.com/JetBrains/jediterm"); insertDetailedUrl(doc, "JADB", "Apache License 2.0", "https://github.com/vidstige/jadb"); insertDetailedUrl(doc, "Mark James' icons", "Creative Commons Attribution License", "http://famfamfam.com"); insertLineBreak(doc); // External tools. insertHeader(doc, "Tools"); insertDetailedUrl(doc, "jdeb", "Apache Software License", "http://vafer.org/projects/jdeb/"); insertDetailedUrl(doc, "Launch4j", "GPL", "http://launch4j.sourceforge.net"); insertDetailedUrl(doc, "NSIS", "zlib/libpng license", "http://nsis.sourceforge.net"); insertDetailedUrl(doc, "p7zip", "LGPL", "http://p7zip.sourceforge.net"); insertLineBreak(doc); insertLineBreak(doc); // Version information insertTitle(doc, "Version information"); // VM information. insertHeader(doc, "trolCommander"); insertNormalString(doc, "Version: " + RuntimeConstants.VERSION); insertNormalString(doc, "Build date: " + getFormatedDate()); insertLineBreak(doc); // VM information. insertHeader(doc, "JVM"); insertNormalString(doc, "Runtime version: " + System.getProperty("java.version")); insertNormalString(doc, "VM name: " + System.getProperty("java.vm.name")); insertNormalString(doc, "VM version: " + System.getProperty("java.vm.version")); insertNormalString(doc, "VM vendor: " + System.getProperty("java.vm.vendor")); insertLineBreak(doc); // OS information. insertHeader(doc, "OS"); insertNormalString(doc, "Name: " + System.getProperty("os.name")); insertNormalString(doc, "Version: " + System.getProperty("os.version")); insertNormalString(doc, "Architecture: " + System.getProperty("os.arch")); insertLineBreak(doc); // Locale information. Locale locale = Locale.getDefault(); insertHeader(doc, "Locale"); insertNormalString(doc, "Language: " + locale.getLanguage()); insertNormalString(doc, "Country: " + locale.getCountry()); insertNormalString(doc, "Encoding: " + Charset.defaultCharset().displayName()); } catch(Exception ignore) {} textPanel = new JScrollPane(text, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); textPanel.getViewport().setPreferredSize(new Dimension((int)text.getPreferredSize().getWidth(), 350)); return textPanel; } /** * Creates the about box's left panel. */ private JPanel createIconPanel() { // Makes sure the panel's a bit roomier than the default configuration. return new FluentPanel(new BorderLayout()) { @Override public Insets getInsets() { return new Insets(10, 10, 0, 10); } }.add(new FluentPanel(new BorderLayout()) .add(new JLabel(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon128_24.png")), BorderLayout.NORTH) .add(new FluentPanel(new FlowLayout(FlowLayout.CENTER)).add(createAppString()), BorderLayout.CENTER) .add(new FluentPanel(new FlowLayout(FlowLayout.CENTER)).add(createCopyright()), BorderLayout.SOUTH), BorderLayout.NORTH) .add(new FluentPanel(new BorderLayout()) .add(createHomeComponent(), BorderLayout.NORTH) .add(createLicenseButton(), BorderLayout.CENTER) .add(createOkButton(), BorderLayout.SOUTH), BorderLayout.SOUTH); } private JLabel createCopyright() { return new JLabel("©" + RuntimeConstants.COPYRIGHT+ " Oleg Trifonov


    Based on muCommander
    "); } private JLabel createAppString() { return createBoldLabel(RuntimeConstants.APP_STRING); } private Component createHomeComponent() { return DesktopManager.canBrowse() ? createHomeButton() : new FluentPanel(new FlowLayout(FlowLayout.CENTER)).add(new JLabel(RuntimeConstants.HOMEPAGE_URL)); } private JButton createHomeButton() { btnHome = new JButton(ActionProperties.getActionLabel(GoToWebsiteAction.Descriptor.ACTION_ID)); btnHome.addActionListener(this); return btnHome; } private JButton createLicenseButton() { btnLicense = new JButton(i18n("license")); btnLicense.addActionListener(this); return btnLicense; } private JButton createOkButton() { btnOk = new JButton(i18n("ok")); btnOk.addActionListener(this); return btnOk; } /** * Creates different styles in the specified StyledDocument * @param doc document in which to create the styles. */ private static void setStyles(StyledDocument doc) { Style master = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE); Font font = ThemeManager.getCurrentFont(Theme.FILE_TABLE_FONT); // Normal style. master = doc.addStyle(STYLE_NORMAL, master); StyleConstants.setFontFamily(master, font.getFamily()); StyleConstants.setFontSize(master, font.getSize()); StyleConstants.setForeground(master, ThemeManager.getCurrentColor(Theme.FILE_FOREGROUND_COLOR)); StyleConstants.setBackground(master, ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR)); StyleConstants.setLeftIndent(master, 10); StyleConstants.setRightIndent(master, 10); StyleConstants.setLineSpacing(master, (float)0.2); doc.setParagraphAttributes(0, 0, master, true); // Header style. Style currentStyle = doc.addStyle(STYLE_HEADER, master); StyleConstants.setBold(currentStyle, true); StyleConstants.setFontSize(currentStyle, font.getSize() + 2); StyleConstants.setForeground(currentStyle, ThemeManager.getCurrentColor(Theme.FOLDER_FOREGROUND_COLOR)); StyleConstants.setBackground(currentStyle, ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR)); // Title style. currentStyle = doc.addStyle(STYLE_TITLE, currentStyle); StyleConstants.setAlignment(currentStyle, StyleConstants.ALIGN_CENTER); StyleConstants.setForeground(currentStyle, ThemeManager.getCurrentColor(Theme.ARCHIVE_FOREGROUND_COLOR)); StyleConstants.setBackground(currentStyle, ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR)); // Details style. currentStyle = doc.addStyle(STYLE_DETAILS, master); StyleConstants.setForeground(currentStyle, ThemeManager.getCurrentColor(Theme.HIDDEN_FILE_FOREGROUND_COLOR)); StyleConstants.setBackground(currentStyle, ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR)); // URL style. currentStyle = doc.addStyle(STYLE_URL, master); StyleConstants.setForeground(currentStyle, ThemeManager.getCurrentColor(Theme.SYMLINK_FOREGROUND_COLOR)); StyleConstants.setBackground(currentStyle, ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR)); StyleConstants.setUnderline(currentStyle, true); } /** * Inserts the specified header in the specified document. * @param doc document in which to insert the text. * @param string text to insert. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertHeader(StyledDocument doc, String string) throws BadLocationException { doc.insertString(doc.getLength(), string + LINE_BREAK, doc.getStyle(STYLE_HEADER)); } /** * Inserts the specified string in the specified document. * @param doc document in which to insert the text. * @param string text to insert. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertNormalString(StyledDocument doc, String string) throws BadLocationException { doc.insertString(doc.getLength(), string + LINE_BREAK, doc.getStyle(STYLE_NORMAL)); } /** * Inserts the specified string and details in the specified document. * @param doc document in which to insert the text. * @param string text to insert. * @param details details that will be added to the text. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertDetailedString(StyledDocument doc, String string, String details) throws BadLocationException { doc.insertString(doc.getLength(), string + " ", doc.getStyle(STYLE_NORMAL)); doc.insertString(doc.getLength(), "(" + details + ")" + LINE_BREAK, doc.getStyle(STYLE_DETAILS)); } /** * Inserts the specified URL in the specified document. * @param doc document in which to insert the text. * @param url url to insert. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertUrl(StyledDocument doc, String url) throws BadLocationException { doc.insertString(doc.getLength(), " ", doc.getStyle(STYLE_NORMAL)); doc.insertString(doc.getLength(), url + LINE_BREAK, doc.getStyle(STYLE_URL)); } /** * Inserts a line break in the specified document. * @param doc document in which to insert the text. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertLineBreak(StyledDocument doc) throws BadLocationException { doc.insertString(doc.getLength(), LINE_BREAK, doc.getStyle(STYLE_NORMAL)); } /** * Inserts the specified string, details and URL in the specified document. * @param doc document in which to insert the text. * @param string text to insert. * @param details details that will be added to the text. * @param url url that will be added to the text. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertDetailedUrl(StyledDocument doc, String string, String details, String url) throws BadLocationException { insertDetailedString(doc, string, details); insertUrl(doc, url); } /** * Inserts the specified title in the specified document. * @param doc document in which to insert the text. * @param string text to insert. * @throws BadLocationException thrown if something wrong happened to the document. */ private static void insertTitle(StyledDocument doc, String string) throws BadLocationException { int pos = doc.getLength(); Style style = doc.getStyle(STYLE_TITLE); string += LINE_BREAK; doc.insertString(pos, string, style); doc.setParagraphAttributes(pos, string.length(), style, true); doc.setParagraphAttributes(doc.getLength(), 0, doc.getStyle(STYLE_NORMAL), true); insertLineBreak(doc); } /** * Reacts to validations of the OK or home buttons. */ @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == btnOk) { dispose(); } else if(e.getSource() == btnHome) { try { DesktopManager.browse(new URI(RuntimeConstants.HOMEPAGE_URL).toURL()); } catch(IOException | URISyntaxException ignored) {} // Ignores errors here as there really isn't anything we can do. } else if(e.getSource() == btnLicense) { new LicenseDialog(this).showDialog(); } } /** * Returns a formatted version of trolCommander's build date. * @return a formatted version of trolCommander's build date. */ private String getFormatedDate() { return RuntimeConstants.BUILD_DATE.substring(0, 4) + '/' + RuntimeConstants.BUILD_DATE.substring(4, 6) + '/' + RuntimeConstants.BUILD_DATE.substring(6, 8); } /** * Creates a JLabel displaying the specified text using a bold font. * @param text text to display in the label. * @return a JLabel displaying the specified text using a bold font. */ private static JLabel createBoldLabel(String text) { JLabel label = new JLabel(text); Font font = label.getFont(); label.setFont(new Font(font.getFontName(), font.getStyle() | Font.BOLD, font.getSize())); return label; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/about/LicenseDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.about; import com.mucommander.RuntimeConstants; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.InputStreamReader; /** * Dialog used to display muCommander's license file. * @author Nicolas Rinaudo */ public class LicenseDialog extends FocusDialog implements ActionListener { private static final Logger LOGGER = LoggerFactory.getLogger(LicenseDialog.class); /** Button used to close the dialog. */ private JButton okButton; /** Panel in which to display the license. */ private JScrollPane licensePanel; /** * Creates a new license dialog centered on the specified window. * @param dialog window on which to center the new dialog. */ LicenseDialog(Dialog dialog) { super(dialog, i18n("license"), dialog); initUI(); } /** * Creates a new license dialog centered on the specified window. * @param frame window on which to center the new dialog. */ public LicenseDialog(Frame frame) { super(frame, i18n("license"), frame); initUI(); } /** * Creates the 'ok' button panel. * @return the 'ok' button panel. */ private JPanel createButtonPanel() { JPanel panel = new JPanel(); panel.setLayout(new FlowLayout(FlowLayout.RIGHT)); okButton = new JButton(i18n("ok")); okButton.addActionListener(this); panel.add(okButton); return panel; } /** * Creates the panel in which the license text is displayed. * @return the panel in which the license text is displayed. */ private JScrollPane createLicensePanel() { JTextArea license = new JTextArea(); license.setEditable(false); // Applies the file editor's theme to the license text. license.setForeground(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR)); license.setBackground(ThemeManager.getCurrentColor(Theme.EDITOR_BACKGROUND_COLOR)); license.setSelectedTextColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_FOREGROUND_COLOR)); license.setSelectionColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_BACKGROUND_COLOR)); license.setFont(ThemeManager.getCurrentFont(Theme.EDITOR_FONT)); license.setText(getLicenseText()); // Sets the scroll policy and preferred dimensions. licensePanel = new JScrollPane(license, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); licensePanel.getViewport().setPreferredSize(new Dimension((int)license.getPreferredSize().getWidth(), 400)); return licensePanel; } /** * Initializes the dialog's UI. */ private void initUI() { Container contentPane = getContentPane(); // Adds the UI components. contentPane.add(createLicensePanel(), BorderLayout.CENTER); contentPane.add(createButtonPanel(), BorderLayout.SOUTH); // Makes OK the default action. setInitialFocusComponent(okButton); getRootPane().setDefaultButton(okButton); // Makes sure the scroll pane is initializes on its first line. SwingUtilities.invokeLater(() -> licensePanel.getViewport().setViewPosition(new Point(0,0))); pack(); } // - IO code ---------------------------------------------------------------- // -------------------------------------------------------------------------- /** * Loads the license text. * @return the license text. */ private String getLicenseText() { StringBuilder text = new StringBuilder(); try (InputStreamReader in = new InputStreamReader(LicenseDialog.class.getResourceAsStream(RuntimeConstants.LICENSE))) { char[] buffer = new char[2048]; int count; while ((count = in.read(buffer)) != -1) { text.append(buffer, 0, count); } } catch(Exception e) { LOGGER.warn("Failed to read license file", e); } return text.toString(); } // - Listener code ---------------------------------------------------------- // -------------------------------------------------------------------------- /** * If the event originates from the OK button, closes the window. */ public void actionPerformed(ActionEvent e) { if (e.getSource() == okButton) { dispose(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/about/package.html ================================================ Contains components used to display the muCommander About dialog. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/auth/AuthDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.auth; import com.mucommander.auth.CredentialsManager; import com.mucommander.auth.CredentialsMapping; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.util.StringUtils; import com.mucommander.ui.combobox.EditableComboBox; import com.mucommander.ui.combobox.EditableComboBoxListener; import com.mucommander.ui.combobox.SaneComboBox; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.helper.FocusRequester; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * This dialog is used to ask the user for credentials (login/password) to access a particular location and offer him * to store them to disk. * *

    It uses CredentialsManager to retrieve and display a list of credentials matching the location so * they can quickly be recalled. * * @see CredentialsManager * @author Maxence Bernard */ public class AuthDialog extends FocusDialog implements ActionListener, EditableComboBoxListener { private final JButton btnOk; private final JButton btnCancel; private JRadioButton guestRadioButton; private JRadioButton userRadioButton; private final JTextField loginField; private EditableComboBox loginComboBox; private final JPasswordField passwordField; private final JCheckBox cbSaveCredentials; private CredentialsMapping selectedCredentialsMapping; private boolean guestCredentialsSelected; private final FileURL fileURL; private final CredentialsMapping[] credentialsMappings; // Dialog size constraints private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320,0); private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(480,10000); public AuthDialog(MainFrame mainFrame, FileURL fileURL, boolean authFailed, String errorMessage) { super(mainFrame.getJFrame(), i18n("auth_dialog.title"), mainFrame.getJFrame()); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); YBoxPanel yPanel = new YBoxPanel(); if (authFailed) { yPanel.add(new InformationPane(i18n("auth_dialog.authentication_failed"), errorMessage, errorMessage==null?Font.PLAIN:Font.BOLD, InformationPane.ERROR_ICON)); yPanel.addSpace(5); yPanel.add(new JSeparator()); } yPanel.addSpace(5); this.fileURL = fileURL; // Retrieve guest credentials (if any) Credentials guestCredentials = fileURL.getGuestCredentials(); // Fetch credentials from the specified FileURL (if any) and use them only if they're different from the guest ones Credentials urlCredentials = fileURL.getCredentials(); if (urlCredentials != null && urlCredentials.equals(guestCredentials)) { urlCredentials = null; } // Retrieve a list of credentials matching the URL from CredentialsManager credentialsMappings = CredentialsManager.getMatchingCredentials(fileURL); XAlignedComponentPanel compPanel = new XAlignedComponentPanel(10); // Connect as Guest/User radio buttons, displayed only if the URL has guest credentials if (guestCredentials != null) { addGuestCredentials(guestCredentials, compPanel); } else { // If not, display an introduction label ("please enter a login and password") yPanel.add(new JLabel(i18n("auth_dialog.desc"))); yPanel.addSpace(15); } // Server URL for which the user has to authenticate compPanel.addRow(i18n("auth_dialog.server"), new JLabel(fileURL.toString(false)), 10); // Login field: create either a text field or an editable combo box, depending on whether // CredentialsManager returned matches (-> combo box) or not (-> text field). int nbCredentials = credentialsMappings.length; JComponent loginComponent; if (nbCredentials > 0) { // Editable combo box loginComboBox = new EditableComboBox<>(); this.loginField = loginComboBox.getTextField(); // Add credentials to the combo box's choices for (CredentialsMapping credentialsMapping : credentialsMappings) { loginComboBox.addItem(credentialsMapping.getCredentials().getLogin()); } loginComboBox.addEditableComboBoxListener(this); loginComponent = loginComboBox; } else { // Simple text field loginField = new JTextField(); loginComponent = loginField; } compPanel.addRow(i18n("login"), loginComponent, 5); // create password field this.passwordField = new JPasswordField(); passwordField.addActionListener(this); compPanel.addRow(i18n("password"), passwordField, 10); // Contains the credentials to set in the login and password text fields Credentials selectedCredentials = null; // Whether the 'save credentials' checkbox should be enabled boolean saveCredentialsCheckBoxSelected = false; // If the provided URL contains credentials, use them if (urlCredentials != null) { selectedCredentials = urlCredentials; } else if (nbCredentials > 0) { // Else if CredentialsManager had matching credentials, use the best ones CredentialsMapping bestCredentialsMapping = credentialsMappings[0]; selectedCredentials = bestCredentialsMapping.getCredentials(); saveCredentialsCheckBoxSelected = bestCredentialsMapping.isPersistent(); } yPanel.add(compPanel); this.cbSaveCredentials = new JCheckBox(i18n("auth_dialog.store_credentials"), saveCredentialsCheckBoxSelected); yPanel.add(cbSaveCredentials); yPanel.addSpace(5); contentPane.add(yPanel, BorderLayout.CENTER); // If we have some existing credentials for this location... if (selectedCredentials != null) { // Pre-fill the login and password fields with the selected credentials loginField.setText(selectedCredentials.getLogin()); passwordField.setText(selectedCredentials.getPassword()); // Select the text fields' so their content can be erased just by typing the replacement string loginField.selectAll(); passwordField.selectAll(); // Select the 'Connect as User' radio button if there is one if (userRadioButton != null) { userRadioButton.setSelected(true); } } else { // Pre-fill the login field with the current user's name (ticket #185) loginField.setText(System.getProperty("user.name")); // Select the 'Connect as Guest' radio button if there is one if (guestRadioButton != null) { guestRadioButton.setSelected(true); loginField.setEnabled(false); passwordField.setEnabled(false); cbSaveCredentials.setEnabled(false); } } // Add OK/Cancel buttons this.btnOk = new JButton(i18n("ok")); this.btnCancel = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this), BorderLayout.SOUTH); // Set the component that will receive the initial focus setInitialFocusComponent(guestRadioButton == null ? loginField : guestRadioButton.isSelected() ? guestRadioButton:loginField); // Set minimum dimension setMinimumSize(MINIMUM_DIALOG_DIMENSION); // Set minimum dimension setMaximumSize(MAXIMUM_DIALOG_DIMENSION); } private void addGuestCredentials(Credentials guestCredentials, XAlignedComponentPanel compPanel) { guestRadioButton = new JRadioButton(StringUtils.capitalize(guestCredentials.getLogin())); guestRadioButton.addActionListener(this); compPanel.addRow(i18n("auth_dialog.connect_as"), guestRadioButton, 0); userRadioButton = new JRadioButton(i18n("user")); userRadioButton.addActionListener(this); compPanel.addRow("", userRadioButton, 15); ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(guestRadioButton); buttonGroup.add(userRadioButton); } /** * Returns the CredentialsMapping corresponding to the credentials selected by the user, either * entered in the login and password fields, or the guest credentials. * * @return the credentials entered by the user, null if the dialog was cancelled */ public CredentialsMapping getCredentialsMapping() { return selectedCredentialsMapping; } /** * Returns true if the user chose the guest credentials (radio button) in the dialog. * If true, {@link #getCredentialsMapping()} will return the guest credentials. * * @return true if the user chose the guest credentials (radio button) in the dialog */ public boolean guestCredentialsSelected() { return guestCredentialsSelected; } /** * Called when the dialog has been validated by the user, when the OK button has been pressed or when enter has * been pressed in a text field. */ private void setCredentialMapping() { if (guestRadioButton != null && guestRadioButton.isSelected()) { guestCredentialsSelected = true; selectedCredentialsMapping = new CredentialsMapping(fileURL.getGuestCredentials(), fileURL, false); } else { Credentials enteredCredentials = new Credentials(loginField.getText(), new String(passwordField.getPassword())); guestCredentialsSelected = false; boolean isPersistent = cbSaveCredentials.isSelected(); selectedCredentialsMapping = new CredentialsMapping(enteredCredentials, fileURL, isPersistent); // Look for an existing matching CredentialsMapping instance to re-use the realm which may contain // connection properties. for (CredentialsMapping cm : credentialsMappings) { if (cm.getCredentials().equals(enteredCredentials, true)) { // Comparison must be password-sensitive // create a new CredentialsMapping instance in case the 'isPersistent' flag has changed. // (original credentials may have originally been added as 'volatile' and then made persistent by // ticking the checkbox, or vice-versa) selectedCredentialsMapping = new CredentialsMapping(cm.getCredentials(), cm.getRealm(), isPersistent); break; } } } } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnOk || source == loginField || source == passwordField) { setCredentialMapping(); dispose(); } else if (source == btnCancel) { dispose(); } else if (source == guestRadioButton) { loginField.setEnabled(false); passwordField.setEnabled(false); cbSaveCredentials.setEnabled(false); } else if(source == userRadioButton) { loginField.setEnabled(true); passwordField.setEnabled(true); cbSaveCredentials.setEnabled(true); loginField.selectAll(); FocusRequester.requestFocus(loginField); } } @Override public void comboBoxSelectionChanged(SaneComboBox source) { CredentialsMapping selectedCredentialsMapping = credentialsMappings[loginComboBox.getSelectedIndex()]; Credentials selectedCredentials = selectedCredentialsMapping.getCredentials(); loginField.setText(selectedCredentials.getLogin()); passwordField.setText(selectedCredentials.getPassword()); // Enable/disable 'save credentials' checkbox depending on whether the selected credentials are persistent or not if (cbSaveCredentials != null) { cbSaveCredentials.setSelected(selectedCredentialsMapping.isPersistent()); } } @Override public void textFieldValidated(EditableComboBox source) { setCredentialMapping(); dispose(); } @Override public void textFieldCancelled(EditableComboBox source) { } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/auth/EditCredentialsDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.auth; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JPasswordField; import javax.swing.JTextField; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.auth.CredentialsManager; import com.mucommander.auth.CredentialsMapping; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.commons.file.Credentials; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EditCredentialsAction; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.list.DynamicList; import com.mucommander.ui.list.SortableListPanel; import com.mucommander.ui.main.MainFrame; /** * This dialog contains a list of all persistent credentials and allows the user to edit, remove, go to and reorder them. * *

    If the contents of this list is modified, credentials will be saved to disk when this dialog is disposed. * * @author Maxence Bernard */ public class EditCredentialsDialog extends FocusDialog implements ActionListener, ListSelectionListener { private static final Logger LOGGER = LoggerFactory.getLogger(EditCredentialsDialog.class); private final MainFrame mainFrame; private final JButton removeButton; private final JButton goToButton; private final JButton closeButton; private final JTextField loginField; private final JPasswordField passwordField; private final AlteredVector credentials; private final DynamicList credentialsList; private CredentialsMapping lastSelectedItem; // Dialog's size has to be at least 400x300 private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(440,330); // Dialog's size has to be at most 600x400 private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(600,400); public EditCredentialsDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(EditCredentialsAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); this.mainFrame = mainFrame; Container contentPane = getContentPane(); // Retrieve persistent credentials list this.credentials = CredentialsManager.getPersistentCredentialMappings(); // create the sortable credentials list panel SortableListPanel listPanel = new SortableListPanel<>(credentials); this.credentialsList = listPanel.dynamicList; this.lastSelectedItem = credentialsList.getSelectedValue(); contentPane.add(listPanel, BorderLayout.CENTER); // Text fields panel XAlignedComponentPanel compPanel = new XAlignedComponentPanel(); // Add login field this.loginField = new JTextField(); compPanel.addRow(i18n("login")+":", loginField, 5); // Add password field this.passwordField = new JPasswordField(); compPanel.addRow(i18n("password")+":", passwordField, 10); YBoxPanel yPanel = new YBoxPanel(10); yPanel.add(compPanel); XBoxPanel buttonsPanel = new XBoxPanel(); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // Remove button removeButton = new JButton(credentialsList.getRemoveAction()); removeButton.setMnemonic(mnemonicHelper.getMnemonic(removeButton)); buttonsPanel.add(removeButton); // Go to button goToButton = new JButton(i18n("go_to")); goToButton.setMnemonic(mnemonicHelper.getMnemonic(goToButton)); goToButton.addActionListener(this); buttonsPanel.add(goToButton); // Button that closes the window closeButton = new JButton(i18n("close")); closeButton.setMnemonic(mnemonicHelper.getMnemonic(closeButton)); closeButton.addActionListener(this); buttonsPanel.add(Box.createHorizontalGlue()); buttonsPanel.add(closeButton); yPanel.add(buttonsPanel); contentPane.add(yPanel, BorderLayout.SOUTH); // Set initial text components and buttons' enabled state updateComponents(); // Listen to selection changes to reflect the change credentialsList.addListSelectionListener(this); // table will receive initial focus setInitialFocusComponent(credentialsList); // Selects 'Done' button when enter is pressed getRootPane().setDefaultButton(closeButton); // Packs dialog setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); // Call dispose() on close and write credentials file setDefaultCloseOperation(DISPOSE_ON_CLOSE); showDialog(); } /** * Updates text fields and buttons' enabled state based on the current selection. Should be called * whenever the list selection has changed. */ private void updateComponents() { String loginValue = null; String passwordValue = null; boolean componentsEnabled = false; if (!credentialsList.isSelectionEmpty() && !credentials.isEmpty()) { componentsEnabled = true; CredentialsMapping credentialsMapping = credentialsList.getSelectedValue(); Credentials credentials = credentialsMapping.getCredentials(); loginValue = credentials.getLogin(); passwordValue = credentials.getPassword(); } loginField.setText(loginValue); loginField.setEnabled(componentsEnabled); passwordField.setText(passwordValue); passwordField.setEnabled(componentsEnabled); removeButton.setEnabled(componentsEnabled); } /** * Updates the value of the item that was being editing. Should be called whenever the list selection has changed. */ private void modifyCredentials() { // Make sure that the item still exists (could have been removed) before trying to modify its value int itemIndex = credentials.indexOf(lastSelectedItem); if (lastSelectedItem != null && itemIndex >= 0) { credentials.setElementAt(new CredentialsMapping(new Credentials(loginField.getText(), new String(passwordField.getPassword())), lastSelectedItem.getRealm(), true), itemIndex); } this.lastSelectedItem = credentialsList.getSelectedValue(); } /** * Overrides dispose() to write credentials if needed (if at least one item has been changed). */ @Override public void dispose() { super.dispose(); // Write credentials file to disk, only if changes were made try { CredentialsManager.writeCredentials(false); } catch (Exception e) { e.printStackTrace(); // We should probably pop an error dialog here... } } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // Dispose the dialog (credentials save is performed in dispose()) if (source == closeButton) { // Commit current credentials modifications. // Note: if the dialog is cancelled, current modifications will be cancelled (i.e. not committed) modifyCredentials(); dispose(); } else if (source == goToButton) { // Dispose dialog first dispose(); // Go to credentials' realm location mainFrame.getActivePanel().tryChangeCurrentFolder((credentialsList.getSelectedValue()).getRealm()); } } /////////////////////////////////// // ListSelectionListener methods // /////////////////////////////////// public void valueChanged(ListSelectionEvent e) { LOGGER.trace("called, e.getValueIsAdjusting="+e.getValueIsAdjusting()+" getSelectedIndex="+ credentialsList.getSelectedIndex()); if (e.getValueIsAdjusting()) { return; } // Commit current credentials modifications modifyCredentials(); // Update components to reflect the new selection updateComponents(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/auth/package.html ================================================ Components for creating and editing sets of credentials. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/bookmark/AddBookmarkDialog.kt ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.bookmark import com.mucommander.bookmark.Bookmark import com.mucommander.bookmark.BookmarkManager import com.mucommander.ui.action.ActionProperties import com.mucommander.ui.action.impl.AddBookmarkAction import com.mucommander.ui.dialog.DialogToolkit import com.mucommander.ui.dialog.FocusDialog import com.mucommander.ui.layout.XAlignedComponentPanel import com.mucommander.ui.layout.YBoxPanel import com.mucommander.ui.main.MainFrame import java.awt.BorderLayout import java.awt.Dimension import java.awt.event.ActionEvent import java.awt.event.ActionListener import javax.swing.JButton import javax.swing.JTextField import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener /** * This dialog allows the user to add a bookmark and enter a name for it. User can also * choose to store login and password information in the bookmark's URL if the bookmark * contains login/password information. * * @author Maxence Bernard */ class AddBookmarkDialog(mainFrame: MainFrame) : FocusDialog( mainFrame.jFrame, ActionProperties.getActionLabel(AddBookmarkAction.Descriptor.ACTION_ID), mainFrame.jFrame ), ActionListener, DocumentListener { private val edtName: JTextField private val edtLocation: JTextField private val cbParent: BookmarkParentComboBox private val addButton: JButton private val cancelButton: JButton init { val currentFolder = mainFrame.activePanel.currentFolder // Text fields panel val compPanel = XAlignedComponentPanel().apply { edtName = JTextField(currentFolder.getName()).apply { isEditable = true document.addDocumentListener(this@AddBookmarkDialog) // Monitors text changes to disable 'Add' button if name field is empty } addRow(i18n("name") + ":", edtName, 10) edtLocation = JTextField(currentFolder.canonicalPath) addRow(i18n("location") + ":", edtLocation, 10) cbParent = BookmarkParentComboBox() addRow(i18n("parent") + ":", cbParent, 10) } val mainPanel = YBoxPanel(5).apply { add(compPanel) } contentPane.add(mainPanel, BorderLayout.NORTH) addButton = JButton(i18n("add_bookmark_dialog.add")) cancelButton = JButton(i18n("cancel")) contentPane.add( DialogToolkit.createOKCancelPanel(addButton, cancelButton, getRootPane(), this), BorderLayout.SOUTH ) // Select text in name field and transfer focus to it for immediate user change edtName.selectAll() setInitialFocusComponent(edtName) // Packs dialog minimumSize = MINIMUM_DIALOG_DIMENSION maximumSize = MAXIMUM_DIALOG_DIMENSION showDialog() } /** * Checks if bookmark name is empty (or white space), and enable/disable 'Add' button * accordingly, in order to prevent user from adding a bookmark with an empty name. */ private fun checkEmptyName() { if (edtName.getText().isBlank()) { if (addButton.isEnabled) { addButton.setEnabled(false) } } else { if (!addButton.isEnabled) { addButton.setEnabled(true) } } } override fun actionPerformed(e: ActionEvent) { val source = e.getSource() if (source === addButton) { // Starts by disposing the dialog dispose() // Add bookmark and write bookmarks file to disk BookmarkManager.addBookmark( Bookmark( edtName.getText(), edtLocation.getText(), cbParent.getSelectedParent() ) ) try { BookmarkManager.writeBookmarks(false) } catch (_: Exception) { // TODO We should probably pop an error dialog here. } } else if (source === cancelButton) { dispose() } } override fun changedUpdate(e: DocumentEvent) = checkEmptyName() override fun insertUpdate(e: DocumentEvent) = checkEmptyName() override fun removeUpdate(e: DocumentEvent) = checkEmptyName() companion object { // Dialog's width has to be at least 320 private val MINIMUM_DIALOG_DIMENSION = Dimension(320, 0) // Dialog's width has to be at most 400 private val MAXIMUM_DIALOG_DIMENSION = Dimension(400, 10000) } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/bookmark/BookmarkParentComboBox.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.bookmark; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.utils.text.Translator; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.util.List; public class BookmarkParentComboBox extends TcComboBox implements PopupMenuListener { private String childName; BookmarkParentComboBox() { super(); addPopupMenuListener(this); } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { removeAllItems(); List parents = BookmarkManager.getParentBookmarks(); addItem("<" + Translator.get("root") + ">"); for (Bookmark bookmark : parents) { if (bookmark.getName().equals(BookmarkManager.BOOKMARKS_SEPARATOR)) { continue; } if (childName == null || !childName.equals(bookmark.getName())) { addItem(bookmark.getName()); } } } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { } @Override public void popupMenuCanceled(PopupMenuEvent e) { } public String getChildName() { return childName; } public void setChildName(String childName) { this.childName = childName; } public String getSelectedParent() { if (getSelectedIndex() == 0) { return null; } return getSelectedItem() == null ? null : getSelectedItem().toString(); } public void setSelectedParent(String parent) { getModel().setSelectedItem(parent); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/bookmark/EditBookmarksDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.bookmark; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EditBookmarksAction; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.list.DynamicList; import com.mucommander.ui.list.SortableListPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.text.Document; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * This dialog contains a list of all bookmarks and allows the user to edit, remove, duplicate, go to and reorder them. * *

    If the contents of this list is modified, bookmarks will be saved to disk when this dialog is disposed. * * @author Maxence Bernard */ public class EditBookmarksDialog extends FocusDialog implements ActionListener, ListSelectionListener, DocumentListener { private final MainFrame mainFrame; private final JButton btnNew; private final JButton btnDuplicate; private final JButton btnRemove; private final JButton btnGoto; private final JButton btnClose; private final JTextField edtName; private final JLabel locationLabel; private final JTextField edtLocation; private final BookmarkParentComboBox cbParent; // separatorNoticePrefix is required to keep the size of the 1st column private final JLabel separatorNoticePrefix; private final JLabel separatorNoticeLabel; private final AlteredVector bookmarks; private final DynamicList bookmarkList; private int currentListIndex; private Bookmark currentBookmarkSave; private boolean ignoreDocumentListenerEvents; // Dialog's size has to be at least 400x300 private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(440,330); // Dialog's size has to be at most 600x400 private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(600,400); public EditBookmarksDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(EditBookmarksAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); this.mainFrame = mainFrame; Container contentPane = getContentPane(); // Retrieve bookmarks list this.bookmarks = BookmarkManager.getBookmarks(); // Temporarily suspend bookmark change events, otherwise an event would be fired for each character // typed in the name / location fields. Events will be resumed when this dialog is disposed BookmarkManager.setFireEvents(false); // create the sortable bookmarks list panel SortableListPanel listPanel = new SortableListPanel<>(bookmarks); this.bookmarkList = listPanel.dynamicList; contentPane.add(listPanel, BorderLayout.CENTER); // Text fields panel XAlignedComponentPanel compPanel = new XAlignedComponentPanel(); // Add bookmark name field this.edtName = new JTextField(); edtName.getDocument().addDocumentListener(this); compPanel.addRow(i18n("name")+":", edtName, 5); // create a path field with auto-completion capabilities this.edtLocation = new FilePathField(); this.locationLabel = new JLabel(i18n("location")+":"); edtLocation.getDocument().addDocumentListener(this); compPanel.addRow(locationLabel, edtLocation, 10); this.cbParent = new BookmarkParentComboBox(); cbParent.addActionListener(this); compPanel.addRow(i18n("parent")+":", cbParent, 5); this.separatorNoticePrefix = new JLabel(); this.separatorNoticeLabel = new JLabel(" "+i18n("edit_bookmarks_dialog.is_separator")); compPanel.addRow(separatorNoticePrefix, separatorNoticeLabel, 10); YBoxPanel yPanel = new YBoxPanel(10); yPanel.add(compPanel); // Add buttons: 'remove', 'move up' and 'move down' buttons are enabled // only if there is at least one bookmark in the table XBoxPanel buttonsPanel = new XBoxPanel(); JPanel buttonGroupPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // New bookmark button btnNew = new JButton(i18n("edit_bookmarks_dialog.new")); btnNew.setMnemonic(mnemonicHelper.getMnemonic(btnNew)); btnNew.addActionListener(this); buttonGroupPanel.add(btnNew); // Duplicate bookmark button btnDuplicate = new JButton(i18n("duplicate")); btnDuplicate.setMnemonic(mnemonicHelper.getMnemonic(btnDuplicate)); btnDuplicate.addActionListener(this); buttonGroupPanel.add(btnDuplicate); // Remove bookmark button btnRemove = new JButton(bookmarkList.getRemoveAction()); btnRemove.setMnemonic(mnemonicHelper.getMnemonic(btnRemove)); buttonGroupPanel.add(btnRemove); // Go to bookmark button btnGoto = new JButton(i18n("go_to")); btnGoto.setMnemonic(mnemonicHelper.getMnemonic(btnGoto)); btnGoto.addActionListener(this); buttonGroupPanel.add(btnGoto); buttonsPanel.add(buttonGroupPanel); // Button that closes the window btnClose = new JButton(i18n("close")); btnClose.setMnemonic(mnemonicHelper.getMnemonic(btnClose)); btnClose.addActionListener(this); buttonsPanel.add(Box.createHorizontalGlue()); buttonsPanel.add(btnClose); yPanel.add(buttonsPanel); contentPane.add(yPanel, BorderLayout.SOUTH); // Set initial text components and buttons' enabled state updateComponents(); // Listen to selection changes to reflect the change bookmarkList.addListSelectionListener(this); // table will receive initial focus setInitialFocusComponent(bookmarkList); // Selects OK when enter is pressed getRootPane().setDefaultButton(btnClose); // Packs dialog setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); // Call dispose() on close and write bookmarks file setDefaultCloseOperation(DISPOSE_ON_CLOSE); showDialog(); } /** * Updates text fields and buttons' enabled state based on the current selection. Should be called * whenever the list selection has changed. */ private void updateComponents() { String nameValue = null; String locationValue = null; String parentValue = null; boolean componentsEnabled = false; if (!bookmarkList.isSelectionEmpty() && !bookmarks.isEmpty()) { componentsEnabled = true; Bookmark b = bookmarkList.getSelectedValue(); nameValue = b.getName(); locationValue = b.getLocation(); parentValue = b.getParent(); } // Ignore text field events while setting values ignoreDocumentListenerEvents = true; edtName.setText(nameValue); edtName.setEnabled(componentsEnabled); edtLocation.setText(locationValue); edtLocation.setEnabled(componentsEnabled); cbParent.setChildName(nameValue); cbParent.setSelectedParent(parentValue); ignoreDocumentListenerEvents = false; btnGoto.setEnabled(componentsEnabled && locationValue != null && !locationValue.isEmpty()); btnDuplicate.setEnabled(componentsEnabled); btnRemove.setEnabled(componentsEnabled); updateSeparatorNoticeVisibility(); } /** * Updates visibility of `The specified name defines a separator` notice */ private void updateSeparatorNoticeVisibility() { String nameFieldValue = edtName.getText(); boolean isSeparator = nameFieldValue != null && nameFieldValue.equals(BookmarkManager.BOOKMARKS_SEPARATOR); if (separatorNoticeLabel.isVisible() == isSeparator) { return; } separatorNoticePrefix.setVisible(isSeparator); separatorNoticeLabel.setVisible(isSeparator); if (isSeparator) { // inherit the preferred sizes of locationXXXX controls, // to keep the layout still separatorNoticePrefix.setPreferredSize(locationLabel.getPreferredSize()); separatorNoticeLabel.setPreferredSize(edtLocation.getPreferredSize()); } locationLabel.setVisible(!isSeparator); edtLocation.setVisible(!isSeparator); } /** * Called whenever a value in one of the text fields has been modified, and updates the current Bookmark instance to * use the new value. * * @param sourceDocument the javax.swing.text.Document of the JTextField that was modified */ private void modifyBookmark(Document sourceDocument) { if (ignoreDocumentListenerEvents || bookmarks.isEmpty()) { return; } int selectedIndex = bookmarkList.getSelectedIndex(); // Make sure that the selected index is not out of bounds if (!bookmarkList.isIndexValid(selectedIndex)) { return; } Bookmark selectedBookmark = bookmarks.elementAt(selectedIndex); if (currentBookmarkSave == null) { // create a clone of the current bookmark in order to cancel any modifications made to it if the dialog // is cancelled. try { currentBookmarkSave = (Bookmark)selectedBookmark.clone(); } catch(CloneNotSupportedException ignored) {} this.currentListIndex = selectedIndex; } // Update name if (sourceDocument == edtName.getDocument()) { String name = edtName.getText(); if (name.trim().isEmpty()) { name = getFreeNameVariation(i18n("untitled")); } selectedBookmark.setName(name); bookmarkList.itemModified(selectedIndex, false); updateSeparatorNoticeVisibility(); } // Update location else if (sourceDocument == edtLocation.getDocument()) { String location = edtLocation.getText(); selectedBookmark.setLocation(location); bookmarkList.itemModified(selectedIndex, false); btnGoto.setEnabled(location != null && !location.isEmpty()); } else { selectedBookmark.setParent(cbParent.getSelectedParent()); bookmarkList.itemModified(selectedIndex, false); } } /** * Returns the first variation of the given name that is not already used by another bookmark, e.g. : *
    "music" -> "music (2)" if there already is bookmark with the "music" name *
    "music (2)" -> "music (3)" and so on... */ private String getFreeNameVariation(String name) { if (!containsName(name)) { return name; } int len = name.length(); char c; int num = 2; if (len > 4 && name.charAt(len - 1) == ')' && (c = name.charAt(len - 2)) >= '0' && c <= '9' && name.charAt(len - 3) == '(' && name.charAt(len - 4) == ' ') { num = (c - '0') + 1; name = name.substring(0, len - 4); } String newName; while (containsName(newName = (name + " (" + num++ + ")"))) ; return newName; } /** * Returns true if the bookmarks list contains a bookmark that has the specified name. */ private boolean containsName(String name) { int nbBookmarks = bookmarks.size(); for (int i = 0; i < nbBookmarks; i++) { if (bookmarks.elementAt(i).getName().equals(name)) { return true; } } return false; } /** * Overrides dispose() to write bookmarks to disk (if needed). */ @Override public void dispose() { super.dispose(); // Rollback current bookmark's modifications if the dialog was cancelled if (currentBookmarkSave!=null) { bookmarks.setElementAt(currentBookmarkSave, currentListIndex); currentBookmarkSave = null; } // Resume bookmark change events BookmarkManager.setFireEvents(true); // Write bookmarks file to disk, only if changes were made to bookmarks try { BookmarkManager.writeBookmarks(false); } catch(Exception e) { // We should probably pop an error here. e.printStackTrace(); } } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // Dispose the dialog (bookmarks save is performed in dispose()) if (source == btnClose) { // Do not rollback current bookmark's modifications on dispose() currentBookmarkSave = null; dispose(); } else if (source == btnNew || source == btnDuplicate) { addBookmark(source == btnDuplicate); } else if (source== btnGoto) { // Dispose dialog first dispose(); // Change active panel's folder mainFrame.getActivePanel().tryChangeCurrentFolder((bookmarkList.getSelectedValue()).getLocation()); } else if (source == cbParent) { modifyBookmark(null); } } private void addBookmark(boolean duplicate) { Bookmark newBookmark; if (duplicate) { try { Bookmark currentBookmark = bookmarkList.getSelectedValue(); newBookmark = (Bookmark)currentBookmark.clone(); newBookmark.setName(getFreeNameVariation(currentBookmark.getName())); } catch (CloneNotSupportedException ex) { return; } } else { newBookmark = new Bookmark(getFreeNameVariation(i18n("untitled")), "", null); } bookmarks.add(newBookmark); int newBookmarkIndex = bookmarks.size()-1; bookmarkList.selectAndScroll(newBookmarkIndex); updateComponents(); edtName.selectAll(); edtName.requestFocus(); } @Override public void valueChanged(ListSelectionEvent e) { if (e.getValueIsAdjusting()) { return; } // Reset current bookmark's save currentBookmarkSave = null; // currentListIndex = bookmarkList.getSelectedIndex(); // Update components to reflect the new selection updateComponents(); } @Override public void changedUpdate(DocumentEvent e) { modifyBookmark(e.getDocument()); } @Override public void insertUpdate(DocumentEvent e) { modifyBookmark(e.getDocument()); } public void removeUpdate(DocumentEvent e) { modifyBookmark(e.getDocument()); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/bookmark/package.html ================================================ Components for creating and editing bookmarks. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/commands/CommandsPanel.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.commands import com.jidesoft.hints.FileIntelliHints import com.mucommander.command.Command import com.mucommander.command.CommandManager import com.mucommander.command.CommandType import com.mucommander.commons.collections.AlteredVector import com.mucommander.ui.helper.MnemonicHelper import com.mucommander.ui.layout.XAlignedComponentPanel import com.mucommander.ui.layout.XBoxPanel import com.mucommander.ui.layout.YBoxPanel import com.mucommander.ui.list.DynamicList import com.mucommander.ui.list.SortableListPanel import com.mucommander.ui.text.FilePathField import com.mucommander.utils.text.Translator import java.awt.BorderLayout import java.awt.FlowLayout import java.awt.event.ActionEvent import java.awt.event.ActionListener import javax.swing.JButton import javax.swing.JPanel import javax.swing.JTextField import javax.swing.event.DocumentEvent import javax.swing.event.DocumentListener import javax.swing.event.ListSelectionEvent import javax.swing.event.ListSelectionListener /** * @author Oleg Trifonov * Created on 10/10/14. */ class CommandsPanel(private val parent: EditCommandsDialog, private val commandType: String?) : JPanel(), ActionListener, DocumentListener, ListSelectionListener { private val commands: AlteredVector private val commandsList: DynamicList private val btnNew: JButton private val btnDuplicate: JButton private val btnRemove: JButton private val edtAlias: JTextField private val edtCommand: JTextField private val edtDisplay: JTextField private val edtFilemask: JTextField private var ignoreDocumentListenerEvents = false private inner class CommandWrapper(val command: Command) { override fun toString(): String { var result: String = if (commandType == null) { command.alias + ":\t" } else { "" } command.displayName?.let { result += it + "\t" } command.fileMask?.let { result += "[" + command.fileMask + "]\t" } result += command.command return result } } init { setLayout(BorderLayout()) val mnemonicHelper = MnemonicHelper() val list = mutableListOf().apply { for (cmd in CommandManager.getCommands(commandType)) { add(CommandWrapper(cmd)) } } commands = AlteredVector(list) // create the sortable commands list panel val listPanel = SortableListPanel(commands) commandsList = listPanel.dynamicList.apply { addListSelectionListener(this@CommandsPanel) } add(listPanel, BorderLayout.CENTER) ignoreDocumentListenerEvents = true // Add alias field edtAlias = JTextField().apply { document.addDocumentListener(this@CommandsPanel) } // Text fields panel val compPanel = XAlignedComponentPanel().apply { addRow(Translator.get("EditCommands.alias") + ":", edtAlias, 5) if (commandType != null) { edtAlias.text = commandType edtAlias.setEnabled(false) } edtCommand = FilePathField().apply { document.addDocumentListener(this@CommandsPanel) } addRow(Translator.get("EditCommands.command") + ":", edtCommand, 10) edtDisplay = FilePathField().apply { document.addDocumentListener(this@CommandsPanel) } addRow(Translator.get("EditCommands.display_name") + ":", edtDisplay, 10) edtFilemask = FilePathField().apply { document.addDocumentListener(this@CommandsPanel) } addRow(Translator.get("EditCommands.filemask") + ":", edtFilemask, 10) } FileIntelliHints(edtCommand) val yPanel = YBoxPanel(10).apply { add(compPanel) } val buttonGroupPanel = JPanel(FlowLayout(FlowLayout.LEFT)).apply { btnNew = JButton(Translator.get("EditCommands.new")).also { it.setMnemonic(mnemonicHelper.getMnemonic(it)) it.addActionListener(this@CommandsPanel) add(it) } btnDuplicate = JButton(Translator.get("duplicate")).also { it.setMnemonic(mnemonicHelper.getMnemonic(it)) it.addActionListener(this@CommandsPanel) add(it) } btnRemove = JButton(commandsList.removeAction).also { it.setMnemonic(mnemonicHelper.getMnemonic(it)) it.addActionListener(this@CommandsPanel) add(it) } } // Add buttons: 'remove', 'move up' and 'move down' buttons are enabled only if there is at least one command in the table val buttonsPanel = XBoxPanel().apply { add(buttonGroupPanel) } yPanel.add(buttonsPanel) add(yPanel, BorderLayout.SOUTH) ignoreDocumentListenerEvents = false updateComponents() } /** * Updates text fields and buttons' enabled state based on the current selection. Should be called * whenever the list selection has changed. */ private fun updateComponents() { var alias: String? = null var value: String? = null var display: String? = null var filemask: String? = null var componentsEnabled = false if (!commandsList.isSelectionEmpty && !commands.isEmpty()) { componentsEnabled = true val cmd = commandsList.getSelectedValue()!!.command alias = cmd.alias value = cmd.command display = cmd.displayName filemask = cmd.fileMask } ignoreDocumentListenerEvents = true if (commandType == null) { edtAlias.text = alias } edtCommand.text = value edtDisplay.text = display edtFilemask.text = filemask ignoreDocumentListenerEvents = false btnDuplicate.setEnabled(componentsEnabled) btnRemove.setEnabled(componentsEnabled) } override fun actionPerformed(e: ActionEvent) { val source = e.getSource() // create a new empty command / duplicate the currently selected command if (source === btnNew || source === btnDuplicate) { parent.enableSave() val newCommand: Command? if (source === btnNew) { val alias = commandType ?: "" newCommand = Command(alias, $$"$f") } else { // source == btnDuplicate val currentCommand = commandsList.getSelectedValue()!!.command newCommand = Command(currentCommand) } commands.add(CommandWrapper(newCommand)) val newCommandIndex = commands.size - 1 commandsList.selectAndScroll(newCommandIndex) updateComponents() edtAlias.selectAll() edtAlias.requestFocus() } else if (source === btnRemove) { parent.enableSave() } } /** * Called whenever a value in one of the text fields has been modified, and updates the current Command instance to * use the new value. */ private fun modifyCommand() { if (ignoreDocumentListenerEvents || commands.isEmpty()) { return } val selectedIndex = commandsList.selectedIndex // Make sure that the selected index is not out of bounds if (!commandsList.isIndexValid(selectedIndex)) return val selectedCommand = Command( edtAlias.getText(), edtCommand.getText(), CommandType.NORMAL_COMMAND, edtDisplay.getText(), edtFilemask.getText() ) commands.setElementAt(CommandWrapper(selectedCommand), selectedIndex) parent.enableSave() } override fun insertUpdate(e: DocumentEvent?) = modifyCommand() override fun removeUpdate(e: DocumentEvent?) = modifyCommand() override fun changedUpdate(e: DocumentEvent?) = modifyCommand() override fun valueChanged(e: ListSelectionEvent) { if (!e.valueIsAdjusting) { updateComponents() } } fun getCommands(): List = mutableListOf().apply { for (cw in commands) { add(cw.command) } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/commands/EditCommandsDialog.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.commands import com.mucommander.command.CommandException import com.mucommander.command.CommandManager import com.mucommander.ui.dialog.FocusDialog import com.mucommander.ui.layout.XBoxPanel import org.slf4j.LoggerFactory import java.awt.BorderLayout import java.awt.Component import java.awt.FlowLayout import java.awt.Frame import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.io.IOException import javax.swing.JButton import javax.swing.JPanel import javax.swing.JTabbedPane /** * @author Oleg Trifonov * Created on 10/10/14. */ class EditCommandsDialog( owner: Frame, locationRelativeComp: Component ) : FocusDialog(owner, i18n("EditCommands.label"), locationRelativeComp), ActionListener { private val log = LoggerFactory.getLogger(EditCommandsDialog::class.java) private val btnApply: JButton private val btnOk: JButton private val btnCancel: JButton private val panels = mutableListOf() init { // Initializes the tabbed pane. val tabbedPane = JTabbedPane(JTabbedPane.TOP) CommandsPanel(this, CommandManager.VIEWER_ALIAS).also { tabbedPane.addTab(i18n("EditCommands.group.view"), it) panels.add(it) } CommandsPanel(this, CommandManager.EDITOR_ALIAS).also { tabbedPane.addTab(i18n("EditCommands.group.edit"), it) panels.add(it) } CommandsPanel(this, null).also { tabbedPane.addTab(i18n("EditCommands.group.others"), it) panels.add(it) } // Adds the tabbed pane val contentPane = contentPane.apply { setLayout(BorderLayout()) add(tabbedPane, BorderLayout.CENTER) } // Buttons panel. val buttonsPanel = XBoxPanel().apply { btnApply = JButton(i18n("apply")).apply { setEnabled(false) addActionListener(this@EditCommandsDialog) } add(btnApply) addSpace(20) btnOk = JButton(i18n("ok")).apply { setEnabled(false) addActionListener(this@EditCommandsDialog) } add(btnOk) btnCancel = JButton(i18n("cancel")).apply { addActionListener(this@EditCommandsDialog) } add(btnCancel) } // Aligns the button panel to the right. val tempPanel = JPanel(FlowLayout(FlowLayout.RIGHT)).apply { add(buttonsPanel) } contentPane.add(tempPanel, BorderLayout.SOUTH) getRootPane().setDefaultButton(btnOk) } override fun actionPerformed(e: ActionEvent) { val source = e.getSource() if (source === btnOk || source === btnApply) { commit() } if (source === btnOk || source === btnCancel) { dispose() } } fun enableSave() { btnApply.setEnabled(true) btnOk.setEnabled(true) } private fun commit() { // clear all Normal commands CommandManager.removeAllNormalCommands() // register all commands from editor panels.forEach { for (cmd in it.getCommands()) { CommandManager.registerCommand(cmd) } } // write file try { CommandManager.writeCommands() } catch (e: IOException) { log.error("Commands write error", e) } catch (e: CommandException) { log.error("Commands error", e) } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/customization/CommandBarDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.customization; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.CustomizeCommandBarAction; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.list.DynamicHorizontalWrapList; import com.mucommander.ui.list.DynamicList; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.commandbar.CommandBarAttributes; import com.mucommander.ui.main.commandbar.CommandBarButtonForDisplay; import com.mucommander.ui.main.commandbar.CommandBarIO; import com.mucommander.ui.text.RecordingKeyStrokeTextField; import javax.swing.*; import javax.swing.Box.Filler; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.*; import java.util.List; /** * Dialog used to customize the command-bar. * * @author Arik Hadas */ public class CommandBarDialog extends CustomizeDialog { /** List that contains all available buttons, i.e buttons that are not used by the command bar */ private DynamicHorizontalWrapList commandBarAvailableButtonsList; /** List that contains the command-bar regular buttons (i.e, not alternative buttons) */ private DynamicList commandBarButtonsList; /** List that contains the command-bar alternative buttons */ private DynamicList commandBarAlternateButtonsList; /** Vector that contains the buttons in the available buttons list */ private AlteredVector commandBarAvailableButtons; /** Vector that contains the buttons in the command-bar regular buttons list */ private AlteredVector commandBarButtons; /** Vector that contains the buttons in the command-bar alternate buttons list */ private AlteredVector commandBarAlternateButtons; /** Field which allows the user to enter new KeyStroke modifier for command-bar */ private RecordingKeyStrokeTextField modifierField; /** Modifier text field length */ private static final int MODIFIER_FIELD_MAX_LENGTH = 6; /** The default color of button's border */ private static final Color JBUTTON_BACKGROUND_COLOR = UIManager.getColor("button.background"); /** Comparator for buttons according to their text */ private static final Comparator BUTTONS_COMPARATOR = (b1, b2) -> { if (b1.getText() == null) return 1; if (b2.getText() == null) return -1; return b1.getText().compareTo(b2.getText()); }; public CommandBarDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(CustomizeCommandBarAction.Descriptor.ACTION_ID)); } @Override protected void componentChanged() { setCommitButtonsEnabled(getNumberOfButtons() > 0 && (areActionsChanged() || areAlternativeActionsChanged() || isModifierChanged())); } @Override protected void commit() { int nbNewActions = getNumberOfButtons(); String[] newActionIds = new String[nbNewActions]; for (int i=0; i(); commandBarButtons = new AlteredVector<>(); commandBarAlternateButtons = new AlteredVector<>(); // a Set that contains all actions that are used by the command-bar (as regular or alternate buttons). Set usedActions = new HashSet<>(); usedActions.addAll(initCommandBarActionsList()); usedActions.addAll(initCommandBarAlternateActionsList()); initActionsPoolList(usedActions); panel.add(createAvailableButtonsPanel(), BorderLayout.CENTER); panel.add(createCommandBarPanel(), BorderLayout.SOUTH); return panel; } private boolean areActionsChanged() { // Fetch command-bar action ids String[] commandBarActionIds = CommandBarAttributes.getActions(); int nbActions = commandBarActionIds.length; if (nbActions != getNumberOfButtons()) return true; for (int i=0; i initCommandBarActionsList() { String[] commandBarActionIds = CommandBarAttributes.getActions(); //int nbCommandBarActionIds = commandBarActionIds.length; for (String commandBarActionId : commandBarActionIds) commandBarButtons.add(CommandBarButtonForDisplay.create(commandBarActionId)); commandBarButtonsList = new DynamicList<>(commandBarButtons); // Set lists cells renderer commandBarButtonsList.setCellRenderer(new CommandBarButtonListCellRenderer()); // Horizontal layout commandBarButtonsList.setLayoutOrientation(JList.HORIZONTAL_WRAP); // All buttons appear in one row commandBarButtonsList.setVisibleRowCount(1); // Drag operations are supported commandBarButtonsList.setDragEnabled(true); // Set transfer handler commandBarButtonsList.setTransferHandler(new TransferHandler(){ @Override public int getSourceActions(JComponent c) { return MOVE; } @Override public Transferable createTransferable(JComponent c) { if (c instanceof JList list) { return new TransferableButton((JButton) list.getSelectedValue()); } return null; } @Override public void exportDone(JComponent c, Transferable t, int action) { if (action == TransferHandler.MOVE) { JList list = (JList) c; removeCommandBarButtonAtIndex(list.getSelectedIndex()); componentChanged(); } } @Override public boolean canImport(TransferHandler.TransferSupport support) { return support.isDataFlavorSupported(TransferableButton.buttonFlavor); } @Override public boolean importData(TransferHandler.TransferSupport support) { if (!canImport(support)) return false; try { Point dropLocation = support.getDropLocation().getDropPoint(); JButton button = (JButton) support.getTransferable().getTransferData(TransferableButton.buttonFlavor); int index = addCommandBarButtonAtLocation(dropLocation, button); commandBarButtonsList.ensureIndexIsVisible(index); commandBarButtonsList.repaint(); return true; } catch (UnsupportedFlavorException | IOException e) { e.printStackTrace(); } return false; } }); // Set list's background color commandBarButtonsList.setBackground(JBUTTON_BACKGROUND_COLOR); commandBarButtonsList.setDropMode(DropMode.INSERT); return Arrays.asList(commandBarActionIds); } private void removeCommandBarButtonAtIndex(int index) { commandBarButtons.remove(index); if (commandBarButtons.isEmpty()) { commandBarButtons.add(null); commandBarButtonsList.setDropMode(DropMode.ON); } JButton alternateButtonAtIndex = commandBarAlternateButtons.remove(index); if (alternateButtonAtIndex != null) insertInOrder(commandBarAvailableButtons, alternateButtonAtIndex); } private int getNumberOfButtons() { int commandBarButtonsSize = commandBarButtons.size(); return commandBarButtonsSize == 1 && commandBarButtons.getFirst() == null ? 0 : commandBarButtonsSize; } private int addCommandBarButtonAtLocation(Point dropLocation, JButton button) { if (getNumberOfButtons() == 0) { int index = 0; commandBarButtons.set(index, button); commandBarAlternateButtons.add(index, null); commandBarButtonsList.setDropMode(DropMode.INSERT); return index; } else { int index = commandBarButtonsList.locationToIndex(dropLocation); index += dropLocation.x > commandBarButtonsList.indexToLocation(index).x + CommandBarButtonForDisplay.PREFERRED_SIZE.width/2 ? 1 : 0; commandBarButtons.add(index, button); commandBarAlternateButtons.add(index, null); return index; } } private Collection initCommandBarAlternateActionsList() { String[] commandBarActionIds = CommandBarAttributes.getAlternateActions(); //int nbCommandBarActionIds = commandBarActionIds.length; for (String commandBarActionId : commandBarActionIds) { commandBarAlternateButtons.add(CommandBarButtonForDisplay.create(commandBarActionId)); } commandBarAlternateButtonsList = new DynamicList<>(commandBarAlternateButtons); // Set lists cells renderer commandBarAlternateButtonsList.setCellRenderer(new CommandBarAlternativeButtonListRenderer()); // Horizontal layout commandBarAlternateButtonsList.setLayoutOrientation(JList.HORIZONTAL_WRAP); // All buttons appear in one row commandBarAlternateButtonsList.setVisibleRowCount(1); // Drag operations are supported commandBarAlternateButtonsList.setDragEnabled(true); // Set transfer handler commandBarAlternateButtonsList.setTransferHandler(new TransferHandler() { @Override public int getSourceActions(JComponent c) { return MOVE; } @Override public Transferable createTransferable(JComponent c) { if (c instanceof JList list) { return new TransferableButton((JButton) list.getSelectedValue()); } return null; } @Override public void exportDone(JComponent c, Transferable t, int action) { if (action == TransferHandler.MOVE) { JList list = (JList) c; commandBarAlternateButtons.set(list.getSelectedIndex(), null); componentChanged(); } } @Override public boolean canImport(TransferHandler.TransferSupport support) { return support.isDataFlavorSupported(TransferableButton.buttonFlavor); } @Override public boolean importData(TransferHandler.TransferSupport support) { if (!canImport(support)) return false; try { Point dropLocation = support.getDropLocation().getDropPoint(); int index = commandBarButtonsList.locationToIndex(dropLocation); JButton prevButton = commandBarAlternateButtons.get(index); if (prevButton != null) { insertInOrder(commandBarAvailableButtons, prevButton); } commandBarAlternateButtons.set(index, (JButton) support.getTransferable().getTransferData(TransferableButton.buttonFlavor)); commandBarAlternateButtonsList.ensureIndexIsVisible(index); commandBarAlternateButtonsList.repaint(); return true; } catch (UnsupportedFlavorException | IOException e) { e.printStackTrace(); } return false; } }); // Set list's background color commandBarAlternateButtonsList.setBackground(JBUTTON_BACKGROUND_COLOR); commandBarAlternateButtonsList.setDropMode(DropMode.ON); return Arrays.asList(commandBarActionIds); } private void initActionsPoolList(Set usedActions) { Iterator actionIds = ActionManager.getActionIds(); while(actionIds.hasNext()) { String actionId = actionIds.next(); // Filter out actions that are currently used in the command bar, and those that are parametrized if (!usedActions.contains(actionId) && !ActionProperties.getActionDescriptor(actionId).isParameterized()) insertInOrder(commandBarAvailableButtons, CommandBarButtonForDisplay.create(actionId)); } commandBarAvailableButtonsList = new DynamicHorizontalWrapList<>(commandBarAvailableButtons, CommandBarButtonForDisplay.PREFERRED_SIZE.width, 6); commandBarAvailableButtonsList.setCellRenderer(new AvailableButtonCellListRenderer()); commandBarAvailableButtonsList.setDragEnabled(true); commandBarAvailableButtonsList.setTransferHandler(new TransferHandler() { @Override public int getSourceActions(JComponent c) { return MOVE; } @Override public Transferable createTransferable(JComponent c) { if (c instanceof JList) { return new TransferableButton((JButton) ((JList) c).getSelectedValue()); } return null; } @Override public void exportDone(JComponent c, Transferable t, int action) { if (action == TransferHandler.MOVE) { if (c instanceof JList list) { Object button = list.getSelectedValue(); if (button instanceof JButton) { commandBarAvailableButtons.remove(button); } else { throw new RuntimeException("JButton expected"); } //commandBarAvailableButtons.remove(((JList) c).getSelectedValue()); } componentChanged(); } } @Override public boolean canImport(TransferHandler.TransferSupport support) { return support.isDataFlavorSupported(TransferableButton.buttonFlavor); } @Override public boolean importData(TransferHandler.TransferSupport support) { if (!canImport(support)) return false; try { int insertedIndex = insertInOrder(commandBarAvailableButtons, (JButton) support.getTransferable().getTransferData(TransferableButton.buttonFlavor)); commandBarAvailableButtonsList.ensureIndexIsVisible(insertedIndex); return true; } catch (UnsupportedFlavorException | IOException e) { e.printStackTrace(); } return false; } }); commandBarAvailableButtonsList.setBackground(JBUTTON_BACKGROUND_COLOR); commandBarAvailableButtonsList.setDropMode(DropMode.ON); } private JPanel createAvailableButtonsPanel() { JPanel panel = new JPanel(new GridLayout(1,0)); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("command_bar_customize_dialog.available_actions"))); JScrollPane scrollPane = new JScrollPane(commandBarAvailableButtonsList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setBorder(null); panel.add(scrollPane); return panel; } private JPanel createCommandBarPanel() { YBoxPanel panel = new YBoxPanel(); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); panel.add(Box.createRigidArea(new Dimension(0, 5))); YBoxPanel listsPanel = new YBoxPanel(); listsPanel.add(commandBarButtonsList); listsPanel.add(commandBarAlternateButtonsList); JScrollPane scrollPane = new JScrollPane(listsPanel, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); scrollPane.setBorder(null); panel.add(scrollPane); panel.add(Box.createRigidArea(new Dimension(0, 5))); panel.add(new JLabel("(" + Translator.get("command_bar_dialog.help") + ")")); panel.add(Box.createRigidArea(new Dimension(0, 5))); JPanel modifierPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); modifierField = new RecordingKeyStrokeTextField(MODIFIER_FIELD_MAX_LENGTH, CommandBarAttributes.getModifier()) { @Override public void setText(String t) { super.setText(t); componentChanged(); } @Override public void keyPressed(KeyEvent e) { int pressedKeyCode = e.getKeyCode(); // Accept modifier keys only if (pressedKeyCode == KeyEvent.VK_CONTROL || pressedKeyCode == KeyEvent.VK_ALT || pressedKeyCode == KeyEvent.VK_META || pressedKeyCode == KeyEvent.VK_SHIFT) super.keyPressed(e); } }; modifierPanel.add(new JLabel(Translator.get("command_bar_customize_dialog.modifier"))); modifierPanel.add(modifierField); panel.add(modifierPanel); return panel; } private static class TransferableButton implements Transferable { static DataFlavor buttonFlavor = new DataFlavor(CommandBarButtonForDisplay.class, null); private final JButton button; TransferableButton(JButton button) { this.button = button; } public Object getTransferData(DataFlavor flavor) { return button; } public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[] {buttonFlavor}; } public boolean isDataFlavorSupported(DataFlavor flavor) { return buttonFlavor.equals(flavor); } } private static class CommandBarButtonListCellRenderer implements ListCellRenderer { @Override public Component getListCellRendererComponent(JList list, JButton value, int index, boolean isSelected, boolean cellHasFocus) { return value == null ? createBoxFiller() : value; } } private static class CommandBarAlternativeButtonListRenderer implements ListCellRenderer { public Component getListCellRendererComponent(JList list, JButton value, int index, boolean isSelected, boolean cellHasFocus) { return value == null ? createBoxFiller() : value; } } private static class AvailableButtonCellListRenderer implements ListCellRenderer { public Component getListCellRendererComponent(JList list, JButton value, int index, boolean isSelected, boolean cellHasFocus) { JPanel panel = new JPanel(new BorderLayout()); panel.add(value, BorderLayout.CENTER); panel.setToolTipText(value.getToolTipText()); return panel; } } ////////////////////////// ///// Helper methods ///// ////////////////////////// private static Filler createBoxFiller() { Box.Filler filler = (Filler) Box.createHorizontalGlue(); filler.setPreferredSize(CommandBarButtonForDisplay.PREFERRED_SIZE); return filler; } private static int insertInOrder(List vector, JButton element) { if (!vector.isEmpty()) { int index = findPlace(vector, element, BUTTONS_COMPARATOR, 0, vector.size() - 1); vector.add(index, element); return index; } else { vector.add(element); return vector.size(); } } private static int findPlace(List vector, JButton element, Comparator comparator, int first, int last) { if (comparator.compare(vector.get(last), element) < 0) return last + 1; if (comparator.compare(vector.get(first), element) > 0) return first; if (last - first == 1) return last; int middle = (first + last) / 2; int result = comparator.compare(vector.get(middle), element); return result > 0 ? findPlace(vector, element, comparator, first, middle) : findPlace(vector, element, comparator, middle, last); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/customization/CustomizeDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.customization; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.XBoxPanel; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * Dialog which let users customize UI element. * * @author Arik Hadas. */ public abstract class CustomizeDialog extends FocusDialog implements ActionListener { private static final Dimension PREFERRED_SIZE = new Dimension(700, 500); /** Apply button. */ private JButton applyButton; /** OK button. */ private JButton okButton; /** Cancel button. */ private JButton cancelButton; CustomizeDialog(Frame parent, String title) { super(parent, title, parent); initUI(); } public CustomizeDialog(Dialog parent, String title) { super(parent, title, parent); initUI(); } private void initUI() { // Get content-pane and set its layout. Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); // Add customization panel contentPane.add(createCustomizationPanel(), BorderLayout.CENTER); // Buttons panel. XBoxPanel buttonsPanel = new XBoxPanel(); buttonsPanel.add(applyButton = new JButton(i18n("apply"))); buttonsPanel.addSpace(20); buttonsPanel.add(okButton = new JButton(i18n("ok"))); buttonsPanel.add(cancelButton = new JButton(i18n("cancel"))); // Disable "commit buttons". applyButton.setEnabled(false); okButton.setEnabled(false); // Buttons listening. applyButton.addActionListener(this); okButton.addActionListener(this); cancelButton.addActionListener(this); // Aligns the button panel to the right. JPanel tempPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); tempPanel.add(buttonsPanel); contentPane.add(tempPanel, BorderLayout.SOUTH); // Selects OK when enter is pressed getRootPane().setDefaultButton(cancelButton); // Set preferred size setPreferredSize(PREFERRED_SIZE); } protected abstract JPanel createCustomizationPanel(); protected abstract void commit(); protected abstract void componentChanged(); // - Listener code ---------------------------------------------------------- // -------------------------------------------------------------------------- /** * Reacts to buttons being pushed. */ public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // Commit changes if (source == okButton || source == applyButton) { commit(); } // Disable OK & Apply buttons if (source == applyButton) { setCommitButtonsEnabled(false); } // Dispose dialog if (source == okButton || source == cancelButton) { dispose(); } } void setCommitButtonsEnabled(boolean enabled) { applyButton.setEnabled(enabled); okButton.setEnabled(enabled); // if commit buttons are enabled then set the "okButton" as default button, // otherwise set the "cancelButton" as default button. getRootPane().setDefaultButton(enabled ? okButton : cancelButton); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/debug/DebugConsoleAppender.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.debug; import java.util.LinkedList; import java.util.List; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.core.Layout; import com.mucommander.utils.TcLogging; import com.mucommander.utils.TcLogging.LogLevel; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; /** * This java.util.logging Handler collects the last log messages that were published by * the different muCommander loggers, so they can be displayed at any time in the {@link DebugConsoleDialog}. * Log records are kept in memory as a sliding window. The number of log records is controlled by the * {@link TcPreferences#LOG_BUFFER_SIZE} configuration variable: the more records, the more memory is used. * * @see DebugConsoleDialog * @see TcPreferences#LOG_BUFFER_SIZE * @author Maxence Bernard, Arik Hadas */ public class DebugConsoleAppender extends AppenderBase { /** Maximum number of log records to keep in memory */ private int bufferSize; /** Contains the last LogRecord instances. */ private List loggingEventsList; /** The layout of the logging event representation */ private Layout loggingEventLayout; /** * Creates a new DebugConsoleHandler. This constructor is automatically by * java.util.logging when it is configured and should never be called directly. */ public DebugConsoleAppender(Layout loggingEventsLayout) { this.loggingEventLayout = loggingEventsLayout; bufferSize = TcConfigurations.getPreferences().getVariable(TcPreference.LOG_BUFFER_SIZE, TcPreferences.DEFAULT_LOG_BUFFER_SIZE); loggingEventsList = new LinkedList<>(); } /** * Returns the last records that were collected by this handler. * * @return the last records that were collected by this handler. */ synchronized LoggingEvent[] getLogRecords() { LogbackLoggingEvent[] records = new LogbackLoggingEvent[0]; records = loggingEventsList.toArray(records); return records; } ///////////////////////////// // Appender implementation // ///////////////////////////// @Override protected void append(ILoggingEvent record) { if (loggingEventsList.size()== bufferSize) { loggingEventsList.remove(0); } loggingEventsList.add(new LogbackLoggingEvent(record)); } /** * Wraps a {@link ILoggingEvent} and overrides {@link #toString()} to have it return a properly formatted string * representation of it so that it can be displayed in a {@link javax.swing.JList} or {@link javax.swing.JTable} and * pasted to the clipboard. * It also implements the LoggingEvent interface so that the logging event can be presented in the debug console. */ public class LogbackLoggingEvent implements LoggingEvent { /** The logging event */ private ILoggingEvent loggingEvent; /** The log level of the event in mucommander's terms */ private LogLevel logLevel; LogbackLoggingEvent(ILoggingEvent lr) { this.loggingEvent = lr; } /** * Returns a properly formatted string representation of the {@link ILoggingEvent}. * * @return a properly formatted string representation of the {@link ILoggingEvent}. */ @Override public String toString() { return loggingEventLayout.doLayout(loggingEvent); } /////////////////////////////////////// /// LogRecordListItem Implementation // /////////////////////////////////////// public boolean isLevelEqualOrHigherThan(LogLevel level) { return getLevel().compareTo(level) <= 0; } public LogLevel getLevel() { if (logLevel == null) { logLevel = TcLogging.getLevel(loggingEvent); } return logLevel; } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/debug/DebugConsoleDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.debug; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.util.Map; import javax.swing.*; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.utils.TcLogging; import com.mucommander.utils.TcLogging.LogLevel; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.RefreshAction; import com.mucommander.ui.action.impl.ShowDebugConsoleAction; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.main.MainFrame; /** * This dialog shows the last log messages collected by {@link DebugConsoleAppender} and allows them to be copied * to the clipboard. It also makes it possible to change the log level, the level combo box being preset to the * level returned by {@link TcLogging#getLogLevel()}. * * @see ShowDebugConsoleAction * @see DebugConsoleAppender * @see TcLogging#setLogLevel(LogLevel) * @author Maxence Bernard */ public class DebugConsoleDialog extends FocusDialog implements ActionListener, ItemListener { /** Displays log events, and allows to copy their values to the clipboard */ private final JList loggingEventsList = new JList<>(); /** Allows the log level to be changed */ private final JComboBox levelComboBox = new TcComboBox<>(LogLevel.values()); /** Closes the debug console when pressed */ private final JButton btnClose; /** Refreshes the list with the latest log records when pressed */ private final JButton btnRefresh; /** Show threads tree */ private final JButton btnThreads; /** Show active threads tree */ private final JButton btnActiveThreads; /** Dialog size constraints */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(600, 400); /** Dialog width should not exceed 360, height is not an issue (always the same) */ private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(700, 500); /** * Creates a new {@link DebugConsoleDialog} using the given {@link MainFrame} as a parent. * * @param mainFrame the {@link MainFrame} to use as a parent */ public DebugConsoleDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(ShowDebugConsoleAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); Container contentPane = getContentPane(); // Autoscroll when dragged loggingEventsList.setAutoscrolls(true); loggingEventsList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); loggingEventsList.setCellRenderer(new DebugListCellRenderer()); refreshLogRecords(); JScrollPane scrollPane = new JScrollPane(loggingEventsList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); contentPane.add(scrollPane, BorderLayout.CENTER); JPanel southPanel = new JPanel(new BorderLayout()); southPanel.add(createComboPanel(), BorderLayout.WEST); JPanel buttonPanel = new JPanel(new FlowLayout()); btnThreads = new JButton(i18n("debug_console_dialog.threads")); btnThreads.addActionListener(this); buttonPanel.add(btnThreads); btnActiveThreads = new JButton(i18n("debug_console_dialog.active_threads")); btnActiveThreads.addActionListener(this); buttonPanel.add(btnActiveThreads); btnRefresh = new JButton(new RefreshAction.Descriptor().getLabel()); btnRefresh.addActionListener(this); buttonPanel.add(btnRefresh); btnClose = new JButton(i18n("close")); btnClose.addActionListener(this); buttonPanel.add(btnClose); southPanel.add(buttonPanel, BorderLayout.EAST); contentPane.add(southPanel, BorderLayout.SOUTH); setInitialFocusComponent(btnClose); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); setInitialFocusComponent(btnClose); } /** * Creates and returns a panel containing the level combo box and a leading localized label describing it. * * @return a panel containing the level combo box and a leading localized label describing it */ private JPanel createComboPanel() { JPanel comboPanel = new JPanel(new FlowLayout()); comboPanel.add(new JLabel(i18n("debug_console_dialog.level")+":")); LogLevel logLevel = TcLogging.getLogLevel(); levelComboBox.setSelectedItem(logLevel); levelComboBox.addItemListener(this); comboPanel.add(levelComboBox); return comboPanel; } /** * Refreshes the JList with the log records contained by {@link DebugConsoleAppender}. */ private void refreshLogRecords() { DebugConsoleAppender handler = TcLogging.getDebugConsoleAppender(); if (handler == null) { return; } final LoggingEvent[] records = handler.getLogRecords(); final LogLevel currentLogLevel = TcLogging.getLogLevel(); if (records == null) { return; } DefaultListModel listModel = new DefaultListModel<>(); for (LoggingEvent record : records) { if (record.isLevelEqualOrHigherThan(currentLogLevel)) { listModel.addElement(record); } } loggingEventsList.setModel(listModel); SwingUtilities.invokeLater(() -> loggingEventsList.ensureIndexIsVisible(records.length-1)); } /** * Changes the log level to the selected combo box value. */ private void updateLogLevel() { LogLevel newLevel = (LogLevel) levelComboBox.getSelectedItem(); if (newLevel != null) { TcLogging.setLogLevel(newLevel); } } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnRefresh) { refreshLogRecords(); } else if (source == btnClose) { dispose(); } else if (source == btnThreads) { printThreads(false); } else if (source == btnActiveThreads) { printThreads(true); } } private void printThreads(boolean onlyActive) { Map stackTraces = Thread.getAllStackTraces(); DefaultListModel model = new DefaultListModel<>(); ListModel oldModel = loggingEventsList.getModel(); for (int i = 0; i < oldModel.getSize(); i++) { model.add(i, oldModel.getElementAt(i)); } model.addElement(buildStringEvent(LogLevel.INFO, "----------[Threads]--------------")); for (Thread t : stackTraces.keySet()) { if (onlyActive && t.getState() != Thread.State.RUNNABLE) { continue; } model.addElement(buildStringEvent(LogLevel.INFO, t.getName() + " (" + t.getState() + ")")); StackTraceElement[] stackTraceElements = stackTraces.get(t); for (StackTraceElement ste : stackTraceElements) { model.addElement(buildStringEvent(LogLevel.TRACE, " " + ste)); } } loggingEventsList.setModel(model); } private static LoggingEvent buildStringEvent(final LogLevel level, final String s) { return new LoggingEvent() { @Override public boolean isLevelEqualOrHigherThan(LogLevel level) { return true; } @Override public LogLevel getLevel() { return level; } @Override public String toString() { return s; } }; } public void itemStateChanged(ItemEvent e) { // Refresh the log records displayed in the JList whenever the selected level has been changed. int selectedIndex = levelComboBox.getSelectedIndex(); if (selectedIndex >= 0) { updateLogLevel(); refreshLogRecords(); } } /** * Custom {@link ListCellRenderer} that renders {@link LoggingEvent} instances. */ private class DebugListCellRenderer extends DefaultListCellRenderer { private Color getLevelColor(LogLevel logLevel) { return switch (logLevel) { case ERROR -> Color.RED; case WARNING -> new Color(255, 100, 0); // Dark orange case CONFIG -> Color.BLUE; case INFO -> Color.BLACK; case DEBUG -> Color.DARK_GRAY; default -> new Color(110, 110, 110); // Between Color.GRAY and Color.DARK_GRAY }; } @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { if (value == null) { return null; } // TODO: line-wrap log items when the text is too long to fit on a single line // A single-column JTable may be the easiest way to go, see: // http://javaspecialists.co.za/archive/newsletter.do?issue=106&locale=en_US // http://forums.sun.com/thread.jspa?threadID=702740&start=0&tstart=0 // Using a JTextArea with line-wrapping enabled does not work as a JList has by design a fixed height // for cells JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); // Change the label's foreground color to match the level of the log record if (!isSelected) { LogLevel level = ((LoggingEvent)value).getLevel(); Color color = getLevelColor(level); label.setForeground(color); } // TODO: remove this when line-wrapping has been implemented // If component's preferred width is larger than the list's width then the component is not entirely // visible. In that case, we set a tooltip text that will display the whole text when mouse is over the // component String toolTip = loggingEventsList.getVisibleRect().getWidth() < label.getPreferredSize().getWidth() ? label.getText() : null; // Have to set it to null because of the rubber-stamp rendering scheme (last value is kept) label.setToolTipText(toolTip); return label; } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/debug/LoggingEvent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.debug; import com.mucommander.utils.TcLogging.LogLevel; /** * Logging events that presented in the debug console should implement this interface * * @author Arik Hadas */ public interface LoggingEvent { /** * Return true if the logging event's level is equal or higher than the given level, false otherwise. * * @param level logging event level * @return true if the logging event's level is equal or higher than the given level, false otherwise */ boolean isLevelEqualOrHigherThan(LogLevel level); /** * Return the logging event's level. * * @return the logging event's level */ LogLevel getLevel(); } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/AbstractCopyDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.main.MainFrame; /** * This abstract class allows to factorize some code among its subclasses. * * @author Maxence Bernard */ public abstract class AbstractCopyDialog extends TransferDestinationDialog { AbstractCopyDialog(MainFrame mainFrame, FileSet files, String title, String labelText, String okText, String errorDialogTitle) { super(mainFrame, files, title, labelText, okText, errorDialogTitle, true); } /** * Returns a {@link PathFieldContent} wrapping the given path and a selection corresponding to the filename, with * a few subtleties: the file's extension is not selected for regular files or application containers * (see {@link DesktopManager#isApplication(AbstractFile)}, but selected for directories. * The rationale behind this is that it happens more often that ones wishes to rename a file's name * than its extension. This is not the case for directories where the extension is usually an artifact of a * filename that contains a '.'. * * @param file the file to be copied or renamed * @param path the destination path * @param filenameStart offset to the start of the filename in the given file * @return a {@link PathFieldContent} wrapping the given path and a selection corresponding to the filename */ public static PathFieldContent selectDestinationFilename(AbstractFile file, String path, int filenameStart) { int endPosition; // Index of the last selected character // If the current file is a directory and not an application file (e.g. Mac OS X .app directory), select // the whole file name. if (file.isDirectory() && !DesktopManager.isApplication(file)) { endPosition = path.length(); } // Otherwise, select the file name without its extension, except when empty ('.DS_Store', for example). else { endPosition = path.lastIndexOf('.'); // Text is selected so that user can directly type and replace path endPosition = endPosition > filenameStart ? endPosition : path.length(); } return new PathFieldContent(path, filenameStart, endPosition); } ////////////////////////////////////////////////////// // TransferDestinationDialog partial implementation // ////////////////////////////////////////////////////// @Override protected PathFieldContent computeInitialPath(FileSet files) { String fieldText; // Text to display in the destination field. int startPosition; // Index of the first selected character in the destination field. int endPosition; // Index of the last selected character in the destination field. // Fill text field with absolute path, and if there is only one file, // append file's name fieldText = mainFrame.getInactivePanel().getCurrentFolder().getAbsolutePath(true); // Append filename to destination path if there is only one file to copy // and if the file is not a directory that already exists in destination // (otherwise folder would be copied into the destination folder) if (files.size() == 1) { AbstractFile file = files.elementAt(0); AbstractFile destFile; startPosition = fieldText.length(); if (!(file.isDirectory() && (destFile = FileFactory.getFile(fieldText+file.getName())) != null && destFile.exists() && destFile.isDirectory())) { return selectDestinationFilename(file, fieldText + file.getName(), startPosition); } else { endPosition = fieldText.length(); } } else { endPosition = fieldText.length(); startPosition = 0; } return new PathFieldContent(fieldText, startPosition, endPosition); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/BatchRenameConfirmationDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.BatchRenameAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class BatchRenameConfirmationDialog extends FocusDialog implements ActionListener { private final JButton btnRename; private boolean proceedWithRename = false; BatchRenameConfirmationDialog(MainFrame mainFrame, FileSet files, int changed, int unchanged) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(BatchRenameAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); YBoxPanel mainPanel = new YBoxPanel(); String msg = i18n("batch_rename_dialog.proceed_renaming", Integer.toString(changed), Integer.toString(unchanged)); mainPanel.add(new InformationPane(msg, i18n("this_operation_cannot_be_undone"), Font.BOLD, InformationPane.getPredefinedIcon(InformationPane.WARNING_ICON))); mainPanel.addSpace(10); btnRename = new JButton(i18n("rename")); JButton cancelButton = new JButton(i18n("cancel")); mainPanel.add(DialogToolkit.createOKCancelPanel(btnRename, cancelButton, getRootPane(), this)); getContentPane().add(mainPanel); setInitialFocusComponent(btnRename); // Call dispose() when dialog is closed setDefaultCloseOperation(DISPOSE_ON_CLOSE); // Size dialog and show it to the screen setResizable(false); showDialog(); } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { if(e.getSource()==btnRename) { proceedWithRename = true; } dispose(); } boolean isProceedWithRename() { return proceedWithRename; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/BatchRenameDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.util.StringUtils; import com.mucommander.job.BatchRenameJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.BatchRenameAction; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.TableModelEvent; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableColumn; import javax.swing.text.BadLocationException; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.text.NumberFormat; import java.util.*; /** * Dialog used to set parameters for renaming multiple files. * * @author Mariusz Jakubowski */ public class BatchRenameDialog extends FocusDialog implements ActionListener, DocumentListener { private static final Logger LOGGER = LoggerFactory.getLogger(BatchRenameDialog.class); private static final int CASE_UNCHANGED = 0; private static final int CASE_LOWER = 1; private static final int CASE_UPPER = 2; private static final int CASE_FIRST_UPPER = 3; private static final int CASE_WORD_UPPER = 4; private static final int COL_ORIG_NAME = 0; private static final int COL_CHANGED_NAME = 1; private static final int COL_CHANGE_BLOCK = 2; private final MainFrame mainFrame; private JTextField edtFileNameMask; private JTable tblNames; private JButton btnRename; private JButton btnClose; private JTextField edtSearchFor; private JTextField edtReplaceWith; private JTextField edtCounterStart; private JTextField edtCounterStep; private JComboBox cbCounterDigits; private JComboBox cbCase; private JCheckBox cbRegExp; private RenameTableModel tableModel; private AbstractAction actRemove; private JButton btnName; private JButton btnNameRange; private JButton btnExtension; private JButton btnCounter; private JLabel lblDuplicates; private TableColumn colBlock; /** files to rename */ private final FileSet files; /** a map of old file names used to check for name conflicts */ private final Map oldNames = new HashMap<>(); /** a list of generated names */ private final List newNames = new ArrayList<>(); /** a list of flags to block file rename */ private final List blockNames = new ArrayList<>(); /** a list of parsed tokens */ private final List tokens = new ArrayList<>(); /** * Creates a new batch-rename dialog. * @param mainFrame the main frame * @param files a list of files to rename */ public BatchRenameDialog(MainFrame mainFrame, FileSet files) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(BatchRenameAction.Descriptor.ACTION_ID), null); this.mainFrame = mainFrame; this.files = files; for (AbstractFile f : files) { this.blockNames.add(Boolean.FALSE); this.newNames.add(""); oldNames.put(PathUtils.removeTrailingSeparator(f.getAbsolutePath()), f); } initialize(); generateNewNames(); } /** * Initializes the dialog. */ private void initialize() { Container content = getContentPane(); content.setLayout(new BorderLayout()); content.add(getPnlTop(), BorderLayout.NORTH); content.add(new JScrollPane(getTblNames()), BorderLayout.CENTER); content.add(getPnlButtons(), BorderLayout.SOUTH); getRootPane().setDefaultButton(btnRename); } /** * Creates bottom panel with buttons. */ private JPanel getPnlButtons() { JPanel pnlButtons = new JPanel(new BorderLayout()); pnlButtons.add(new JButton(getActRemove()), BorderLayout.WEST); XBoxPanel pnlButtonsRight = new XBoxPanel(); lblDuplicates = new JLabel(i18n("batch_rename_dialog.duplicate_names")); lblDuplicates.setForeground(Color.red); lblDuplicates.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 15)); pnlButtonsRight.add(lblDuplicates); btnRename = new JButton(i18n("rename")); btnRename.addActionListener(this); pnlButtonsRight.add(btnRename); btnClose = new JButton(i18n("cancel")); btnClose.addActionListener(this); pnlButtonsRight.add(btnClose); pnlButtons.add(pnlButtonsRight, BorderLayout.EAST); return pnlButtons; } /** * Creates a table with file names. */ private JTable getTblNames() { if (tblNames == null) { tableModel = new RenameTableModel(); tblNames = new JTable(tableModel); // add del key for remove action tblNames.getActionMap().put("del", getActRemove()); tblNames.getInputMap().put((KeyStroke) getActRemove().getValue(Action.ACCELERATOR_KEY), "del"); // add tab key tblNames.getActionMap().put("tab", new AbstractAction() { public void actionPerformed(ActionEvent e) { tblNames.transferFocus(); } }); tblNames.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0, false), "tab"); // add shift tab key tblNames.getActionMap().put("shift tab", new AbstractAction() { public void actionPerformed(ActionEvent e) { tblNames.transferFocusBackward(); } }); tblNames.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK, false), "shift tab"); // tblNames.getColumnModel().getColumn(COL_CHANGED_NAME).setCellEditor(new DefaultCellEditor(new JTextField())); colBlock = tblNames.getColumnModel().getColumn(COL_CHANGE_BLOCK); colBlock.setMaxWidth(60); tblNames.removeColumn(colBlock); } return tblNames; } private Action getActRemove() { if (actRemove == null) { actRemove = new AbstractAction() { public void actionPerformed(ActionEvent e) { removeSelectedFiles(); } }; actRemove.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); actRemove.putValue(Action.NAME, i18n("remove")); } return actRemove; } /** * Creates a panel with edit controls. */ private JPanel getPnlTop() { // file & extension mask edtFileNameMask = new JTextField("[N].[E]"); edtFileNameMask.setColumns(20); edtFileNameMask.getDocument().addDocumentListener(this); edtFileNameMask.setToolTipText(getPatternHelp()); // search & replace edtSearchFor = new JTextField(); edtSearchFor.setColumns(20); edtSearchFor.getDocument().addDocumentListener(this); edtReplaceWith = new JTextField(); edtReplaceWith.setColumns(20); edtReplaceWith.getDocument().addDocumentListener(this); // upper/lower case Vector ulcase = new Vector<>(); ulcase.add(i18n("batch_rename_dialog.no_change")); ulcase.add(i18n("batch_rename_dialog.lower_case")); ulcase.add(i18n("batch_rename_dialog.upper_case")); ulcase.add(i18n("batch_rename_dialog.first_upper")); ulcase.add(i18n("batch_rename_dialog.word")); cbCase = new JComboBox<>(ulcase); cbCase.addActionListener(this); // Regexp cbRegExp = new JCheckBox(); cbRegExp.addActionListener(this); // counter edtCounterStart = new JTextField("1"); edtCounterStart.getDocument().addDocumentListener(this); edtCounterStart.setColumns(2); edtCounterStep = new JTextField("1"); edtCounterStep.getDocument().addDocumentListener(this); edtCounterStep.setColumns(2); Vector digits = new Vector<>(); String zeros = "0000"; for (int i = 1; i <= 5; i++) { digits.add(zeros.substring(0, i - 1) + "1"); } cbCounterDigits = new JComboBox<>(digits); cbCounterDigits.addActionListener(this); // add controls XBoxPanel pnlTop = new XBoxPanel(); YBoxPanel pnl1 = new YBoxPanel(); pnl1.setBorder(BorderFactory.createTitledBorder(i18n("batch_rename_dialog.mask"))); pnl1.add(edtFileNameMask); JPanel pnl1Btns = new JPanel(new GridLayout(3, 2)); btnName = new JButton("[N] - " + i18n("name")); btnName.addActionListener(this); btnName.setHorizontalAlignment(SwingConstants.LEFT); pnl1Btns.add(btnName); btnExtension = new JButton("[E] - " + i18n("extension")); btnExtension.addActionListener(this); btnExtension.setHorizontalAlignment(SwingConstants.LEFT); pnl1Btns.add(btnExtension); btnNameRange = new JButton("[N#-#] - " + i18n("batch_rename_dialog.range")); btnNameRange.addActionListener(this); btnNameRange.setHorizontalAlignment(SwingConstants.LEFT); pnl1Btns.add(btnNameRange); btnCounter = new JButton("[C] - " + i18n("batch_rename_dialog.counter")); btnCounter.addActionListener(this); btnCounter.setHorizontalAlignment(SwingConstants.LEFT); pnl1Btns.add(btnCounter); pnl1.add(pnl1Btns); pnl1.add(new JPanel()); pnlTop.add(pnl1); XAlignedComponentPanel pnl2 = new XAlignedComponentPanel(5); pnl2.setBorder(BorderFactory.createTitledBorder(i18n("batch_rename_dialog.search_replace"))); pnl2.addRow(i18n("batch_rename_dialog.search_for"), edtSearchFor, 5); pnl2.addRow(i18n("batch_rename_dialog.replace_with"), edtReplaceWith, 5); pnl2.addRow(i18n("batch_rename_dialog.upper_lower_case"), cbCase, 5); pnl2.addRow(i18n("batch_rename_dialog.regexp"), cbRegExp, 5); pnlTop.add(pnl2); XAlignedComponentPanel pnl3 = new XAlignedComponentPanel(5); pnl3.setBorder(BorderFactory.createTitledBorder(i18n("batch_rename_dialog.counter") + " [C]")); pnl3.addRow(i18n("batch_rename_dialog.start_at"), edtCounterStart, 5); pnl3.addRow(i18n("batch_rename_dialog.step_by"), edtCounterStep, 5); pnl3.addRow(i18n("batch_rename_dialog.format"), cbCounterDigits, 5); pnlTop.add(pnl3); return pnlTop; } /** * Creates a label with help. * @return a label with help */ private String getPatternHelp() { return "" + "[N] - the whole name
    " + "[N2,3] - 3 characters starting from the 2nd character of the name
    " + "[N2-5] - characters 2 to 5
    " + "[N2-] - all characters starting from the 2nd character
    " + "[N-3,2] - two characters starting at 3rd character from the end of the name
    " + "[N2--2] - characters from the 2nd to the 2nd-last character
    " + "[C] - inserts a counter
    " + "[C10,2,3] - inserts a counter starting at 10, steps by 2, uses 3 digits
    " + "[YMD] - inserts a year, month and day when the file was last modified"; // TODO add to dictionary } /** * Removes selected files from a list of files to rename. */ private void removeSelectedFiles() { int[] sel = tblNames.getSelectedRows(); for (int i = sel.length - 1; i >= 0; i--) { files.remove(sel[i]); newNames.remove(sel[i]); blockNames.remove(sel[i]); tableModel.fireTableRowsDeleted(sel[i], sel[i]); } if (files.isEmpty()) { dispose(); } } /** * Checks if there are duplicates in new file names. */ private void checkForDuplicates() { boolean duplicates = false; boolean oldNamesConflict = false; Set names = new HashSet<>(); for (int i=0; i fi = files.iterator(); Iterator ni = newNames.iterator(); int changed = 0; while (fi.hasNext()) { AbstractFile file = fi.next(); String nn = ni.next(); if (file.getName().equals(nn)) { if (!countOnly) { fi.remove(); ni.remove(); } } else { changed++; } } return changed; } /** * Renames files. */ private void doRename() { removeUnchangedFiles(false); // start rename job if (!files.isEmpty()) { ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("progress_dialog.processing_files")); BatchRenameJob job = new BatchRenameJob(progressDialog, mainFrame, files, newNames); progressDialog.start(job); } } /** * Inserts pattern into pattern edit field. * @param pattern a text to insert */ private void insertPattern(String pattern) { int pos = edtFileNameMask.getSelectionStart(); try { int selLen = edtFileNameMask.getSelectionEnd() - edtFileNameMask.getSelectionStart(); if (selLen > 0) { edtFileNameMask.getDocument().remove(edtFileNameMask.getSelectionStart(), selLen); } edtFileNameMask.getDocument().insertString(pos, pattern, null); } catch (BadLocationException e) { LOGGER.debug("Caught exception", e); } } // ///////////////////////////////// // ActionListener implementation // // ///////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnClose) { dispose(); } else if (source == btnRename) { int unchanged = files.size(); int changed = removeUnchangedFiles(true); if (changed > 0) { BatchRenameConfirmationDialog dlg = new BatchRenameConfirmationDialog(mainFrame, files, changed, unchanged); if (dlg.isProceedWithRename()) { dispose(); doRename(); } } } else if (source == cbCase) { generateNewNames(); } else if (source == cbRegExp) { generateNewNames(); } else if (source == cbCounterDigits) { generateNewNames(); } else if (source == btnName) { insertPattern("[N]"); } else if (source == btnExtension) { insertPattern("[E]"); } else if (source == btnCounter) { insertPattern("[C]"); } else if (source == btnNameRange) { String firstFile = files.getFirst().getNameWithoutExtension(); BatchRenameSelectRange dlg = new BatchRenameSelectRange(this, firstFile); dlg.showDialog(); String range = dlg.getRange(); if (range != null) { insertPattern(range); } } } // these methods are invoked when one of edit boxes changes public void changedUpdate(DocumentEvent e) { generateNewNames(); } public void insertUpdate(DocumentEvent e) { generateNewNames(); } public void removeUpdate(DocumentEvent e) { generateNewNames(); } /** * Table model with old and new file names. * @author Mariusz Jakubowski * */ private class RenameTableModel extends AbstractTableModel { public int getColumnCount() { return 3; } public int getRowCount() { return files.size(); } public Object getValueAt(int rowIndex, int columnIndex) { AbstractFile f = files.get(rowIndex); return switch (columnIndex) { case COL_ORIG_NAME -> f.getName(); case COL_CHANGED_NAME -> newNames.get(rowIndex); case COL_CHANGE_BLOCK -> blockNames.get(rowIndex); default -> null; }; } /** * Sets the value in the cell at columnIndex and rowIndex to aValue. * Called when a user manually entered a new file name or blocked * a name from a rename pattern. */ @Override public void setValueAt(Object value, int rowIndex, int columnIndex) { switch (columnIndex) { case COL_CHANGED_NAME: if (!newNames.get(rowIndex).equals(value)) { newNames.set(rowIndex, (String)value); if (Boolean.FALSE.equals(blockNames.get(rowIndex))) { blockNames.set(rowIndex, Boolean.TRUE); if (tblNames.getColumnCount() == 2) { tblNames.addColumn(colBlock); } fireTableCellUpdated(rowIndex, COL_CHANGE_BLOCK); } checkForDuplicates(); } break; case COL_CHANGE_BLOCK: blockNames.set(rowIndex, (Boolean)value); if (Boolean.FALSE.equals(value)) { AbstractFile file = files.get(rowIndex); String newName = generateNewName(file); newNames.set(rowIndex, newName); fireTableCellUpdated(rowIndex, COL_CHANGED_NAME); } checkForDuplicates(); break; } } @Override public String getColumnName(int column) { return switch (column) { case COL_ORIG_NAME -> i18n("batch_rename_dialog.old_name"); case COL_CHANGED_NAME -> i18n("batch_rename_dialog.new_name"); case COL_CHANGE_BLOCK -> i18n("batch_rename_dialog.block_name"); default -> ""; }; } @Override public Class getColumnClass(int columnIndex) { if (columnIndex == COL_CHANGE_BLOCK) { return Boolean.class; } else { return String.class; } } @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return columnIndex != COL_ORIG_NAME; } } /** * Base class for handling tokens. * * @author Mariusz Jakubowski * */ private abstract static class AbstractToken { /** a string with a token */ protected String token; /** a current position in the token */ protected int pos = 1; /** a length of the token */ protected int len; AbstractToken(String token) { this.token = token; this.len = token.length(); } /** * Parses a token information. */ protected abstract void parse(); /** * Applies this token to a file. * * @param file a file * @return a part of filename after applying this token */ public abstract String apply(AbstractFile file); /** * Gets one char from this token. * * @return the next character in the token */ public char getChar() { if (pos < len) { return token.charAt(pos++); } return 0; } /** * Trys to get an integer from this token string. Advances the current * position in the token string. * * @param def * a default value if an integer cannot be parsed * @return the integer from this token string or the default value */ int getInt(int def) { int startPos = pos; while (pos < len) { char c = token.charAt(pos); if (c < '0' || c > '9') { if (c != '-' || startPos != pos) { break; } } pos++; } return startPos == pos ? def : StringUtils.parseIntDef(token.substring(startPos, pos), def); } } /** * Token handler that copies a character from a source string to a * destination. This is used for all characters without brackets. * * @author Mariusz Jakubowski * */ static class CopyChar extends AbstractToken { CopyChar(String token) { super(token); } @Override protected void parse() { } @Override public String apply(AbstractFile file) { return token; } } /** * Token handler that parses file name. Examples: *

      *
    • [N] - whole name *
    • [N2] - 2nd character of a name *
    • [N2,3] - 3 characters starting at the 2nd character of a name *
    • [N2-5] - characters 2 to 5 *
    • [N2-] - all characters starting from the 2nd character *
    • [N-2] - 2nd character from the end of name *
    • [N-3,2] - two characters starting at 3rd character from the end of a name *
    • [N2--2] - characters from the 2nd to the 2nd-last character *
    • [N-5-10] - characters from 5th from end to 10th from beginning of a name *
    * * @author Mariusz Jakubowski * */ static class NameToken extends AbstractToken { private int startIndex; private int endIndex; private int charCount; NameToken(String token) { super(token); } @Override protected void parse() { startIndex = getInt(0); char sep = getChar(); switch (sep) { case '-': endIndex = getInt(999); // default - to the end break; case ',': charCount = getInt(0); } } @Override public String apply(AbstractFile file) { // split name & extension String oldName = file.getName(); int dot = oldName.lastIndexOf('.'); String name = dot >= 0 ? oldName.substring(0, dot) : oldName; return extractNamePart(name); } /** * Extracts a part of a name. * * @param name * a string from which extract a part * @return the part of the name */ String extractNamePart(String name) { int targetLen = name.length(); int currentStartIndex = startIndex; int currentEndIndex = endIndex; if (currentStartIndex < 0) { currentStartIndex = targetLen + currentStartIndex + 1; if (currentStartIndex < 1) { currentStartIndex = 1; } } if (currentEndIndex < 0) { currentEndIndex = targetLen + currentEndIndex + 1; } if (currentStartIndex > 0) { if (charCount > 0) { currentEndIndex = currentStartIndex + charCount - 1; } else if (currentEndIndex == 0) { currentEndIndex = currentStartIndex; } if (currentStartIndex <= currentEndIndex && currentStartIndex - 1 < targetLen) { try { name = name.substring(currentStartIndex - 1, Math.min(currentEndIndex, targetLen)); } catch (Exception e) { LOGGER.info("currentStartIndex={}, currentEndIndex={}", currentStartIndex, currentEndIndex, e); } } else { name = ""; } } return name; } } /** * Token handler that parses a file extension. [E] - an extension of a file, * this token can also be used with parameters like in [N...] * * @author Mariusz Jakubowski */ static class ExtToken extends NameToken { ExtToken(String token) { super(token); } @Override public String apply(AbstractFile file) { // split name & extension String oldName = file.getName(); int dot = oldName.lastIndexOf('.'); String ext = dot >= 0 ? oldName.substring(dot + 1) : ""; return extractNamePart(ext); } } /** * Token handler that inserts a counter. * Examples: *
      *
    • [C] - inserts counter, as defined on the dialog *
    • [C10] - inserts counter starting at 10 *
    • [C10,2] - inserts counter starting at 10, step by 2 *
    • [C10,-2] - inserts counter starting at 10, step by -2 *
    • [C10,2,3] - inserts counter starting at 10, step by 2, use 3 digits to display *
    • [C10,,3] - inserts counter starting at 10, step by as defined on the dialog, use 3 digits to display *
    * @author Mariusz Jakubowski * */ static class CounterToken extends AbstractToken { private int start; private int step; private int digits; private int current; private NumberFormat numberFormat; CounterToken(String token, int start, int step, int digits) { super(token); this.start = start; this.step = step; this.digits = digits; } @Override protected void parse() { start = getInt(start); if (getChar() == ',') { step = getInt(step); if (getChar() == ',') { digits = getInt(digits); } } numberFormat = NumberFormat.getIntegerInstance(); numberFormat.setMinimumIntegerDigits(digits); numberFormat.setGroupingUsed(false); current = start; } @Override public String apply(AbstractFile file) { String counter = numberFormat.format(current); current += step; return counter; } } /** * Token handler that inserts a directory information. * [P] - inserts a name of the parent directory. * @author Mariusz Jakubowski */ static class ParentDirToken extends NameToken { ParentDirToken(String token) { super(token); } @Override public String apply(AbstractFile file) { AbstractFile parent = file.getParent(); return parent != null ? extractNamePart(parent.getName()) : ""; } } /** * Inserts a date or time. *
      *
    • [Y] - inserts year (4 digits) *
    • [M] - inserts month (2 digits) *
    • [D] - inserts day of a month (2 digits) *
    • [h] - inserts hours in 24-hour format (2 digits) *
    • [m] - inserts minutes (2 digits) *
    • [s] - inserts seconds *
    • [YMD] - inserts file last modified year, month and day *
    * @author Mariusz Jakubowski * */ static class DateToken extends AbstractToken { private final NumberFormat year; private final NumberFormat digits2; DateToken(String token) { super(token); year = NumberFormat.getIntegerInstance(); year.setMinimumIntegerDigits(4); year.setGroupingUsed(false); digits2 = NumberFormat.getIntegerInstance(); digits2.setMinimumIntegerDigits(2); digits2.setGroupingUsed(false); } @Override public String apply(AbstractFile file) { Calendar c = Calendar.getInstance(); c.setTimeInMillis(file.getLastModifiedDate()); StringBuilder result = new StringBuilder(); for (int i = 0; i < len; i++) { switch (token.charAt(i)) { case 'Y': result.append(year.format(c.get(Calendar.YEAR))); break; case 'M': result.append(digits2.format(c.get(Calendar.MONTH))); break; case 'D': result.append(digits2.format(c.get(Calendar.DAY_OF_MONTH))); break; case 'h': result.append(digits2.format(c.get(Calendar.HOUR_OF_DAY))); break; case 'm': result.append(digits2.format(c.get(Calendar.MINUTE))); break; case 's': result.append(digits2.format(c.get(Calendar.SECOND))); break; } } return result.toString(); } @Override protected void parse() { } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/BatchRenameSelectRange.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import javax.swing.*; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.PlainDocument; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * A dialog that allows select a range of characters from file name. * Invoked from batch-rename dialog. * * @author Mariusz Jakubowski * */ public class BatchRenameSelectRange extends FocusDialog implements ActionListener { private final JTextField edtRange; private final JButton btnCancel; private final JButton btnOK; private String range = null; BatchRenameSelectRange(Dialog owner, String filename) { super(owner, i18n("batch_rename_dialog.range"), owner); edtRange = new JTextField(); ReadOnlyDocument doc = new ReadOnlyDocument(); edtRange.setDocument(doc); edtRange.setText(filename); edtRange.setColumns(filename.length() + 5); edtRange.setSelectionStart(0); edtRange.setSelectionEnd(filename.length()); doc.setReadOnly(true); Container content = getContentPane(); content.setLayout(new BorderLayout()); content.add(edtRange, BorderLayout.CENTER); btnOK = new JButton(i18n("ok")); btnCancel = new JButton(i18n("cancel")); content.add(DialogToolkit.createOKCancelPanel(btnOK, btnCancel, getRootPane(), this), BorderLayout.SOUTH); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnCancel) { dispose(); } else if (source == btnOK) { range = "[N" + (edtRange.getSelectionStart() + 1); if (edtRange.getSelectionEnd() > 0 && edtRange.getSelectionEnd() > edtRange.getSelectionStart()+1) { range += "-" + edtRange.getSelectionEnd(); } range += "]"; dispose(); } } /** * Returns a token with selected range. */ public String getRange() { return range; } /** * A document model that can be disabled for editing. * @author Mariusz Jakubowski * */ private static class ReadOnlyDocument extends PlainDocument { private boolean readOnly = false; public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } @Override public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { if (!readOnly) { super.insertString(offs, str, a); } } @Override public void remove(int offs, int len) throws BadLocationException { if (!readOnly) { super.remove(offs, len); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/CalculateChecksumDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.io.security.MuProvider; import com.mucommander.job.CalculateChecksumJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.CalculateChecksumAction; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; import javax.swing.*; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.util.Comparator; import java.util.SortedSet; import java.util.TreeSet; /** * This dialog prepares a {@link com.mucommander.job.CalculateChecksumJob} and lets the user choose a checksum * algorithm, and a destination for the checksum file. * * @author Maxence Bernard */ public class CalculateChecksumDialog extends JobDialog implements ActionListener, ItemListener { private final TcComboBox algorithmComboBox = new TcComboBox<>(); private final JRadioButton specificLocationRadioButton; private final JTextField specificLocationTextField; private final JButton btnOk; /** An instance of all MessageDigest implementations */ private final MessageDigest[] messageDigests; /** Default checksum algorithm (most commonly used) */ private final static String DEFAULT_ALGORITHM = "MD5"; /** Last algorithm used, saved after validation of this dialog */ private static String lastUsedAlgorithm = DEFAULT_ALGORITHM; /** Dialog size constraints */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320,0); static { // Register additional MessageDigest implementations provided by the muCommander API MuProvider.registerProvider(); } public CalculateChecksumDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, ActionProperties.getActionLabel(CalculateChecksumAction.Descriptor.ACTION_ID), files); YBoxPanel mainPanel = new YBoxPanel(); // Retrieve all MessageDigest instances and sort them by alphabetical order of their algorithm // create a TreeSet with a custom Comparator SortedSet algorithmSortedSet = new TreeSet<>(Comparator.comparing(MessageDigest::getAlgorithm)); // Add all MessageDigest to the TreeSet for (String algo : Security.getAlgorithms("MessageDigest")) { try { algorithmSortedSet.add(MessageDigest.getInstance(algo)); } catch (NoSuchAlgorithmException e) { // Should never happen and if it ever does, the digest will simply be discarded } } // Convert the sorted set into an array messageDigests = new MessageDigest[algorithmSortedSet.size()]; algorithmSortedSet.toArray(messageDigests); // Add the sorted list of algorithms to a combo box to let the user choose one for (MessageDigest messageDigest : messageDigests) { algorithmComboBox.addItem(messageDigest.getAlgorithm()); } // Select the last used algorithm (if any), or the default algorithm algorithmComboBox.setSelectedItem(lastUsedAlgorithm); algorithmComboBox.addItemListener(this); FlowLayout flowLayout = new FlowLayout(FlowLayout.LEADING, 0, 0); JPanel tempPanel = new JPanel(flowLayout); tempPanel.add(new JLabel(i18n("calculate_checksum_dialog.checksum_algorithm")+" : ")); tempPanel.add(algorithmComboBox); mainPanel.add(tempPanel); mainPanel.addSpace(10); // create the components that allow to choose where the checksum file should be created mainPanel.add(new JLabel(i18n("destination")+" :")); mainPanel.addSpace(5); JRadioButton tempLocationRadioButton = new JRadioButton(i18n("calculate_checksum_dialog.temporary_file"), true); mainPanel.add(tempLocationRadioButton); specificLocationRadioButton = new JRadioButton("", false); tempPanel = new JPanel(new BorderLayout()); tempPanel.add(specificLocationRadioButton, BorderLayout.WEST); specificLocationRadioButton.addItemListener(this); // create a path field with auto-completion capabilities specificLocationTextField = new FilePathField(getChecksumFilename(lastUsedAlgorithm)); specificLocationTextField.setEnabled(false); tempPanel.add(specificLocationTextField, BorderLayout.CENTER); JPanel tempPanel2 = new JPanel(new BorderLayout()); tempPanel2.add(tempPanel, BorderLayout.NORTH); mainPanel.add(tempPanel2); ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(tempLocationRadioButton); buttonGroup.add(specificLocationRadioButton); // create file details button and OK/cancel buttons and lay them out a single row JPanel fileDetailsPanel = createFileDetailsPanel(); btnOk = new JButton(i18n("ok")); JButton cancelButton = new JButton(i18n("cancel")); mainPanel.add(createButtonsPanel(createFileDetailsButton(fileDetailsPanel), DialogToolkit.createOKCancelPanel(btnOk, cancelButton, getRootPane(), this))); mainPanel.addSpace(3); mainPanel.add(fileDetailsPanel); // mainPanel.add(new HelpButtonPanel(new HelpButton(mainFrame, "CalculateChecksum"))); getContentPane().add(mainPanel); // Give initial keyboard focus to the 'Delete' button setInitialFocusComponent(algorithmComboBox); // Call dispose() when dialog is closed setDefaultCloseOperation(DISPOSE_ON_CLOSE); // Size dialog and show it to the screen setMinimumSize(MINIMUM_DIALOG_DIMENSION); setResizable(true); } /** * Returns the MessageDigest instance corresponding to the currently selected algorithm. * * @return the MessageDigest instance corresponding to the currently selected algorithm. */ private MessageDigest getSelectedMessageDigest() { return messageDigests[algorithmComboBox.getSelectedIndex()]; } /** * Returns a de-facto standard filename for the specified checksum algorithm, e.g. MD5SUMS for * md5. * * @param algorithm a checksum algorithm * @return a standard filename for the specified checksum algorithm */ private String getChecksumFilename(String algorithm) { // Adler32 -> ADLER32SUMS // CRC32 -> .sfv (needs special treatment) // MD2 -> MD2SUMS // MD4 -> MD4SUMS // MD5 -> MD5SUMS // SHA -> SHA1SUMS (needs special treatment) // SHA-256 -> SHA256SUMS // SHA-384 -> SHA384SUMS // SHA-512 -> SHA512SUMS algorithm = algorithm.toUpperCase(); if ("SHA".equals(algorithm)) { return "SHA1SUMS";} if ("CRC32".equals(algorithm)) { return (files.size() == 1 ? files.elementAt(0) : files.getBaseFolder()).getName() + ".sfv"; } return algorithm.replace("-", "")+"SUMS"; } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { // Start by disposing this dialog dispose(); if (e.getSource() != btnOk) { return; } try { MessageDigest digest = getSelectedMessageDigest(); String algorithm = digest.getAlgorithm(); AbstractFile checksumFile; // Resolve the destination checksum file if (specificLocationRadioButton.isSelected()) { // User-defined checksum file String enteredPath = specificLocationTextField.getText(); PathUtils.ResolvedDestination resolvedDest = PathUtils.resolveDestination(enteredPath, mainFrame.getActivePanel().getCurrentFolder()); // The path entered doesn't correspond to any existing folder if (resolvedDest == null) { showErrorDialog(i18n("invalid_path", enteredPath)); return; } checksumFile = resolvedDest.getDestinationFile(); if (resolvedDest.getDestinationType()==PathUtils.ResolvedDestination.EXISTING_FOLDER) { checksumFile = checksumFile.getDirectChild(getChecksumFilename(algorithm)); } } else { // Temporary file checksumFile = FileFactory.getTemporaryFile(getChecksumFilename(algorithm), true); } // Save the algorithm that was used for the next time this dialog is invoked lastUsedAlgorithm = algorithm; // Start processing files ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("properties_dialog.calculating")); CalculateChecksumJob job = new CalculateChecksumJob(progressDialog, mainFrame, files, checksumFile, digest); progressDialog.start(job); } catch (IOException ex) { // Note: FileFactory.getTemporaryFile() should never throw an IOException showErrorDialog(i18n("invalid_path", specificLocationTextField.getText())); } } @Override public void itemStateChanged(ItemEvent e) { Object source = e.getSource(); if (source == specificLocationRadioButton) { // Enables/disables the text field when the corresponding radio button's selected state has changed. specificLocationTextField.setEnabled(specificLocationRadioButton.isSelected()); specificLocationTextField.requestFocus(); } else if (source == algorithmComboBox) { specificLocationTextField.setText(getChecksumFilename(getSelectedMessageDigest().getAlgorithm())); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/ChangeDateDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.ChangeFileAttributesJob; import com.mucommander.utils.text.CustomDateFormat; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ChangeDateAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.FluentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.util.Date; /** * This dialog allows the user to change the date of the currently selected/marked file(s). By default, the date is now * but a specific date can be specified. * * @author Maxence Bernard */ public class ChangeDateDialog extends JobDialog implements ActionListener, ItemListener { private final JRadioButton nowRadioButton; private final JSpinner dateSpinner; private final JCheckBox cdRecurseDir; private final JButton btnOk; private final JButton btnCancel; public ChangeDateDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, ActionProperties.getActionLabel(ChangeDateAction.Descriptor.ACTION_ID), files); YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(ActionProperties.getActionLabel(ChangeDateAction.Descriptor.ACTION_ID)+" :")); mainPanel.addSpace(5); ButtonGroup buttonGroup = new ButtonGroup(); AbstractFile destFile = files.size()==1?files.elementAt(0):files.getBaseFolder(); boolean canChangeDate = destFile.isFileOperationSupported(FileOperation.CHANGE_DATE); nowRadioButton = new JRadioButton(i18n("change_date_dialog.now")); nowRadioButton.setSelected(true); nowRadioButton.addItemListener(this); mainPanel.add(new FluentPanel(new FlowLayout(FlowLayout.LEFT)).add(nowRadioButton)); buttonGroup.add(nowRadioButton); JRadioButton specificDateRadioButton = new JRadioButton(i18n("change_date_dialog.specific_date")); buttonGroup.add(specificDateRadioButton); this.dateSpinner = new JSpinner(new SpinnerDateModel()); dateSpinner.setEditor(new JSpinner.DateEditor(dateSpinner, CustomDateFormat.getDateFormatString())); // Use the selected file's date if there is only one file, if not use base folder's date. dateSpinner.setValue(new Date(destFile.getLastModifiedDate())); // Spinner is disabled until the 'Specific date' radio button is selected dateSpinner.setEnabled(false); mainPanel.add(new FluentPanel(new FlowLayout(FlowLayout.LEFT)) .add(specificDateRadioButton) .add(dateSpinner)); mainPanel.addSpace(10); cdRecurseDir = new JCheckBox(i18n("recurse_directories")); mainPanel.add(cdRecurseDir); mainPanel.addSpace(15); // create file details button and OK/cancel buttons and lay them out a single row JPanel fileDetailsPanel = createFileDetailsPanel(); btnOk = new JButton(i18n("change")); btnCancel = new JButton(i18n("cancel")); mainPanel.add(createButtonsPanel(createFileDetailsButton(fileDetailsPanel), DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this))); mainPanel.add(fileDetailsPanel); getContentPane().add(mainPanel, BorderLayout.NORTH); if (!canChangeDate) { nowRadioButton.setEnabled(false); specificDateRadioButton.setEnabled(false); dateSpinner.setEnabled(false); cdRecurseDir.setEnabled(false); btnOk.setEnabled(false); } getRootPane().setDefaultButton(canChangeDate? btnOk : btnCancel); setInitialFocusComponent(canChangeDate?nowRadioButton: btnCancel); setResizable(false); } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnOk) { dispose(); // Starts copying files ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("progress_dialog.processing_files")); ChangeFileAttributesJob job = new ChangeFileAttributesJob(progressDialog, mainFrame, files, nowRadioButton.isSelected() ? System.currentTimeMillis() : ((SpinnerDateModel)dateSpinner.getModel()).getDate().getTime(), cdRecurseDir.isSelected()); progressDialog.start(job); } else if (source == btnCancel) { dispose(); } } ///////////////////////////////// // ItemListener implementation // ///////////////////////////////// // Enable/disables the date spinner component when the radio button selection has changed public void itemStateChanged(ItemEvent e) { if (e.getSource() == nowRadioButton) { dateSpinner.setEnabled(!nowRadioButton.isSelected()); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/ChangePermissionsDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.PermissionAccesses; import com.mucommander.commons.file.PermissionTypes; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.ChangeFileAttributesJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ChangePermissionsAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.SizeConstrainedDocument; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; /** * This dialog allows the user to change the permissions of the currently selected/marked file(s). The permissions can be * selected either by clicking individual read/write/executable checkboxes for each of the user/group/other accesses, * or by entering an octal permission value. * * @author Maxence Bernard */ public class ChangePermissionsDialog extends JobDialog implements ActionListener, ItemListener, DocumentListener, PermissionTypes, PermissionAccesses { private final JCheckBox[][] permCheckBoxes; private final JTextField octalPermTextField; private final JCheckBox recurseDirCheckBox; /** If true, ItemEvent events should be ignored */ private boolean ignoreItemEvent; /** If true, DocumentEvent events should be ignored */ private boolean ignoreDocumentEvent; private final JButton btnOk; private JButton btnCancel; public ChangePermissionsDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, ActionProperties.getActionLabel(ChangePermissionsAction.Descriptor.ACTION_ID), files); YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(ActionProperties.getActionLabel(ChangePermissionsAction.Descriptor.ACTION_ID)+" :")); mainPanel.addSpace(10); JPanel gridPanel = new JPanel(new GridLayout(4, 4)); permCheckBoxes = new JCheckBox[5][5]; JCheckBox permCheckBox; AbstractFile firstFile = files.elementAt(0); int permSetMask = firstFile.getChangeablePermissions().getIntValue(); boolean canSetPermission = permSetMask!=0; int defaultPerms = firstFile.getPermissions().getIntValue(); gridPanel.add(new JLabel()); gridPanel.add(new JLabel(i18n("permissions.read"))); gridPanel.add(new JLabel(i18n("permissions.write"))); gridPanel.add(new JLabel(i18n("permissions.executable"))); for (int a = USER_ACCESS; a >= OTHER_ACCESS; a--) { gridPanel.add(new JLabel(i18n(a == USER_ACCESS ?"permissions.user" : a == GROUP_ACCESS ? "permissions.group" : "permissions.other"))); for (int p = READ_PERMISSION; p >= EXECUTE_PERMISSION; p = p>>1) { permCheckBox = new JCheckBox(); permCheckBox.setSelected((defaultPerms & (p< '7') { return; } } super.insertString(offset, str, attributeSet); } }; octalPermTextField.setDocument(doc); // Initializes the field's value updateOctalPermTextField(); if (canSetPermission) { doc.addDocumentListener(this); } else { // Disable text field if no permission bit can be set octalPermTextField.setEnabled(false); } mainPanel.addSpace(10); JPanel tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(i18n("permissions.octal_notation"))); tempPanel.add(octalPermTextField); mainPanel.add(tempPanel); mainPanel.addSpace(15); recurseDirCheckBox = new JCheckBox(i18n("recurse_directories")); // Disable check box if no permission bit can be set recurseDirCheckBox.setEnabled(canSetPermission && (files.size()>1 || files.elementAt(0).isDirectory())); mainPanel.add(recurseDirCheckBox); // create file details button and OK/cancel buttons and lay them out a single row JPanel fileDetailsPanel = createFileDetailsPanel(); btnOk = new JButton(i18n("change")); btnCancel = new JButton(i18n("cancel")); mainPanel.add(createButtonsPanel(createFileDetailsButton(fileDetailsPanel), DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this))); mainPanel.add(fileDetailsPanel); getContentPane().add(mainPanel, BorderLayout.NORTH); if (!canSetPermission) { // Disable OK button if no permission bit can be set btnOk.setEnabled(false); } getRootPane().setDefaultButton(canSetPermission ? btnOk : btnCancel); setResizable(false); } /** * Creates and returns a permissions int using the values of the permission checkboxes. */ private int getPermInt() { int perms = 0; for (int a = USER_ACCESS; a >= OTHER_ACCESS; a--) { for (int p = READ_PERMISSION; p >= EXECUTE_PERMISSION; p = p>>1) { JCheckBox permCheckBox = permCheckBoxes[a][p]; if (permCheckBox.isSelected()) { perms |= (p << a * 3); } } } return perms; } /** * Updates the octal permissions text field's value to reflect the permission checkboxes' values. */ private void updateOctalPermTextField() { String octalStr = Integer.toOctalString(getPermInt()); int len = octalStr.length(); for (int i = len; i < 3; i++) { octalStr = "0" + octalStr; } octalPermTextField.setText(octalStr); } /** * Updates the permission checkboxes' values to reflect the octal permissions text field. */ private void updatePermCheckBoxes() { String octalStr = octalPermTextField.getText(); int perms = octalStr.isEmpty() ? 0 : Integer.parseInt(octalStr, 8); for (int a = USER_ACCESS; a >= OTHER_ACCESS; a--) { for (int p = READ_PERMISSION; p >= EXECUTE_PERMISSION; p = p>>1) { JCheckBox permCheckBox = permCheckBoxes[a][p]; // if(permCheckBox.isEnabled()) permCheckBox.setSelected((perms & (p<. */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.file.util.FileSet; import com.mucommander.job.ChangeFileAttributesJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ChangeReplicationAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.FluentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.PlainDocument; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * This dialog allows the user to change the date of the currently selected/marked file(s). By default, the date is now * but a specific date can be specified. * * @author Maxence Bernard */ public class ChangeReplicationDialog extends JobDialog implements ActionListener { private final IntTextField replication; private final JCheckBox cbRecurseDir; private final JButton btnOk; private final JButton btnCancel; public ChangeReplicationDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, ActionProperties.getActionLabel(ChangeReplicationAction.Descriptor.ACTION_ID), files); YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(ActionProperties.getActionLabel(ChangeReplicationAction.Descriptor.ACTION_ID)+" :")); mainPanel.addSpace(5); AbstractFile destFile = files.size()==1?files.elementAt(0):files.getBaseFolder(); boolean canChangeReplication = destFile.isFileOperationSupported(FileOperation.CHANGE_REPLICATION); short lastReplication=0; try { lastReplication=destFile.getReplication(); } catch (UnsupportedFileOperationException e) { e.printStackTrace(); } replication = new IntTextField(lastReplication, 2); JPanel tempPanel = new FluentPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(i18n("replication.number"))); tempPanel.add(replication); mainPanel.add(tempPanel); mainPanel.addSpace(10); cbRecurseDir = new JCheckBox(i18n("recurse_directories")); mainPanel.add(cbRecurseDir); mainPanel.addSpace(15); // create file details button and OK/cancel buttons and lay them out a single row JPanel fileDetailsPanel = createFileDetailsPanel(); btnOk = new JButton(i18n("change")); btnCancel = new JButton(i18n("cancel")); mainPanel.add(createButtonsPanel(createFileDetailsButton(fileDetailsPanel), DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this))); mainPanel.add(fileDetailsPanel); getContentPane().add(mainPanel, BorderLayout.NORTH); if (!canChangeReplication) { replication.setEnabled(false); cbRecurseDir.setEnabled(false); btnOk.setEnabled(false); } getRootPane().setDefaultButton(canChangeReplication? btnOk : btnCancel); setInitialFocusComponent(canChangeReplication?replication: btnCancel); setResizable(true); } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnOk) { dispose(); // Change replication ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("progress_dialog.processing_files")); ChangeFileAttributesJob job = new ChangeFileAttributesJob(progressDialog, mainFrame, files, (short)replication.getValue(), cbRecurseDir.isSelected()); progressDialog.start(job); } else if (source == btnCancel) { dispose(); } } static class IntTextField extends JTextField { IntTextField(int defval, int size) { super("" + defval, size); } protected Document createDefaultModel() { return new IntTextDocument(); } public boolean isValid() { try { Integer.parseInt(getText()); return true; } catch (Exception e) { return false; } } public int getValue() { try { return Integer.parseInt(getText()); } catch (NumberFormatException e) { return 0; } } static class IntTextDocument extends PlainDocument { public void insertString(int offs, String str, AttributeSet a) throws BadLocationException { if (str == null) { return; } String oldString = getText(0, getLength()); String newString = oldString.substring(0, offs) + str + oldString.substring(offs); try { Integer.parseInt(newString + "0"); super.insertString(offs, str, a); } catch (NumberFormatException ignored) {} } } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/CombineFilesDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.EqualsFilenameFilter; import com.mucommander.commons.file.filter.StartsWithFilenameFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.file.util.PathUtils.ResolvedDestination; import com.mucommander.job.CombineFilesJob; import com.mucommander.job.TransferFileJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.CombineFilesAction; import com.mucommander.ui.main.MainFrame; /** * Dialog used to combine file parts into the original file. * * @author Mariusz Jakubowski */ public class CombineFilesDialog extends TransferDestinationDialog { private static final Logger LOGGER = LoggerFactory.getLogger(CombineFilesDialog.class); private final AbstractFile destFolder; /** * Creates a new combine file dialog. * @param mainFrame the main frame * @param files a list of files to combine * @param destFolder default destination folder */ public CombineFilesDialog(MainFrame mainFrame, FileSet files, AbstractFile destFolder) { super(mainFrame, files, ActionProperties.getActionLabel(CombineFilesAction.Descriptor.ACTION_ID), i18n("copy_dialog.destination"), i18n("combine"), i18n("combine_files_dialog.error_title"), true); this.destFolder = destFolder; } /** * Searches for parts of a file. * @param part1 first part of a file */ private void searchParts(AbstractFile part1) { AbstractFile parent = part1.getParent(); if (parent == null) { return; } String ext = part1.getExtension(); int firstIndex; try { firstIndex = Integer.parseInt(ext); } catch (NumberFormatException e) { return; } AndFileFilter filter = new AndFileFilter( new StartsWithFilenameFilter(part1.getNameWithoutExtension(), false), new AttributeFileFilter(FileAttribute.FILE), new EqualsFilenameFilter(part1.getName(), false, true) ); try { AbstractFile[] otherParts = parent.ls(filter); for (AbstractFile otherPart : otherParts) { String ext2 = otherPart.getExtension(); try { int partIdx = Integer.parseInt(ext2); if (partIdx > firstIndex) files.add(otherPart); } catch (NumberFormatException e) { // nothing } } } catch (IOException e) { LOGGER.debug("Caught exception", e); } setFiles(files); } @Override protected boolean isValidDestination(PathUtils.ResolvedDestination resolvedDest, String destPath) { // The path entered doesn't correspond to any existing folder if (resolvedDest == null) { showErrorDialog(i18n("invalid_path", destPath), errorDialogTitle); return false; } return true; } ////////////////////////////////////////////// // TransferDestinationDialog implementation // ////////////////////////////////////////////// @Override protected PathFieldContent computeInitialPath(FileSet files) { String path = destFolder.getAbsolutePath(true) + files.elementAt(0).getNameWithoutExtension(); if (files.size() == 1) { searchParts(files.elementAt(0)); } return new PathFieldContent(path); } @Override protected TransferFileJob createTransferFileJob(ProgressDialog progressDialog, ResolvedDestination resolvedDest, int defaultFileExistsAction) { return new CombineFilesJob(progressDialog, mainFrame, files, resolvedDest.getDestinationFile(), defaultFileExistsAction); } @Override protected String getProgressDialogTitle() { return i18n("progress_dialog.processing_files"); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/CopyDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractArchiveEntryFile; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.ArchiveEntry; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.job.CopyJob; import com.mucommander.job.TransferFileJob; import com.mucommander.job.UnpackJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.CopyAction; import com.mucommander.ui.main.MainFrame; import java.util.ArrayList; import java.util.List; /** * Dialog invoked when the user wants to copy currently selected files. The destination field is pre-filled with * the 'other' panel's path and, if there is only one file to copy, with the source file's name. * * @see com.mucommander.ui.action.impl.CopyAction * @author Maxence Bernard */ public class CopyDialog extends AbstractCopyDialog { /** * Creates a new CopyDialog. * * @param mainFrame the main frame that spawned this dialog. * @param files files to be copied */ public CopyDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, files, ActionProperties.getActionLabel(CopyAction.Descriptor.ACTION_ID), i18n("copy_dialog.destination"), i18n("copy"), i18n("copy_dialog.error_title")); } ////////////////////////////////////////////// // TransferDestinationDialog implementation // ////////////////////////////////////////////// @Override protected TransferFileJob createTransferFileJob(ProgressDialog progressDialog, PathUtils.ResolvedDestination resolvedDest, int defaultFileExistsAction) { AbstractFile baseFolder = files.getBaseFolder(); AbstractArchiveFile parentArchiveFile = baseFolder.getParentArchive(); TransferFileJob job; String newName = resolvedDest.getDestinationType() == PathUtils.ResolvedDestination.EXISTING_FOLDER ? null:resolvedDest.getDestinationFile().getName(); // If the source files are located inside an archive, use UnpackJob instead of CopyJob to unpack archives in // their natural order (more efficient) if (parentArchiveFile != null) { // Add all selected archive entries to a vector //int nbFiles = files.size(); List selectedEntries = new ArrayList<>(); for (AbstractFile file : files) { System.out.println(": " + file.getAbsolutePath() + " => " + file.getAncestor(AbstractArchiveEntryFile.class) + " ==> " + file.getAncestor(AbstractArchiveEntryFile.class).getUnderlyingFileObject()); //selectedEntries.add((ArchiveEntry)file.getAncestor(AbstractArchiveEntryFile.class).getUnderlyingFileObject()); selectedEntries.add((ArchiveEntry)file.getAncestor(AbstractArchiveEntryFile.class).getUnderlyingFileObject()); } job = new UnpackJob( progressDialog, mainFrame, parentArchiveFile, PathUtils.getDepth(baseFolder.getAbsolutePath(), baseFolder.getSeparator()) - PathUtils.getDepth(parentArchiveFile.getAbsolutePath(), parentArchiveFile.getSeparator()), resolvedDest.getDestinationFolder(), newName, defaultFileExistsAction, selectedEntries ); } else { job = new CopyJob( progressDialog, mainFrame, files, resolvedDest.getDestinationFolder(), newName, CopyJob.Mode.COPY, defaultFileExistsAction); } return job; } @Override protected String getProgressDialogTitle() { return i18n("copy_dialog.copying"); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/DeleteDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.DeleteJob; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.DeleteAction; import com.mucommander.ui.action.impl.PermanentDeleteAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; /** * Confirmation dialog invoked when the user wants to delete currently selected files. It allows to choose between two * different ways of deleting files: move them to the trash or permanently erase them. The former choice is only given * if a trash is available on the current platform and capable of moving the selected files. * The choice (use trash or not) is saved in the preferences and reused next time this dialog is invoked. * * @see com.mucommander.ui.action.impl.DeleteAction * @author Maxence Bernard */ public class DeleteDialog extends JobDialog implements ItemListener, ActionListener { /** Should files be moved to the trash or permanently erased */ private boolean moveToTrash; /** Allows to control whether files should be moved to trash when deleted or permanently erased */ private JCheckBox cbMoveToTrash; /** Informs the user about the consequences of deleting files, based on the current 'Move to trash' choice */ private final InformationPane informationPane; /** The button that confirms deletion */ private final JButton btnDelete; /** Dialog size constraints */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(360, 0); public DeleteDialog(MainFrame mainFrame, FileSet files, boolean deletePermanently) { super(mainFrame, ActionProperties.getActionLabel(DeleteAction.Descriptor.ACTION_ID), files); this.mainFrame = mainFrame; YBoxPanel mainPanel = new YBoxPanel(); // Allow 'Move to trash' option only if: // - the current platform has a trash // - the base folder is not an archive // - the base folder of the to-be-deleted files is not a trash folder or one of its children // - the base folder can be moved to the trash (the eligibility conditions should be the same as the files to-be-deleted) AbstractTrash trash = DesktopManager.getTrash(); AbstractFile baseFolder = files.getBaseFolder(); if (trash != null && !baseFolder.isArchive() && !trash.isTrashFile(baseFolder) && trash.canMoveToTrash(baseFolder)) { moveToTrash = !deletePermanently; cbMoveToTrash = new JCheckBox(i18n("delete_dialog.move_to_trash.option"), moveToTrash); cbMoveToTrash.addItemListener(this); } informationPane = new InformationPane(); mainPanel.add(informationPane); mainPanel.addSpace(10); // add panel with one file above buttons JPanel fileDetailsPanel = createFileDetailsPanel(files.size() > 1); if (files.size() == 1) { mainPanel.add(fileDetailsPanel); } // create file details button and OK/cancel buttons and lay them out a single row btnDelete = new JButton(i18n("delete")); JButton cancelButton = new JButton(i18n("cancel")); mainPanel.add(createButtonsPanel(files.size() > 1 ? createFileDetailsButton(fileDetailsPanel) : null, DialogToolkit.createOKCancelPanel(btnDelete, cancelButton, getRootPane(), this))); // add panel with multiple fil list below buttons if (files.size() > 1) { mainPanel.add(fileDetailsPanel); } if (cbMoveToTrash != null) { mainPanel.add(cbMoveToTrash); } getContentPane().add(mainPanel); // Give initial keyboard focus to the 'Delete' button setInitialFocusComponent(btnDelete); // Call dispose() when dialog is closed setDefaultCloseOperation(DISPOSE_ON_CLOSE); updateDialog(); if (files.size() > 1) { // Size dialog and show it to the screen setMinimumSize(MINIMUM_DIALOG_DIMENSION); //setResizable(false); } else { Dimension d = getContentPane().getPreferredSize(); getContentPane().setMaximumSize(new Dimension(getContentPane().getMaximumSize().width, getContentPane().getPreferredSize().height)); setMinimumSizeDialog(new Dimension(d.width*6/5, d.height*6/5)); int maxWidth = Math.max(d.width*4, mainFrame.getJFrame().getWidth()); setMaximumSizeDialog(new Dimension(maxWidth, d.height*3/2)); } } /** * Updates the information pane to reflect the current 'Move to trash' choice. */ private void updateDialog() { String textId = buildTitleId(); informationPane.getMainLabel().setText(i18n(textId)); String messageId = buildMessageId(); informationPane.getCaptionLabel().setText(i18n(messageId)); informationPane.setIcon(moveToTrash ? null : InformationPane.getPredefinedIcon(InformationPane.WARNING_ICON)); setTitle(ActionManager.getActionInstance(moveToTrash ? DeleteAction.Descriptor.ACTION_ID:PermanentDeleteAction.Descriptor.ACTION_ID, mainFrame).getLabel()); } @NotNull private String buildMessageId() { if (moveToTrash) { if (files.size() == 1) { AbstractFile file = files.getFirst(); return file.isSymlink() ? "this_operation_cannot_be_undone" : "delete_dialog.move_to_trash.confirmation_details_1"; } else { return "delete_dialog.move_to_trash.confirmation_details"; } } else { return "this_operation_cannot_be_undone"; } } @NotNull private String buildTitleId() { boolean singleFileMode = files.size() == 1; if (singleFileMode) { AbstractFile file = files.getFirst(); if (file.isSymlink()) { return "delete_dialog.permanently_delete.symlink_confirmation_1"; } else { return moveToTrash ? "delete_dialog.move_to_trash.confirmation_1" : "delete_dialog.permanently_delete.confirmation_1"; } } return moveToTrash ? "delete_dialog.move_to_trash.confirmation" : "delete_dialog.permanently_delete.confirmation"; } @Override public void itemStateChanged(ItemEvent e) { moveToTrash = cbMoveToTrash.isSelected(); updateDialog(); pack(); } @Override public void actionPerformed(ActionEvent e) { // Start by disposing this dialog dispose(); if (e.getSource() == btnDelete) { // Starts deleting files ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("delete_dialog.deleting")); if (getReturnFocusTo() != null) { progressDialog.returnFocusTo(getReturnFocusTo()); } DeleteJob deleteJob = new DeleteJob(progressDialog, mainFrame, files, moveToTrash); progressDialog.start(deleteJob); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/DownloadDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.job.CopyJob; import com.mucommander.job.TransferFileJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; /** * Dialog invoked when the user wants to download a file. * * @author Maxence Bernard */ public class DownloadDialog extends TransferDestinationDialog { public DownloadDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, files, Translator.get("download_dialog.download"), Translator.get("download_dialog.description"), Translator.get("download_dialog.download"), Translator.get("download_dialog.error_title"), true); } ////////////////////////////////////////////// // TransferDestinationDialog implementation // ////////////////////////////////////////////// @Override protected PathFieldContent computeInitialPath(FileSet files) { AbstractFile file = files.elementAt(0); // AbstractFile activeFolder = mainFrame.getActiveTable().getCurrentFolder(); AbstractFile inactiveFolder = mainFrame.getInactivePanel().getCurrentFolder(); // Fill text field with current folder's absolute path and file name return new PathFieldContent(inactiveFolder.getAbsolutePath(true)+file.getName()); } @Override protected TransferFileJob createTransferFileJob(ProgressDialog progressDialog, PathUtils.ResolvedDestination resolvedDest, int defaultFileExistsAction) { return new CopyJob( progressDialog, mainFrame, files, resolvedDest.getDestinationFolder(), resolvedDest.getDestinationType()==PathUtils.ResolvedDestination.EXISTING_FOLDER?null:resolvedDest.getDestinationFile().getName(), CopyJob.Mode.DOWNLOAD, defaultFileExistsAction); } @Override protected String getProgressDialogTitle() { return Translator.get("download_dialog.downloading"); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/EmailFilesDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.IOException; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.job.SendMailJob; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EmailAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; /** * Dialog allowing the user to email files to someone. * *

    One or several recipients, as well as a mail subject and body can be input. * The dialog also allows the user to review the files that have been marked, * select/unselect some, and displays the total file size. * * @author Maxence Bernard */ public class EmailFilesDialog extends JobDialog implements ActionListener, ItemListener { private FileSet flattenedFiles; private JTextField toField; private JTextField subjectField; private JTextArea bodyArea; private JLabel infoLabel; private JCheckBox fileCheckboxes[]; private static String lastTo = ""; private static String lastSubject = ""; private static String lastBody = ""; private JButton okButton; private JButton cancelButton; // Dialog size constraints private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(400,0); private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(550,400); public EmailFilesDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, ActionProperties.getActionLabel(EmailAction.Descriptor.ACTION_ID), files); try { // Figures out which files to send and calculates the number of files and the number of bytes this.flattenedFiles = getFlattenedFiles(files); Container contentPane = getContentPane(); YBoxPanel mainPanel = new YBoxPanel(5); // Text fields panel XAlignedComponentPanel compPanel = new XAlignedComponentPanel(); // From (sender) field, non editable JLabel fromLabel = new JLabel(TcConfigurations.getPreferences().getVariable(TcPreference.MAIL_SENDER_NAME) +" <"+ TcConfigurations.getPreferences().getVariable(TcPreference.MAIL_SENDER_ADDRESS)+">"); // fromField.setEditable(false); compPanel.addRow(i18n("email_dialog.from")+":", fromLabel, 10); // To (recipients) field toField = new JTextField(lastTo); compPanel.addRow(i18n("email_dialog.to")+":", toField, 10); // Subject field subjectField = new JTextField(lastSubject); compPanel.addRow(i18n("email_dialog.subject")+":", subjectField, 15); mainPanel.add(compPanel); // Body area bodyArea = new JTextArea(lastBody); bodyArea.setRows(6); bodyArea.setLineWrap(true); JScrollPane scrollPane = new JScrollPane(bodyArea, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); mainPanel.add(scrollPane); mainPanel.addSpace(15); // Label showing the number of files and total size infoLabel = new JLabel(); mainPanel.add(infoLabel); contentPane.add(mainPanel, BorderLayout.NORTH); // checkbox showing all files that are to be sent, allowing them to be unselected int nbFiles = flattenedFiles.size(); fileCheckboxes = new JCheckBox[nbFiles]; if (nbFiles > 0) { YBoxPanel tempPanel2 = new YBoxPanel(); for(int i=0; i 0) { bytesTotal += fileSize; } nbSelected++; } } String text = i18n("nb_files", ""+nbSelected) +(nbSelected==0?"":" ("+ SizeFormat.format(bytesTotal, SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_LONG| SizeFormat.ROUND_TO_KB)+")"); infoLabel.setText(text); infoLabel.repaint(100); } /** * Returns a FileSet of *files* (as opposed to folders) that have been found either in the given * FileSet or in one of the subfolders. * * @param originalFiles files as selected by the user which may contain folders */ private FileSet getFlattenedFiles(FileSet originalFiles) throws IOException { int nbFiles = originalFiles.size(); FileSet flattenedFiles = new FileSet(originalFiles.getBaseFolder()); for (int i=0; i. */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.job.FileCollisionChecker; import com.mucommander.utils.text.CustomDateFormat; import com.mucommander.utils.text.SizeFormat; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.layout.CompareImagesPanel; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.notifier.AbstractNotifier; import com.mucommander.ui.notifier.NotificationType; import com.mucommander.ui.text.FileLabel; import com.mucommander.ui.text.FontUtils; import ru.trolsoft.ui.TMenuSeparator; import javax.swing.*; import java.awt.Component; import java.awt.Dialog; import java.awt.Font; import java.awt.Frame; import java.util.ArrayList; import java.util.List; /** * Dialog used to inform the user that a file collision has been detected and ask him how to resolve the conflict. * Prior to invoking this dialog, {@link com.mucommander.job.FileCollisionChecker} can be used to check for file collisions. * * @see com.mucommander.job.FileCollisionChecker * @author Maxence Bernard */ public class FileCollisionDialog extends QuestionDialog { /** This value is used by some FileJob classes */ public final static int ASK_ACTION = -1; public final static int CANCEL_ACTION = 0; public final static int SKIP_ACTION = 1; public final static int OVERWRITE_ACTION = 2; public final static int OVERWRITE_IF_OLDER_ACTION = 3; public final static int RESUME_ACTION = 4; public final static int RENAME_ACTION = 5; public final static String CANCEL_TEXT = Translator.get("cancel"); final static String SKIP_TEXT = Translator.get("skip"); final static String OVERWRITE_TEXT = Translator.get("overwrite"); final static String OVERWRITE_IF_OLDER_TEXT = Translator.get("overwrite_if_older"); final static String RESUME_TEXT = Translator.get("resume"); final static String RENAME_TEXT = Translator.get("rename"); private JCheckBox applyToAllCheckBox; private JLabel lblImageSize1, lblImageSize2; /** * Creates a new FileCollisionDialog. * * @param owner the Frame that owns this dialog * @param locationRelative component the location of this dialog will be based on * @param collisionType the type of collision as returned by {@link com.mucommander.job.FileCollisionChecker} * @param sourceFile the source file that 'conflicts' with the destination file, can be null. * @param destFile the destination file which already exists * @param multipleFilesMode if true, options that apply to multiple files will be displayed (skip, apply to all) * @param allowRename if true, display an option to rename a file */ public FileCollisionDialog(Dialog owner, Component locationRelative, int collisionType, AbstractFile sourceFile, AbstractFile destFile, boolean multipleFilesMode, boolean allowRename) { super(owner, Translator.get("file_collision_dialog.title"), locationRelative); init(collisionType, sourceFile, destFile, multipleFilesMode, allowRename); } /** * Creates a new FileCollisionDialog. * * @param owner the Frame that owns this dialog * @param locationRelative component the location of this dialog will be based on * @param collisionType the type of collision as returned by {@link com.mucommander.job.FileCollisionChecker} * @param sourceFile the source file that 'conflicts' with the destination file, can be null. * @param destFile the destination file which already exists * @param multipleFilesMode if true, options that apply to multiple files will be displayed (skip, apply to all) * @param allowRename if true, display an option to rename a file */ public FileCollisionDialog(Frame owner, Component locationRelative, int collisionType, AbstractFile sourceFile, AbstractFile destFile, boolean multipleFilesMode, boolean allowRename) { super(owner, Translator.get("file_collision_dialog.title"), locationRelative); init(collisionType, sourceFile, destFile, multipleFilesMode, allowRename); } private void init(int collisionType, AbstractFile sourceFile, AbstractFile destFile, boolean multipleFilesMode, boolean allowRename) { // Init choices List choicesTextV = new ArrayList<>(); List choicesActionsV = new ArrayList<>(); choicesTextV.add(CANCEL_TEXT); choicesActionsV.add(CANCEL_ACTION); if (multipleFilesMode) { choicesTextV.add(SKIP_TEXT); choicesActionsV.add(SKIP_ACTION); } // Add 'overwrite' / 'overwrite if older' / 'resume' actions only for 'destination file already exists' collision type if (collisionType == FileCollisionChecker.DESTINATION_FILE_ALREADY_EXISTS && !destFile.isDirectory()) { choicesTextV.add(OVERWRITE_TEXT); choicesActionsV.add(OVERWRITE_ACTION); if (sourceFile != null) { choicesTextV.add(OVERWRITE_IF_OLDER_TEXT); choicesActionsV.add(OVERWRITE_IF_OLDER_ACTION); // Give resume option only if destination file is smaller than source file long destSize = destFile.getSize(); long sourceSize = sourceFile.getSize(); if (destSize != -1 && (sourceSize == -1 || destSize < sourceSize)) { choicesTextV.add(RESUME_TEXT); choicesActionsV.add(RESUME_ACTION); } if (allowRename) { choicesTextV.add(RENAME_TEXT); choicesActionsV.add(RENAME_ACTION); } } } // Convert choice vectors into arrays int nbChoices = choicesActionsV.size(); String[] choicesText = new String[nbChoices]; choicesTextV.toArray(choicesText); int[] choicesActions = new int[nbChoices]; for (int i = 0; i < nbChoices; i++) { choicesActions[i] = choicesActionsV.get(i); } // Init UI String desc; if (collisionType == FileCollisionChecker.DESTINATION_FILE_ALREADY_EXISTS) { desc = Translator.get("file_exists_in_destination"); } else if (collisionType==FileCollisionChecker.SAME_SOURCE_AND_DESTINATION) { desc = Translator.get("same_source_destination"); } else if (collisionType==FileCollisionChecker.SOURCE_PARENT_OF_DESTINATION) { desc = Translator.get("source_parent_of_destination"); } else { desc = null; } YBoxPanel yPanel = new YBoxPanel(); String destFilePath = destFile.getAbsolutePath().toLowerCase(); boolean imageMode = destFilePath.endsWith(".png") || destFilePath.endsWith(".jpg") || destFilePath.endsWith(".jpeg") || destFilePath.endsWith(".bmp") || destFilePath.endsWith(".gif"); if (imageMode) { lblImageSize1 = new JLabel(); lblImageSize2 = new JLabel(); } if (desc != null) { if (imageMode) { yPanel.add(new InformationPane(desc, null, Font.PLAIN, null)); } else { yPanel.add(new InformationPane(desc, null, Font.PLAIN, InformationPane.QUESTION_ICON)); yPanel.addSpace(10); } } if (imageMode) { if (collisionType != FileCollisionChecker.SAME_SOURCE_AND_DESTINATION) { yPanel.add(new CompareImagesPanel(sourceFile, destFile, this, lblImageSize1, lblImageSize2)); } else { yPanel.add(new CompareImagesPanel(sourceFile, null, this, lblImageSize1, null)); } yPanel.addSpace(10); } // Add a separator before file details yPanel.add(new TMenuSeparator()); XAlignedComponentPanel tfPanel = new XAlignedComponentPanel(10); // If collision type is 'same source and destination' no need to show both source and destination if (collisionType == FileCollisionChecker.SAME_SOURCE_AND_DESTINATION) { addFileDetails(tfPanel, sourceFile, Translator.get("name"), lblImageSize1); } else { if (sourceFile != null) { addFileDetails(tfPanel, sourceFile, Translator.get("source"), lblImageSize1); } addFileDetails(tfPanel, destFile, Translator.get("destination"), lblImageSize2); } yPanel.add(tfPanel); // Add a separator after file details yPanel.add(new TMenuSeparator()); init(yPanel, choicesText, choicesActions, 3); // 'Apply to all' is available only for 'destination file already exists' collision type if (multipleFilesMode && collisionType == FileCollisionChecker.DESTINATION_FILE_ALREADY_EXISTS) { applyToAllCheckBox = new JCheckBox(Translator.get("apply_to_all")); addComponent(applyToAllCheckBox); } // Send a system notification if a notifier is available and enabled if (AbstractNotifier.isAvailable() && AbstractNotifier.getNotifier().isEnabled()) { AbstractNotifier.getNotifier().displayBackgroundNotification(NotificationType.JOB_ERROR, getTitle(), desc); } } private void addFileDetails(XAlignedComponentPanel panel, AbstractFile file, String nameLabel, JLabel imgSizeLabel) { addFileDetailsRow(panel, nameLabel+":", new FileLabel(file, false), 0); AbstractFile parent = file.getParent(); addFileDetailsRow(panel, Translator.get("location")+":", new FileLabel((parent == null ? file:parent), true), 0); addFileDetailsRow(panel, Translator.get("size")+":", new JLabel(SizeFormat.format(file.getSize(), SizeFormat.DIGITS_FULL| SizeFormat.UNIT_LONG| SizeFormat.INCLUDE_SPACE)), 0); addFileDetailsRow(panel, Translator.get("date")+":", new JLabel(CustomDateFormat.format(file.getLastModifiedDate())), 0); addFileDetailsRow(panel, Translator.get("permissions")+":", new JLabel(file.getPermissionsString()), imgSizeLabel == null ? 10 :0); if (imgSizeLabel != null) { addFileDetailsRow(panel, Translator.get("image_size")+":", imgSizeLabel, 10); } } private void addFileDetailsRow(XAlignedComponentPanel panel, String label, JComponent comp, int ySpaceAfter) { panel.addRow(FontUtils.makeMini(new JLabel(label)), FontUtils.makeMini(comp), ySpaceAfter); } /** * Returns true if the 'apply to all' checkbox has been selected. * * @return true if the 'apply to all' checkbox has been selected. */ public boolean applyToAllSelected() { return applyToAllCheckBox != null && applyToAllCheckBox.isSelected(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/FileCollisionRenameDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.job.CopyJob; import com.mucommander.job.ui.DialogResult; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * Dialog invoked when the user wants to change a file name after a collision has been detected * while copying or moving files. * * @see CopyJob * @author Mariusz Jakubowski */ public class FileCollisionRenameDialog extends FocusDialog implements ActionListener, DialogResult { private final JTextField edtNewName; private final JButton btnOk; private String newName; // Dialog size constraints private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320,0); // Dialog width should not exceed 360, height is not an issue (always the same) private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(400,10000); /** * Creates a new rename file dialog. * * @param mainFrame the parent MainFrame * @param file the file to rename. */ public FileCollisionRenameDialog(MainFrame mainFrame, AbstractFile file) { super(mainFrame.getJFrame(), i18n("rename"), mainFrame.getJFrame()); Container contentPane = getContentPane(); YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(i18n("rename_dialog.new_name") + ":")); edtNewName = new JTextField(); edtNewName.addActionListener(this); // Sets the initial selection. AbstractCopyDialog.selectDestinationFilename(file, file.getName(), 0).feedToPathField(edtNewName); mainPanel.add(edtNewName); mainPanel.addSpace(10); contentPane.add(mainPanel, BorderLayout.NORTH); btnOk = new JButton(i18n("rename")); JButton cancelButton = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, cancelButton, getRootPane(), this), BorderLayout.SOUTH); // Path field will receive initial focus setInitialFocusComponent(edtNewName); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // OK Button if (source == btnOk || source == edtNewName) { newName = edtNewName.getText(); } else { newName = null; } dispose(); } public Object getUserInput() { showDialog(); return newName; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/FileSelectionDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.List; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import com.jidesoft.hints.ListDataIntelliHints; import com.mucommander.cache.TextHistory; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.filter.WildcardFileFilter; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; /** * This dialog allows the user to add (mark) or remove (unmark) files from the current selection, * based on a match criterium and string. * * @author Maxence Bernard */ public class FileSelectionDialog extends FocusDialog implements ActionListener { /** Add to or remove from selection ? */ private final boolean addToSelection; private final JTextField selectionField; private final JCheckBox cbCaseSensitive; private final JCheckBox cbIncludeFolders; private final JButton btnOk; private final MainFrame mainFrame; /** * Is selection case-sensitive? (initially false) *
    Note: this field is static so the value is kept after the dialog is OKed. */ private static boolean caseSensitive = false; /** * Does the selection include folders? (initially false) *
    Note: this field is static so the value is kept after the dialog is OKed. */ private static boolean includeFolders = false; /** * Keyword which has last been typed to mark or unmark files. *
    Note: this field is static so the value is kept after the dialog is OKed. */ private static String keywordString = "*"; private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320,0); private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(400,10000); /** * Creates a new 'mark' or 'unmark' dialog. * * @param addToSelection if true, files matching */ public FileSelectionDialog(MainFrame mainFrame, boolean addToSelection) { super(mainFrame.getJFrame(), i18n(addToSelection?"file_selection_dialog.mark":"file_selection_dialog.unmark"), mainFrame.getJFrame()); this.mainFrame = mainFrame; this.addToSelection = addToSelection; Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); YBoxPanel northPanel = new YBoxPanel(5); JLabel label = new JLabel(i18n(addToSelection?"file_selection_dialog.mark_description":"file_selection_dialog.unmark_description")+" :"); northPanel.add(label); JPanel tempPanel = new JPanel(); tempPanel.setLayout(new BoxLayout(tempPanel, BoxLayout.X_AXIS)); // selectionField is initialized with last textfield's value (if any) selectionField = new JTextField(keywordString); selectionField.addActionListener(this); selectionField.setSelectionStart(0); selectionField.setSelectionEnd(keywordString.length()); List filesHistory = TextHistory.getInstance().getList(TextHistory.Type.FILE_NAME); new ListDataIntelliHints<>(selectionField, filesHistory).setCaseSensitive(false); tempPanel.add(selectionField); northPanel.add(tempPanel); // Add some vertical space northPanel.addSpace(10); cbCaseSensitive = new JCheckBox(i18n("file_selection_dialog.case_sensitive"), caseSensitive); northPanel.add(cbCaseSensitive); cbIncludeFolders = new JCheckBox(i18n("file_selection_dialog.include_folders"), includeFolders); northPanel.add(cbIncludeFolders); northPanel.addSpace(10); northPanel.add(Box.createVerticalGlue()); contentPane.add(northPanel, BorderLayout.NORTH); btnOk = new JButton(i18n(addToSelection?"file_selection_dialog.mark":"file_selection_dialog.unmark")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, new JButton(i18n("cancel")), getRootPane(), this), BorderLayout.SOUTH); // Selection field receives initial keyboard focus setInitialFocusComponent(selectionField); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); FileTable activeTable = mainFrame.getActiveTable(); // Action coming from the selection dialog if (source == btnOk || source == selectionField) { // Save values for next time this dialog is invoked caseSensitive = cbCaseSensitive.isSelected(); includeFolders = cbIncludeFolders.isSelected(); keywordString = selectionField.getText(); // Instantiate the main file IMAGE_FILTER FileFilter filter = new WildcardFileFilter(keywordString, caseSensitive); // If folders are excluded, add a regular file IMAGE_FILTER and chain it with an AndFileFilter if (!includeFolders) { filter = new AndFileFilter( new AttributeFileFilter(FileAttribute.FILE), filter ); } // Mark/unmark the files using the IMAGE_FILTER activeTable.getFileTableModel().setFilesMarked(filter, addToSelection); // Notify registered listeners that currently marked files have changed on this FileTable activeTable.fireMarkedFilesChangedEvent(); activeTable.repaint(); } dispose(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/FindFileDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.jidesoft.hints.ListDataIntelliHints; import com.mucommander.cache.TextHistory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.util.FileSet; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.job.FileJob; import com.mucommander.job.FindFileJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.FindFileAction; import com.mucommander.ui.combobox.SaneComboBox; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.encoding.EncodingPreferences; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.ui.viewer.EditorRegistrar; import com.mucommander.ui.viewer.ViewerRegistrar; import ru.trolsoft.ui.InputField; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.*; import java.util.List; /** * @author Oleg Trifonov * Find file dialog */ public class FindFileDialog extends FocusDialog implements ActionListener, DocumentListener { private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(640, 480); private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(10000, 1024); /** How often should progress information be refreshed (in ms) */ private final static int REFRESH_RATE = 200; private final MainFrame mainFrame; private FindFileJob job; private final SpinningDial dial; private final JButton btnNewSearch; private final JButton btnStop; private final JButton btnClean; private final JButton btnClose; private final JTextField edtFileName; private final InputField edtText; private final JTextField edtFromDirectory; private final JCheckBox cbSearchSubdirectories; private final JCheckBox cbSearchArchives; private final JCheckBox cbIgnoreHidden; private final JCheckBox cbCaseSensitive; private final JCheckBox cbSearchHex; private final JComboBox cbEncoding; private DefaultListModel listModel = new DefaultListModel<>(); private JList list; private final JLabel lblTotal; private AbstractFile startDirectory; private ListDataIntelliHints textHints, hexHints; private UpdateRunner updateRunner; private class UpdateRunner extends SwingWorker, AbstractFile> { @Override protected List doInBackground() { btnNewSearch.setEnabled(false); while (job != null && job.getState() != FileJob.State.FINISHED) { checkUpdates(); try { Thread.sleep(REFRESH_RATE); } catch(InterruptedException ignore) {} } checkUpdates(); job = null; return null; } @Override protected void done() { showProgress(false); updateButtons(); super.done(); } @Override protected void process(List chunks) { for (AbstractFile f : chunks) { if (isCancelled()) { break; } listModel.addElement(f); updateResultLabel(); } } private void checkUpdates() { if (job == null) { return; } final List jobResults = job.getResults(); synchronized (job) { for (int i = listModel.size(); i < jobResults.size(); i++) { AbstractFile f = jobResults.get(i); publish(f); } } } } public FindFileDialog(final MainFrame mainFrame, AbstractFile currentFolder) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(FindFileAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); this.mainFrame = mainFrame; Container contentPane = getContentPane(); YBoxPanel yPanel = new YBoxPanel(10); // Text fields panel XAlignedComponentPanel compPanel = new XAlignedComponentPanel(); // Add filename field this.edtFileName = new JTextField(); edtFileName.getDocument().addDocumentListener(this); List filesHistory = TextHistory.getInstance().getList(TextHistory.Type.FILE_NAME); new ListDataIntelliHints<>(edtFileName, filesHistory).setCaseSensitive(false); edtFileName.setText(""); compPanel.addRow(i18n("find_dialog.name") + ":", edtFileName, 5); // Add contains field this.edtText = new InputField(); edtText.getDocument().addDocumentListener(this); // List textHistory = TextHistory.getInstance().getList(TextHistory.Type.TEXT_SEARCH); // new ListDataIntelliHints<>(edtText, textHistory).setCaseSensitive(false); // edtText.setText(""); compPanel.addRow(i18n("find_dialog.contains") + ":", edtText, 5); // Add encoding field this.cbEncoding = new SaneComboBox<>(); List encodings = EncodingPreferences.getPreferredEncodings(); for (String encoding: encodings) { cbEncoding.addItem(encoding); } compPanel.addRow(i18n("find_dialog.encoding") + ":", cbEncoding, 5); // create a path field with auto-completion capabilities this.edtFromDirectory = new FilePathField(); this.edtFromDirectory.setText(currentFolder.toString()); edtFromDirectory.getDocument().addDocumentListener(this); compPanel.addRow(i18n("find_dialog.initial_directory") + ":", edtFromDirectory, 10); ProportionalGridPanel gridPanel = new ProportionalGridPanel(3); // Checkboxes this.cbSearchSubdirectories = new JCheckBox(i18n("find_dialog.search_subdirectories")); this.cbSearchArchives = new JCheckBox(i18n("find_dialog.search_archives")); this.cbCaseSensitive = new JCheckBox(i18n("find_dialog.case_sensitive")); this.cbIgnoreHidden = new JCheckBox(i18n("find_dialog.ignore_hidden")); this.cbSearchHex = new JCheckBox(i18n("find_dialog.search_hex")); TcPreferencesAPI prefs = TcConfigurations.getPreferences(); cbSearchSubdirectories.setSelected(prefs.getVariable(TcPreference.FIND_FILE_SUBDIRECTORIES, true)); cbSearchArchives.setSelected(prefs.getVariable(TcPreference.FIND_FILE_ARCHIVES, false)); cbCaseSensitive.setSelected(prefs.getVariable(TcPreference.FIND_FILE_CASE_SENSITIVE, false)); cbIgnoreHidden.setSelected(prefs.getVariable(TcPreference.FIND_FILE_IGNORE_HIDDEN, false)); cbSearchHex.setSelected(prefs.getVariable(TcPreference.FIND_FILE_SEARCH_HEX, false)); cbEncoding.setSelectedItem(prefs.getVariable(TcPreference.FIND_FILE_ENCODING, "UTF-8")); cbSearchHex.addActionListener(e -> setHexMode(cbSearchHex.isSelected())); setHexMode(cbSearchHex.isSelected()); gridPanel.add(cbSearchSubdirectories); gridPanel.add(cbSearchArchives); gridPanel.add(cbIgnoreHidden); gridPanel.add(cbCaseSensitive); gridPanel.add(cbSearchHex); compPanel.addRow(gridPanel, 0); yPanel.add(compPanel); // Search results yPanel.add(new JLabel(i18n("find_dialog.search_results"))); list = new JList<>(listModel); list.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { final AbstractFile file = getSelectedFile(); if (file == null) { return; } //final FileTable table = mainFrame.getActivePanel().getFileTable(); if (e.getClickCount() >= 2) { mainFrame.getActivePanel().tryChangeCurrentFolder(file.getParent(), file, false); } } }); list.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { super.keyPressed(e); final AbstractFile file = getSelectedFile(); if (file == null) { return; } switch (e.getKeyCode()) { case KeyEvent.VK_F3: ViewerRegistrar.createViewerFrame(mainFrame, file, IconManager.getImageIcon(file.getIcon()).getImage(), (fileFrame) -> { fileFrame.returnFocusTo(getFocusOwner()); if (cbSearchHex.isSelected()) { fileFrame.setSearchedBytes(edtText.getBytes()); } else { fileFrame.setSearchedText(edtText.getText()); } }); break; case KeyEvent.VK_F4: EditorRegistrar.createEditorFrame(mainFrame, file, IconManager.getImageIcon(file.getIcon()).getImage(), (fileFrame) -> { fileFrame.returnFocusTo(getFocusOwner()); if (cbSearchHex.isSelected()) { fileFrame.setSearchedBytes(edtText.getBytes()); } else { fileFrame.setSearchedText(edtText.getText()); } }); break; case KeyEvent.VK_SPACE: mainFrame.getActivePanel().tryChangeCurrentFolder(file.getParent(), file, false); break; case KeyEvent.VK_F5: new CopyDialog(mainFrame, getSelectedFiles()).returnFocusTo(getFocusOwner()).showDialog(); break; case KeyEvent.VK_F6: new MoveDialog(mainFrame, getSelectedFiles()).returnFocusTo(getFocusOwner()).showDialog(); break; case KeyEvent.VK_F8: case KeyEvent.VK_DELETE: new DeleteDialog(mainFrame, getSelectedFiles(), false).returnFocusTo(getFocusOwner()).showDialog(); break; } } }); list.setCellRenderer(new FindFileResultRenderer()); list.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]); JScrollPane scrollPane = new JScrollPane(list); contentPane.add(scrollPane, BorderLayout.CENTER); // Bottom line MnemonicHelper mnemonicHelper = new MnemonicHelper(); // yPanel.add(new JLabel(dial = new SpinningDial())); XBoxPanel buttonsPanel = new XBoxPanel(); JPanel buttonGroupPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); buttonsPanel.add(lblTotal = new JLabel()); buttonsPanel.add(new JLabel(dial = new SpinningDial())); buttonsPanel.add(Box.createHorizontalGlue()); btnNewSearch = new JButton(i18n("search")); btnNewSearch.addActionListener(this); btnNewSearch.setMnemonic(mnemonicHelper.getMnemonic(btnNewSearch)); buttonGroupPanel.add(btnNewSearch); btnStop = new JButton(i18n("stop")); btnStop.addActionListener(this); btnStop.setMnemonic(mnemonicHelper.getMnemonic(btnStop)); buttonGroupPanel.add(btnStop); btnClean = new JButton(i18n("clean")); btnClean.addActionListener(this); btnClean.setMnemonic(mnemonicHelper.getMnemonic(btnClean)); buttonGroupPanel.add(btnClean); btnClose = new JButton(i18n("close")); btnClose.addActionListener(this); btnClose.setMnemonic(mnemonicHelper.getMnemonic(btnClose)); buttonGroupPanel.add(btnClose); buttonsPanel.add(buttonGroupPanel); contentPane.add(buttonsPanel, BorderLayout.SOUTH); contentPane.add(yPanel, BorderLayout.NORTH); setInitialFocusComponent(edtFileName); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); updateButtons(); getRootPane().setDefaultButton(btnNewSearch); setModal(false); } private void setHexMode(boolean hexMode) { cbEncoding.setEnabled(!hexMode); edtText.setText(""); if (textHints != null) { textHints.setAutoPopup(false); textHints = null; } if (hexHints != null) { hexHints.setAutoPopup(false); hexHints = null; } edtText.setFilterType(cbSearchHex.isSelected() ? InputField.FilterType.HEX_DUMP : InputField.FilterType.ANY_TEXT); if (hexMode) { List hexHistory = TextHistory.getInstance().getList(TextHistory.Type.HEX_DATA_SEARCH); textHints = new ListDataIntelliHints<>(edtText, hexHistory); textHints.setCaseSensitive(false); } else { List textHistory = TextHistory.getInstance().getList(TextHistory.Type.TEXT_SEARCH); hexHints = new ListDataIntelliHints<>(edtText, textHistory); hexHints.setCaseSensitive(false); } } private void updateButtons() { btnNewSearch.setEnabled(job == null); btnStop.setEnabled(!btnNewSearch.isEnabled()); btnClean.setEnabled(!(listModel == null || listModel.isEmpty())); } private void start() { TextHistory.getInstance().add(TextHistory.Type.FILE_NAME, edtFileName.getText(), true); TextHistory.getInstance().add(TextHistory.Type.TEXT_SEARCH, edtText.getText(), true); showProgress(true); clearResults(); job = new FindFileJob(mainFrame); startDirectory = FileFactory.getFile(edtFromDirectory.getText()); job.setStartDirectory(startDirectory); job.setup(edtFileName.getText(), edtText.getText(), cbSearchSubdirectories.isSelected(), cbSearchArchives.isSelected(), cbCaseSensitive.isSelected(), cbIgnoreHidden.isSelected(), cbEncoding.getSelectedItem().toString(), cbSearchHex.isSelected(), cbSearchHex.isSelected() ? edtText.getBytes() : null); updateResultLabel(); job.start(); updateButtons(); updateRunner = new UpdateRunner(); updateRunner.execute(); } private void clearResults() { if (listModel != null) { listModel.clear(); } lblTotal.setText(""); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == btnNewSearch) { if (job == null) { start(); } } else if (e.getSource() == btnStop) { if (job != null) { job.interrupt(); job = null; } } else if (e.getSource() == btnClean) { clearResults(); } else if (e.getSource() == btnClose) { cancel(); } } private void showProgress(boolean show) { dial.setAnimated(show); } @Override public void insertUpdate(DocumentEvent e) { } @Override public void removeUpdate(DocumentEvent e) { } @Override public void changedUpdate(DocumentEvent e) { } private AbstractFile getSelectedFile() { int index = list.getSelectedIndex(); if (index < 0) { return null; } return listModel.get(index); } private FileSet getSelectedFiles() { FileSet files = new FileSet(startDirectory); int[] selectedIx = list.getSelectedIndices(); for (int aSelectedIx : selectedIx) { AbstractFile file = listModel.get(aSelectedIx); files.add(file); } return files; } private void updateResultLabel() { lblTotal.setText(i18n("find_dialog.found") + ": " + listModel.size() + " "); } @Override public void cancel() { if (job != null) { job.interrupt(); } TcPreferencesAPI prefs = TcConfigurations.getPreferences(); prefs.setVariable(TcPreference.FIND_FILE_ARCHIVES, cbSearchArchives.isSelected()); prefs.setVariable(TcPreference.FIND_FILE_CASE_SENSITIVE, cbCaseSensitive.isSelected()); prefs.setVariable(TcPreference.FIND_FILE_IGNORE_HIDDEN, cbIgnoreHidden.isSelected()); prefs.setVariable(TcPreference.FIND_FILE_SEARCH_HEX, cbSearchHex.isSelected()); prefs.setVariable(TcPreference.FIND_FILE_SUBDIRECTORIES, cbSearchSubdirectories.isSelected()); prefs.setVariable(TcPreference.FIND_FILE_ENCODING, cbEncoding.getSelectedItem().toString()); super.cancel(); } @Override public void dispose() { super.dispose(); if (updateRunner != null) { try { updateRunner.cancel(true); } catch (Throwable t) { t.printStackTrace(); } } clearResults(); updateRunner = null; listModel = null; job = null; list = null; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/FindFileResultRenderer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.main.table.*; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.utils.FileIconsCache; import javax.swing.*; import java.awt.*; /** * @author Oleg Trifonov * Created on 11.01.15. */ public class FindFileResultRenderer implements ListCellRenderer { private final CellLabel cellLabel = new CellLabel(); @Override public Component getListCellRendererComponent(JList list, AbstractFile value, int index, boolean isSelected, boolean cellHasFocus) { // Need to check that row index is not out of bounds because when the folder ListModel model = list.getModel(); if (value == null) { return null; } // Retrieves the various indexes of the colors to apply. // Selection only applies when the table is the active one final int selectedIndex = isSelected ? ThemeCache.SELECTED : ThemeCache.NORMAL; final int colorIndex = getColorIndex(value); cellLabel.setIcon(FileIconsCache.getInstance().getIcon(value)); String text = value.getAbsolutePath(); Color foregroundColor; if (isSelected) { foregroundColor = ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED][colorIndex]; } else { int group = FileGroupResolver.getInstance().resolve(value); foregroundColor = group >= 0 ? ThemeCache.groupColors[group] : ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL][colorIndex]; } cellLabel.setForeground(foregroundColor); // Set the label's text, before calculating it width cellLabel.setText(text); cellLabel.setToolTipText(text); // Set background color depending on whether the row is selected or not, and whether the table has focus or not if (isSelected) { cellLabel.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED], ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SECONDARY]); } else { int matchesColorIndex = (index % 2 == 0) ? ThemeCache.NORMAL : ThemeCache.ALTERNATE; cellLabel.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][matchesColorIndex], ThemeCache.backgroundColors[ThemeCache.ACTIVE][matchesColorIndex]); } if (selectedIndex == ThemeCache.SELECTED) { cellLabel.setOutline(cellHasFocus ? ThemeCache.activeOutlineColor : ThemeCache.inactiveOutlineColor); } else { cellLabel.setOutline(null); } return cellLabel; } private static int getColorIndex(AbstractFile file) { // Symlink. if (file.isSymlink()) { return ThemeCache.SYMLINK; } // Hidden file/folder. if (file.isHidden()) { return file.isDirectory() ? ThemeCache.HIDDEN_FOLDER : ThemeCache.HIDDEN_FILE; } // Directory. if (file.isDirectory()) { return ThemeCache.FOLDER; } // Archive. if (file.isBrowsable()) { return ThemeCache.ARCHIVE; } // Plain file. return ThemeCache.PLAIN_FILE; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/JobDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.button.CollapseExpandButton; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.layout.AsyncPanel; import com.mucommander.ui.list.FileList; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.Container; import java.awt.Window; /** * This abstract dialog is to be sub-classed by job confirmation dialogs and provides helper methods for common * components. * * @author Maxence Bernard */ public abstract class JobDialog extends FocusDialog { /** Number of files displayed in the 'file details' text area */ private final static int NB_FILE_DETAILS_ROWS = 10; private static boolean lastExpanded = false; protected MainFrame mainFrame; protected FileSet files; private CollapseExpandButton collapseExpandButton; JobDialog(MainFrame mainFrame, String title, FileSet files) { super(mainFrame.getJFrame(), title, mainFrame.getJFrame()); this.mainFrame = mainFrame; this.files = files; } /** * Displays an error dialog with the specified message and title. * * @param message the error message * @param title the error title */ protected void showErrorDialog(String message, String title) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), title, message); } /** * Displays an error dialog with the specified message and the default error title. * * @param message the error message */ protected void showErrorDialog(String message) { showErrorDialog(message, i18n("error")); } /** * Creates and returns a 'File details' panel, showing details about the files that the job will operate on. The file details * are loaded in a separate thread, when the panel becomes visible. * * @param packOnUpdate pack the window after list loading * * @return a 'File details' panel, showing details about the files that the job will operate on */ AsyncPanel createFileDetailsPanel(boolean packOnUpdate) { return new AsyncPanel() { @Override public JComponent getTargetComponent(Exception e) { FileList fileList = new FileList(files, true); fileList.setVisibleRowCount(NB_FILE_DETAILS_ROWS); return new JScrollPane(fileList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); } @Override public void initTargetComponent() { } @Override protected void updateLayout() { if (packOnUpdate) { Container tla = getTopLevelAncestor(); if (tla instanceof Window) { ((Window) tla).pack(); } } } }; } protected AsyncPanel createFileDetailsPanel() { return createFileDetailsPanel(true); } /** * Creates and returns a button that expands/collapses the specified 'File details' panel. * The number of files that the job will operate on are displayed in the button's label. * * @param detailsPanel the 'File details' panel to expand/collapse * @return a button that expands/collapses the specified 'File details' panel */ protected CollapseExpandButton createFileDetailsButton(JPanel detailsPanel) { collapseExpandButton = new CollapseExpandButton(i18n("nb_files", String.valueOf(files.size())), detailsPanel, lastExpanded); return collapseExpandButton; } /** * Creates a panel where the specified 'File details' button and OK/cancel control buttons are laid out on a single row, * with a space separation between the two components. * * @param fileDetailsButton the button that expands/collapses the 'File details' panel * @param buttonsPanel the panel that contains the OK/cancel control buttons * @return a panel where the specified 'File details' button and OK/cancel control buttons are laid out on a single row */ protected JPanel createButtonsPanel(JButton fileDetailsButton, JPanel buttonsPanel) { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); if (fileDetailsButton != null) { panel.add(fileDetailsButton); } panel.add(Box.createVerticalGlue()); panel.add(buttonsPanel); return panel; } /** * Sets the list of files used by this job. * @param files */ protected void setFiles(FileSet files) { this.files = files; if (collapseExpandButton != null) { collapseExpandButton.setText(i18n("nb_files", Integer.toString(files.size()))); } } @Override public void dispose() { if (collapseExpandButton != null) { lastExpanded = collapseExpandButton.getExpandedState(); } super.dispose(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/LocalCopyDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.main.MainFrame; /** * Dialog invoked when the user wants to copy a single file to the same directory under a different name. * The destination field is pre-filled with the file's name. * * @author Maxence Bernard */ public class LocalCopyDialog extends CopyDialog { /** * Creates a new LocalCopyDialog. * * @param mainFrame the main frame that spawned this dialog. * @param files files to be copied */ public LocalCopyDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, files); } @Override protected PathFieldContent computeInitialPath(FileSet files) { AbstractFile file = files.elementAt(0); return selectDestinationFilename(file, file.getName(), 0); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/MakeDirectoryFileDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.job.MakeDirectoryFileJob; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.action.impl.MkdirAction; import com.mucommander.ui.action.impl.MkfileAction; import com.mucommander.ui.chooser.SizeChooser; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.helper.FocusRequester; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; import com.mucommander.ui.viewer.EditorRegistrar; import org.jetbrains.annotations.NotNull; /** * Dialog invoked when the user wants to create a new folder or an empty file in the current folder. * * @see MkdirAction * @see MkfileAction * @author Maxence Bernard */ public class MakeDirectoryFileDialog extends FocusDialog implements ActionListener, ItemListener { private final MainFrame mainFrame; private final JTextField pathField; private JCheckBox cbAllocateSpace; private JCheckBox cbOpenTextEditor; private JCheckBox cbMakeExecutable; private SizeChooser allocateSpaceChooser; private final JButton btnOk; private final boolean mkfileMode; private boolean autoExecutableSelect; private static boolean openInTextEditor = true; /** * Dialog size constraints */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320, 0); /** * Dialog width should not exceed 360, height is not an issue (always the same) */ private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(400, 10000); private JCheckBox convertWhiteSpaceCheckBox; /** * As a developer, it is annoyed to meet with Folder name that contains whitespace */ private String oldDirName; /** * Creates a new Mkdir/Mkfile dialog. * * @param mkfileMode if true, the dialog will operate in 'mkfile' mode, if false in 'mkdir' mode */ public MakeDirectoryFileDialog(MainFrame mainFrame, boolean mkfileMode) { super(mainFrame.getJFrame(), ActionManager.getActionInstance(mkfileMode ? MkfileAction.Descriptor.ACTION_ID : MkdirAction.Descriptor.ACTION_ID, mainFrame).getLabel(), mainFrame.getJFrame()); this.mainFrame = mainFrame; this.mkfileMode = mkfileMode; setStorageSuffix(mkfileMode ? "file" : "dir"); Container contentPane = getContentPane(); YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(ActionProperties.getActionTooltip(mkfileMode ? MkfileAction.Descriptor.ACTION_ID : MkdirAction.Descriptor.ACTION_ID)+" :")); // Create a path field with auto-completion capabilities pathField = new FilePathField(); pathField.addActionListener(this); // Sets the initial selection. AbstractFile currentFile = mainFrame.getActiveTable().getSelectedFile(); if (currentFile != null) { String initialValue = makeInitialValue(currentFile); if (initialValue != null) { pathField.setText(initialValue); } } pathField.setSelectionStart(0); pathField.setSelectionEnd(pathField.getText().length()); mainPanel.add(pathField); if (mkfileMode) { JPanel allocPanel = new JPanel(new BorderLayout()); cbAllocateSpace = new JCheckBox(i18n("mkfile_dialog.allocate_space")+":", false); cbAllocateSpace.addItemListener(this); allocPanel.add(cbAllocateSpace, BorderLayout.WEST); allocateSpaceChooser = new SizeChooser(false); allocateSpaceChooser.setEnabled(false); allocPanel.add(allocateSpaceChooser, BorderLayout.EAST); mainPanel.add(allocPanel); cbOpenTextEditor = new JCheckBox(i18n("mkfile_dialog.open_in_editor"), false); cbOpenTextEditor.addItemListener(this); cbOpenTextEditor.setSelected(openInTextEditor); mainPanel.add(cbOpenTextEditor); if (OsFamily.getCurrent().isUnixBased()) { cbMakeExecutable = new JCheckBox(i18n("mkfile_dialog.make_executable"), false); cbMakeExecutable.addItemListener(this); mainPanel.add(cbMakeExecutable); pathField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { check(); } @Override public void removeUpdate(DocumentEvent e) { check(); } @Override public void changedUpdate(DocumentEvent e) { check(); } private void check() { if (!autoExecutableSelect && pathField.getText().endsWith(".sh")) { cbMakeExecutable.setSelected(true); autoExecutableSelect = true; } } }); } } else { JPanel convertWhitespacePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); convertWhitespacePanel.add(new JLabel(i18n("mkfile_dialog.convert_whitespace"))); this.convertWhiteSpaceCheckBox = new JCheckBox(); convertWhiteSpaceCheckBox.addItemListener(arg0 -> { if (convertWhiteSpaceCheckBox.isSelected()) { oldDirName = pathField.getText(); pathField.setText(oldDirName.replace(" ", "_")); } else { pathField.setText(oldDirName); } }); convertWhitespacePanel.add(convertWhiteSpaceCheckBox); mainPanel.add(convertWhitespacePanel); } mainPanel.addSpace(10); contentPane.add(mainPanel, BorderLayout.NORTH); btnOk = new JButton(i18n("create")); JButton cancelButton = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, cancelButton, getRootPane(), this), BorderLayout.SOUTH); // Path field will receive initial focus setInitialFocusComponent(pathField); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); } private String makeInitialValue(AbstractFile currentFile) { if (mkfileMode) { return currentFile.getName(); } else { return currentFile.getNameWithoutExtension(); } } /** * Starts an {@link com.mucommander.job.MakeDirectoryFileJob}. This method is trigged by the 'OK' button or the return key. */ private void startJob() { String enteredPath = pathField.getText(); // Resolves destination folder PathUtils.ResolvedDestination resolvedDest = PathUtils.resolveDestination(enteredPath, mainFrame.getActivePanel().getCurrentFolder(), false); // The path entered doesn't correspond to any existing folder if (resolvedDest == null) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), i18n("invalid_path", enteredPath)); return; } // Checks if the directory already exists and reports the error if that's the case int destinationType = resolvedDest.getDestinationType(); if (destinationType == PathUtils.ResolvedDestination.EXISTING_FOLDER) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), i18n("directory_already_exists", enteredPath)); return; } // Don't check for existing regular files, MakeDirectoryFileJob will take of it and popup a FileCollisionDialog AbstractFile destFile = resolvedDest.getDestinationFile(); FileSet fileSet = new FileSet(destFile.getParent()); // Job's FileSet needs to contain at least one file fileSet.add(destFile); ProgressDialog progressDialog = new ProgressDialog(mainFrame, getTitle()); MakeDirectoryFileJob job; job = buildJob(fileSet, progressDialog); progressDialog.start(job); } @NotNull private MakeDirectoryFileJob buildJob(FileSet fileSet, ProgressDialog progressDialog) { if (mkfileMode) { long allocateSpace = cbAllocateSpace.isSelected() ? allocateSpaceChooser.getValue() : -1; boolean executable = cbMakeExecutable != null && cbMakeExecutable.isSelected(); openInTextEditor = cbOpenTextEditor.isSelected(); return new MakeDirectoryFileJob(progressDialog, mainFrame, fileSet, allocateSpace, executable) { @Override protected boolean processFile(AbstractFile file, Object recurseParams) { boolean result = super.processFile(file, recurseParams); if (result && openInTextEditor) { Image icon = ActionProperties.getActionIcon(EditAction.Descriptor.ACTION_ID).getImage(); EditorRegistrar.createEditorFrame(mainFrame, file, icon, FocusRequester::requestFocus); } return result; } }; } else { return new MakeDirectoryFileJob(progressDialog, mainFrame, fileSet); } } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); dispose(); // OK Button if (source == btnOk || source == pathField) { startJob(); } } public void itemStateChanged(ItemEvent e) { allocateSpaceChooser.setEnabled(cbAllocateSpace.isSelected()); if (e.getItem() == cbAllocateSpace && cbAllocateSpace.isSelected()) { cbOpenTextEditor.setSelected(false); } else if (e.getItem() == cbOpenTextEditor && cbOpenTextEditor.isSelected()) { cbAllocateSpace.setSelected(false); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/MoveDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.job.MoveJob; import com.mucommander.job.TransferFileJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.MoveAction; import com.mucommander.ui.main.MainFrame; /** * Dialog invoked when the user wants to move or rename currently selected files. * * @see com.mucommander.ui.action.impl.MoveAction * @see com.mucommander.ui.action.impl.RenameAction * @author Maxence Bernard */ public class MoveDialog extends AbstractCopyDialog { public MoveDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, files, ActionProperties.getActionLabel(MoveAction.Descriptor.ACTION_ID), Translator.get("move_dialog.move_description"), Translator.get("move"), Translator.get("move_dialog.error_title")); } @Override protected TransferFileJob createTransferFileJob(ProgressDialog progressDialog, PathUtils.ResolvedDestination resolvedDest, int defaultFileExistsAction) { return new MoveJob( progressDialog, mainFrame, files, resolvedDest.getDestinationFolder(), resolvedDest.getDestinationType()==PathUtils.ResolvedDestination.EXISTING_FOLDER?null:resolvedDest.getDestinationFile().getName(), defaultFileExistsAction, false); } @Override protected String getProgressDialogTitle() { return Translator.get("move_dialog.moving"); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/PackDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.awt.FlowLayout; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import javax.swing.*; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.archiver.ArchiveFormat; import com.mucommander.commons.file.archiver.Archiver; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.ArchiveJob; import com.mucommander.job.TransferFileJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.PackAction; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; /** * This dialog allows the user to pack marked files to an archive file of a selected format (Zip, TAR, ...) * and add an optional comment to the archive (for the formats that support it). * * @author Maxence Bernard */ public class PackDialog extends TransferDestinationDialog implements ItemListener { private final JComboBox formatsComboBox; private final ArchiveFormat[] formats; private final JTextArea commentArea; /** Used to keep track of the last selected archive format. */ private int lastFormatIndex; /** Last archive format used (Zip initially), selected by default when this dialog is created */ private static ArchiveFormat lastFormat = ArchiveFormat.ZIP; public PackDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, files, ActionProperties.getActionLabel(PackAction.Descriptor.ACTION_ID), i18n("pack_dialog_description"), i18n("pack"), i18n("pack_dialog.error_title"), false); // Retrieve available formats for single file or many file archives int nbFiles = files.size(); this.formats = Archiver.getFormats(nbFiles > 1 || (nbFiles > 0 && files.elementAt(0).isDirectory())); int nbFormats = formats.length; ArchiveFormat initialFormat = formats[0]; // this value will only be used if last format is not available int initialFormatIndex = 0; // this value will only be used if last format is not available for (int i = 0; i(); for (ArchiveFormat format : formats) { formatsComboBox.addItem(format.name); } formatsComboBox.setSelectedIndex(lastFormatIndex); formatsComboBox.addItemListener(this); tempPanel.add(formatsComboBox); YBoxPanel mainPanel = getMainPanel(); mainPanel.add(tempPanel); mainPanel.addSpace(10); // Comment area, enabled only if selected archive format has comment support mainPanel.add(new JLabel(i18n("comment"))); commentArea = new JTextArea(); commentArea.setRows(4); mainPanel.add(commentArea); cbBackgroundMode = new JCheckBox(i18n("destination_dialog.background_mode")); cbBackgroundMode.setSelected(enableBackgroundMode); mainPanel.add(cbBackgroundMode); } ////////////////////////////////////////////// // TransferDestinationDialog implementation // ////////////////////////////////////////////// @Override protected PathFieldContent computeInitialPath(FileSet files) { String initialPath = mainFrame.getInactivePanel().getCurrentFolder().getAbsolutePath(true); AbstractFile file; String fileName; // Computes the archive's default name: // - if it only contains one file, uses that file's name. // - if it contains more than one file, uses the FileSet's parent folder's name. if (files.size() == 1) { file = files.elementAt(0); fileName = file.isDirectory() && !DesktopManager.isApplication(file) ?file.getName() :file.getNameWithoutExtension(); } else { file = files.getBaseFolder(); fileName = file.isRoot() ? "" : DesktopManager.isApplication(file) ? file.getNameWithoutExtension() : file.getName(); } return new PathFieldContent(initialPath + fileName + "." + lastFormat.ext, initialPath.length(), initialPath.length() + fileName.length()); } @Override protected TransferFileJob createTransferFileJob(ProgressDialog progressDialog, PathUtils.ResolvedDestination resolvedDest, int defaultFileExistsAction) { // Remember last format used, for next time this dialog is invoked lastFormat = formats[formatsComboBox.getSelectedIndex()]; return new ArchiveJob(progressDialog, mainFrame, files, resolvedDest.getDestinationFile(), lastFormat, Archiver.formatSupportsComment(lastFormat)?commentArea.getText():null); } @Override protected String getProgressDialogTitle() { return i18n("pack_dialog.packing"); } //////////////////////// // Overridden methods // //////////////////////// @Override protected boolean isValidDestination(PathUtils.ResolvedDestination resolvedDest, String destPath) { if (resolvedDest == null) { return false; } int destType = resolvedDest.getDestinationType(); return destType == PathUtils.ResolvedDestination.NEW_FILE || destType == PathUtils.ResolvedDestination.EXISTING_FILE; } ////////////////////////// // ItemListener methods // ////////////////////////// public void itemStateChanged(ItemEvent e) { FilePathField pathField = getPathField(); int newFormatIndex = formatsComboBox.getSelectedIndex(); // Updates the GUI if, and only if, the format selection has changed. if (lastFormatIndex != newFormatIndex) { String fileName = pathField.getText(); // Name of the destination archive file. String oldFormatExtension = formats[lastFormatIndex].ext; // Old/current format's extension if (fileName.endsWith("." + oldFormatExtension)) { // Saves the old selection. int selectionStart = pathField.getSelectionStart(); int selectionEnd = pathField.getSelectionEnd(); // Computes the new file name. fileName = fileName.substring(0, fileName.length() - oldFormatExtension.length()) + formats[newFormatIndex].ext; // Makes sure that the selection stays somewhat coherent. if (selectionEnd == pathField.getText().length()) { selectionEnd = fileName.length(); } // Resets the file path field. pathField.setText(fileName); pathField.setSelectionStart(selectionStart); pathField.setSelectionEnd(selectionEnd); } commentArea.setEnabled(Archiver.formatSupportsComment(formats[formatsComboBox.getSelectedIndex()])); lastFormatIndex = newFormatIndex; } // Transfer focus back to the text field pathField.requestFocus(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/PathFieldContent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import javax.swing.*; /** * This class wraps a path, and start and end offsets for the portion of the text to be selected in a text field. * * @author Maxence Bernard */ public class PathFieldContent { protected String path; protected int selectionStart; protected int selectionEnd; public PathFieldContent(String path) { this(path, 0, path.length()); } public PathFieldContent(String path, int selectionStart, int selectionEnd) { this.path = path; this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; } /** * Sets the given {@link JTextField}'s text, selection start and end with that contained by this * PathFieldContent. * * @param pathField instance of {@link JTextField} to update */ public void feedToPathField(JTextField pathField) { // Set the initial path pathField.setText(path); // Text is selected so that user can directly type and replace path pathField.setSelectionStart(selectionStart); pathField.setSelectionEnd(selectionEnd); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/ProgressDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.WindowEvent; import java.util.List; import java.util.Vector; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.UIManager; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import com.mucommander.ui.main.statusbar.TaskWidget; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.job.FileJob; import com.mucommander.job.FileJobListener; import com.mucommander.job.TransferFileJob; import com.mucommander.job.progress.JobProgress; import com.mucommander.job.progress.JobProgressListener; import com.mucommander.job.progress.JobProgressMonitor; import com.mucommander.utils.text.DurationFormat; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.button.ButtonChoicePanel; import com.mucommander.ui.button.CollapseExpandButton; import com.mucommander.ui.chooser.SizeChooser; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.statusbar.StatusBar; import static com.mucommander.job.FileJob.State; /** * This dialog informs the user of the progress made by a FileJob and allows to control it: pause/resume it, stop it, * limit transfer rate... * * @author Maxence Bernard */ public class ProgressDialog extends FocusDialog implements ActionListener, ItemListener, ChangeListener, FileJobListener, JobProgressListener { private static final Logger LOGGER = LoggerFactory.getLogger(ProgressDialog.class); // Button icons private final static String RESUME_ICON = "resume.png"; private final static String PAUSE_ICON = "pause.png"; private final static String SKIP_ICON = "skip.png"; private final static String STOP_ICON = "stop.png"; private final static String CURRENT_SPEED_ICON = "speed.png"; // Dialog width is constrained to 320, height is not an issue (always the same) private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(640, 10000); private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(410, 0); /** Height allocated to the 'speed graph' */ private final static int SPEED_GRAPH_HEIGHT = 80; private JLabel lblCurrentFile; private JLabel lblTotalTransferred; private JProgressBar totalProgressBar; private JProgressBar currentFileProgressBar; private JLabel lblCurrentSpeed; private JCheckBox cbLimitSpeed; private SizeChooser speedChooser; private JLabel lblElapsedTime; private SpeedGraph speedGraph; private CollapseExpandButton collapseExpandButton; private ButtonChoicePanel buttonsChoicePanel; private JButton btnPauseResume; private JButton btnSkip; private JButton btnStop; private JButton btnBackground; private JCheckBox cbCloseWhenFinished; // private JButton hideButton; private FileJob job; private TransferFileJob transferFileJob; private boolean firstTimeActivated = true; private TaskWidget taskWidget; private final MainFrame mainFrame; static { // Disable JProgressBar animation which is a real CPU hog under Mac OS X UIManager.put("ProgressBar.repaintInterval", Integer.MAX_VALUE); } public ProgressDialog(MainFrame mainFrame, String title, TaskWidget taskWidget) { super(mainFrame.getJFrame(), title, mainFrame.getJFrame()); this.taskWidget = taskWidget; this.mainFrame = mainFrame; // Sets maximum and minimum dimensions for this dialog setMaximumSize(MAXIMUM_DIALOG_DIMENSION); setMinimumSize(MINIMUM_DIALOG_DIMENSION); if (taskWidget != null) { taskWidget.setProgressDialog(this); taskWidget.setText(getTitle()); } // setResizable(false); } public ProgressDialog(MainFrame mainFrame, String title) { this(mainFrame, title, null); } private void initUi() { Container contentPane = getContentPane(); totalProgressBar = new JProgressBar(); totalProgressBar.setStringPainted(true); totalProgressBar.setAlignmentX(LEFT_ALIGNMENT); lblCurrentFile = new JLabel(job.getStatusString()); lblCurrentFile.setAlignmentX(LEFT_ALIGNMENT); YBoxPanel yPanel = new YBoxPanel(); // 2 progress bars if (transferFileJob != null) { yPanel.add(lblCurrentFile); currentFileProgressBar = new JProgressBar(); currentFileProgressBar.setStringPainted(true); yPanel.add(currentFileProgressBar); yPanel.addSpace(10); lblTotalTransferred = new JLabel(i18n("progress_dialog.starting")); yPanel.add(lblTotalTransferred); yPanel.add(totalProgressBar); } // Single progress bar else { yPanel.add(lblCurrentFile); yPanel.add(totalProgressBar); } yPanel.addSpace(10); lblElapsedTime = new JLabel(i18n("progress_dialog.elapsed_time")+": "); lblElapsedTime.setIcon(IconManager.getIcon(IconManager.IconSet.STATUS_BAR, StatusBar.WAITING_ICON)); yPanel.add(lblElapsedTime); if (transferFileJob != null) { JPanel tempPanel = new JPanel(new BorderLayout()); lblCurrentSpeed = new JLabel(); updateCurrentSpeedLabel(""); lblCurrentSpeed.setIcon(IconManager.getIcon(IconManager.IconSet.PROGRESS, CURRENT_SPEED_ICON)); tempPanel.add(lblCurrentSpeed, BorderLayout.WEST); YBoxPanel advancedPanel = new YBoxPanel(); speedGraph = new SpeedGraph(); speedGraph.setPreferredSize(new Dimension(0, SPEED_GRAPH_HEIGHT)); advancedPanel.add(speedGraph); advancedPanel.addSpace(5); JPanel tempPanel2 = new JPanel(new BorderLayout()); cbLimitSpeed = new JCheckBox(i18n("progress_dialog.limit_speed")+":", false); cbLimitSpeed.addItemListener(this); tempPanel2.add(cbLimitSpeed, BorderLayout.WEST); speedChooser = new SizeChooser(true); speedChooser.setEnabled(false); speedChooser.addChangeListener(this); tempPanel2.add(speedChooser, BorderLayout.EAST); advancedPanel.add(tempPanel2); advancedPanel.addSpace(5); // panel height can't be greater than speedChooser height tempPanel2.setMaximumSize(new Dimension(tempPanel2.getMaximumSize().width, speedChooser.getPreferredSize().height)); collapseExpandButton = new CollapseExpandButton(i18n("progress_dialog.advanced"), advancedPanel, true); collapseExpandButton.setExpandedState(TcConfigurations.getPreferences().getVariable(TcPreference.PROGRESS_DIALOG_EXPANDED, TcPreferences.DEFAULT_PROGRESS_DIALOG_EXPANDED)); tempPanel.add(collapseExpandButton, BorderLayout.EAST); // panel height can't be greater than collapseExpandButton height tempPanel.setMaximumSize(new Dimension(tempPanel.getMaximumSize().width, collapseExpandButton.getPreferredSize().height)); yPanel.add(tempPanel); yPanel.addSpace(5); yPanel.add(advancedPanel); } cbCloseWhenFinished = new JCheckBox(i18n("progress_dialog.close_when_finished")); cbCloseWhenFinished.setSelected(TcConfigurations.getPreferences().getVariable(TcPreference.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED, TcPreferences.DEFAULT_PROGRESS_DIALOG_CLOSE_WHEN_FINISHED)); yPanel.add(cbCloseWhenFinished); // yPanel.add(Box.createVerticalGlue()); contentPane.add(yPanel, BorderLayout.CENTER); btnPauseResume = new JButton(i18n("pause"), IconManager.getIcon(IconManager.IconSet.PROGRESS, PAUSE_ICON)); btnPauseResume.addActionListener(this); if (transferFileJob != null) { btnSkip = new JButton(i18n("skip"), IconManager.getIcon(IconManager.IconSet.PROGRESS, SKIP_ICON)); btnSkip.addActionListener(this); } btnStop = new JButton(i18n("stop"), IconManager.getIcon(IconManager.IconSet.PROGRESS, STOP_ICON)); btnStop.addActionListener(this); btnBackground = new JButton(i18n("progress_dialog.hide")); btnBackground.addActionListener(this); this.buttonsChoicePanel = new ButtonChoicePanel( btnSkip == null ? new JButton[] {btnPauseResume, btnStop, btnBackground} : new JButton[] {btnPauseResume, btnSkip, btnStop, btnBackground}, 0, getRootPane()); contentPane.add(buttonsChoicePanel, BorderLayout.SOUTH); // Cancel button receives initial focus setInitialFocusComponent(btnStop); // Enter triggers cancel button getRootPane().setDefaultButton(btnStop); } public void start(FileJob job) { this.job = job; // Listen to job state changes job.addFileJobListener(this); if (job instanceof TransferFileJob) { this.transferFileJob = (TransferFileJob) job; } initUi(); JobProgressMonitor.getInstance().addJob(job); JobProgressMonitor.getInstance().addJobProgressListener(this); if (taskWidget == null) { showDialog(); } else { firstTimeActivated = false; this.job.start(); } } /** * Stops repaint thread. */ public void stop() { JobProgressMonitor.getInstance().removeJobProgressListener(this); } // /** // * This method is called by the registered FileJob starts each time a new file is being processed. // */ // public void notifyCurrentFileChanged() { // // Update current file label // currentFileLabel.setText(job.getStatusString()); // } private void updateThroughputLimit() { transferFileJob.setThroughputLimit(cbLimitSpeed.isSelected() ? speedChooser.getValue() : -1); } private void updateCurrentSpeedLabel(String value) { lblCurrentSpeed.setText(i18n("progress_dialog.current_speed") + ": " + value); } @Override public void jobStateChanged(FileJob source, State oldState, State newState) { LOGGER.debug("currentThread="+Thread.currentThread()+" oldState="+oldState+" newState="+newState); if (newState == State.INTERRUPTED) { // Stop repaint thread and dispose dialog stop(); stopBackground(); dispose(); } else if (newState == State.FINISHED) { stopBackground(); // Dispose dialog only if 'Close when finished option' is selected if (cbCloseWhenFinished.isSelected()) { // Stop repaint thread and dispose dialog stop(); dispose(); // If not, disable components to indicate that the job is finished and leave the dialog open } else { // Repaint thread should not be stopped now, it will die naturally after updating labels and progress // bars to indicate that the job is finished // Change 'Stop' button's label to 'Close' btnStop.setText(i18n("close")); // Disable components btnPauseResume.setEnabled(false); if (transferFileJob != null) { btnSkip.setEnabled(false); cbLimitSpeed.setEnabled(false); speedChooser.setEnabled(false); } } } else if (newState == State.PAUSED) { btnPauseResume.setText(i18n("resume")); btnPauseResume.setIcon(IconManager.getIcon(IconManager.IconSet.PROGRESS, RESUME_ICON)); // Update buttons mnemonics buttonsChoicePanel.updateMnemonics(); if (transferFileJob != null) { updateCurrentSpeedLabel("N/A"); } } else if (newState == State.RUNNING) { btnPauseResume.setText(i18n("pause")); btnPauseResume.setIcon(IconManager.getIcon(IconManager.IconSet.PROGRESS, PAUSE_ICON)); // Update buttons mnemonics buttonsChoicePanel.updateMnemonics(); } } // Refresh current file label in a separate thread, more frequently than other components to give a sense // of speed when small files are being transferred. // This 'pull' approach allows to throttle the number label updates which have a cost VS updating the label // for each file being processed (job notifications) which can hog the CPU when lots of small files // are being transferred. private void updateProgressLabel(JobProgress progress) { lblCurrentFile.setText(progress.getJobStatusString()); } private void updateProgressUI(JobProgress progress) { if (progress.isTransferFileJob()) { currentFileProgressBar.setValue(progress.getFilePercentInt()); currentFileProgressBar.setString(progress.getFileProgressText()); // Update total transferred label lblTotalTransferred.setText( i18n("progress_dialog.transferred", SizeFormat.format(progress.getBytesTotal(), SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_LONG | SizeFormat.ROUND_TO_KB), SizeFormat.format(progress.getTotalBps(), SizeFormat.UNIT_SPEED | SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_SHORT | SizeFormat.ROUND_TO_KB)) ); // Add new immediate bytes per second speed sample to speed graph and label and repaint it // Skip this sample if job was paused and resumed, speed would not be accurate if (progress.getLastTime() > progress.getJobPauseStartDate()) { speedGraph.addSample(progress.getCurrentBps()); updateCurrentSpeedLabel(SizeFormat.format(progress.getCurrentBps(), SizeFormat.UNIT_SPEED| SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_SHORT)); } } totalProgressBar.setValue(progress.getTotalPercentInt()); totalProgressBar.setString(progress.getTotalProgressText()); if (taskWidget != null) { taskWidget.setProgress(progress.getTotalPercentInt()); } // Update elapsed time label lblElapsedTime.setText(i18n("progress_dialog.elapsed_time")+": "+DurationFormat.format(progress.getEffectiveJobTime())); } @Override public void jobAdded(FileJob source, int idx) { // nothing here } @Override public void jobRemoved(FileJob source, int idx) { // nothing here } @Override public void jobProgress(FileJob source, int idx, boolean fullUpdate) { if (job.equals(source)) { updateProgressLabel(source.getJobProgress()); if (fullUpdate) { updateProgressUI(source.getJobProgress()); } } } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnStop) { stopJob(); } else if (source == btnSkip) { transferFileJob.skipCurrentFile(); } else if (source == btnPauseResume) { // Pause/resume job job.setPaused(job.getState() != State.PAUSED); } else if (source == btnBackground) { hideToBackground(); } // else if(source==hideButton) { // mainFrame.setState(Frame.ICONIFIED); // } } private void stopJob() { State jobState = job.getState(); // Case when 'Close when finished' isn't selected, stop button's action becomes 'Close' if (jobState == State.FINISHED || jobState == State.INTERRUPTED) { dispose(); } else { job.interrupt(); } } private void hideToBackground() { if (taskWidget == null) { taskWidget = new TaskWidget(); taskWidget.setText(getTitle()); taskWidget.setProgressDialog(this); mainFrame.getStatusBar().getTaskPanel().addTask(taskWidget); mainFrame.getStatusBar().revalidate(); mainFrame.getStatusBar().repaint(); } taskWidget.setVisible(true); setVisible(false); } @Override public void itemStateChanged(ItemEvent e) { Object source = e.getSource(); if (source == cbLimitSpeed) { boolean isEnabled = cbLimitSpeed.isSelected(); speedChooser.setEnabled(isEnabled); updateThroughputLimit(); } } @Override public void stateChanged(ChangeEvent e) { if (e.getSource() == speedChooser) { updateThroughputLimit(); } } @Override public void windowActivated(WindowEvent e) { // This method is called each time the dialog is activated super.windowActivated(e); if (firstTimeActivated) { firstTimeActivated = false; this.job.start(); } } @Override public void windowClosed(WindowEvent e) { super.windowClosed(e); // Stop repaint thread if it isn't already stop(); // Stop job if it isn't already State jobState = job.getState(); if (jobState != State.FINISHED && jobState != State.INTERRUPTED) { job.interrupt(); } // Remember 'advanced panel' expanded state if (collapseExpandButton != null) { TcConfigurations.getPreferences().setVariable(TcPreference.PROGRESS_DIALOG_EXPANDED, collapseExpandButton.getExpandedState()); } // Remember 'close window when finished' option state TcConfigurations.getPreferences().setVariable(TcPreference.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED, cbCloseWhenFinished.isSelected()); } private void stopBackground() { if (taskWidget != null) { taskWidget.removeFromPanel(); } } /** * Transfer speed graph. */ private class SpeedGraph extends JPanel { private final Color GRAPH_OUTLINE_COLOR = new Color(70, 70, 70); private final Color GRAPH_FILL_COLOR = new Color(215, 215, 215); private final Color BPS_LIMIT_COLOR = new Color(204, 0, 0); private static final int LINE_SPACING = 6; private static final int NB_SAMPLES_MAX = 320; private static final int STROKE_WIDTH = 1; private final List samples = new Vector<>(NB_SAMPLES_MAX); private final Stroke lineStroke = new BasicStroke(STROKE_WIDTH); private SpeedGraph() { } private void addSample(long bytesPerSecond) { synchronized(samples) { // Ensures that paint() is not currently accessing the Vector // Capacity reached, remove first sample if (samples.size() == NB_SAMPLES_MAX) { samples.removeFirst(); } // Add sample to the vector samples.add(bytesPerSecond); } repaint(); } @Override public void paint(Graphics g) { Graphics2D g2d = (Graphics2D)g; // Enable antialiasing, looks way better g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int width = getWidth(); int height = getHeight(); // Fill the background with the panel's background color g.setColor(getBackground()); g.fillRect(0, 0, width, height); g2d.setStroke(lineStroke); synchronized(samples) { // Ensures that addSample() is not currently accessing the Vector // Number of collected sample int nbSamples = samples.size(); // Number of displayable samples based on their spacing int nbDisplayableSamples = (width-2*STROKE_WIDTH)/LINE_SPACING; // Index of the first sample int firstSample = nbSamples>nbDisplayableSamples?nbSamples-nbDisplayableSamples:0; // Number of lines to be drawn int nbLines = Math.min(nbSamples, nbDisplayableSamples); // Calculate the maximum bytes per second of all the samples to be displayed long maxBps = 0; for (int i = firstSample; i < firstSample+nbLines; i++) { long sample = samples.get(i); if (sample > maxBps) { maxBps = sample; } } // Y-scale projection ratio, leave some space on both sides of the graph float yRatio = maxBps/((float)height-2*STROKE_WIDTH); // Draw throughput limit as an horizontal line, only if there is a limit long bpsLimit = transferFileJob.getThroughputLimit(); if (bpsLimit > 0) { g.setColor(BPS_LIMIT_COLOR); int y = height-STROKE_WIDTH - (int)(bpsLimit/yRatio); g.drawLine(0, y, width, y); } // Fill the graph g.setColor(GRAPH_FILL_COLOR); int x = STROKE_WIDTH; Polygon p = new Polygon(); int sampleOffset = firstSample; for (int l = 0; l < nbLines; l++) { p.addPoint(x, height-STROKE_WIDTH-(int)(samples.get(sampleOffset++) /yRatio)); x += LINE_SPACING; } p.addPoint(x-LINE_SPACING, height-1); p.addPoint(0, height-1); g.fillPolygon(p); // Draw the graph outline in a darker color g.setColor(GRAPH_OUTLINE_COLOR); x = STROKE_WIDTH; sampleOffset = firstSample; for (int l = 0; l < nbLines-1; l++) { g.drawLine(x, height-STROKE_WIDTH-(int)(samples.get(sampleOffset) /yRatio), (x+=LINE_SPACING), height-STROKE_WIDTH-(int)(samples.get(++sampleOffset) /yRatio)); } // Draw an horizontal line at the bottom of the graph g.drawLine(0, height-1,width-1, height-1); // Unsuccessful rendering test using curves // GeneralPath gp = new GeneralPath(); // int x = STROKE_WIDTH; // gp.moveTo(x, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset++)).longValue()/yRatio)); // x += LINE_SPACING; // for(int l=1; l. */ package com.mucommander.ui.dialog.file; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.List; import javax.swing.*; import javax.swing.border.EmptyBorder; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.OSXFileUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.commons.util.Pair; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.FileJob; import com.mucommander.job.PropertiesJob; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowFilePropertiesAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FileLabel; import com.mucommander.ui.text.MultiLineLabel; import com.mucommander.utils.Convert; /** * This dialog shows properties of a file or a group of files : number of files, file kind, * combined size and location. * * @author Maxence Bernard */ public class PropertiesDialog extends FocusDialog implements Runnable, ActionListener { private final PropertiesJob job; private Thread repaintThread; private final SpinningDial dial; private final JTextField textfield; private final JLabel lblCounter; private final JLabel lblSize; private JLabel ownerLabel; private JLabel groupLabel; private final JLabel lblLastMod; private final JLabel lblCreateTime; private final JLabel lblLastAccess; AbstractFile file; SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); private final JButton btnOkCancel; private String newName; private JTextField edtNewName; // Dialog width is constrained to 320, height is not an issue (always the same) private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(360, 0); private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(1024, 800); /** How often should progress information be refreshed (in ms) */ private final static int REFRESH_RATE = 500; /** Dimension of the large file icon displayed on left side of the dialog */ private final static Dimension ICON_DIMENSION = new Dimension(64, 64); public PropertiesDialog(MainFrame mainFrame, FileSet files) { super(mainFrame.getJFrame(), files.size() > 1 ? ActionProperties.getActionLabel(ShowFilePropertiesAction.Descriptor.ACTION_ID) : i18n("properties_dialog.file_properties", files.elementAt(0).getName()), mainFrame.getJFrame()); this.job = new PropertiesJob(files, mainFrame); Container contentPane = getContentPane(); JPanel fileDetailsPanel = new JPanel(new BorderLayout()); Icon icon; boolean isSingleFile = files.size()==1; AbstractFile singleFile = isSingleFile?files.elementAt(0):null; if (isSingleFile) { icon = FileIcons.getFileIcon(singleFile, ICON_DIMENSION); } else { ImageIcon imageIcon = IconManager.getIcon(IconManager.IconSet.COMMON, "many_files.png"); icon = IconManager.getScaledIcon(imageIcon, (float)ICON_DIMENSION.getWidth()/imageIcon.getIconWidth()); } JLabel iconLabel = new JLabel(icon); iconLabel.setVerticalAlignment(JLabel.TOP); iconLabel.setBorder(new EmptyBorder(0, 0, 0, 8)); fileDetailsPanel.add(iconLabel, BorderLayout.WEST); XAlignedComponentPanel labelPanel = new XAlignedComponentPanel(10); // Name of file for renaming file = files.elementAt(0); textfield = new JTextField(); labelPanel.addRow("Name" + ":", textfield, 6); textfield.setText(file.getName()); textfield.addActionListener(this); textfield.setEditable(true); // Contents (set later) lblCounter = new JLabel(""); labelPanel.addRow(i18n("properties_dialog.contents")+":", lblCounter, 6); // Location (set here) labelPanel.addRow(i18n("location")+":", new FileLabel(files.getBaseFolder(), true), 6); // Combined size (set later) JPanel sizePanel; sizePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); sizePanel.add(lblSize = new JLabel("")); sizePanel.add(new JLabel(dial = new SpinningDial())); labelPanel.addRow(i18n("size") + ":", sizePanel, 6); // more information lblLastMod = new JLabel(""); labelPanel.addRow("Last Modified" + ":", lblLastMod, 6); lblCreateTime = new JLabel(""); labelPanel.addRow("Created" + ":", lblCreateTime, 6); lblLastAccess = new JLabel(""); labelPanel.addRow("Last Accessed" + ":", lblLastAccess, 6); if (isSingleFile) { if (singleFile.canGetOwner()) { String owner = singleFile.getOwner(); if (owner != null) { labelPanel.addRow(i18n("owner") + ":", new JLabel(owner), 6); } } if (singleFile.canGetGroup()) { String group = singleFile.getGroup(); if (group != null) { labelPanel.addRow(i18n("group") + ":", new JLabel(group), 6); } } if (singleFile.isFileOperationSupported(FileOperation.GET_REPLICATION)) { short replication; try { replication = singleFile.getReplication(); labelPanel.addRow(i18n("replication") + ":", new JLabel(Short.toString(replication)), 6); } catch (UnsupportedFileOperationException e) { e.printStackTrace(); } } if (singleFile.isFileOperationSupported(FileOperation.GET_BLOCKSIZE)) { long blocksize; try { blocksize = singleFile.getBlocksize(); labelPanel.addRow(i18n("blocksize") + ":", new JLabel(Convert.readableFileSize(blocksize)), 6); } catch (UnsupportedFileOperationException e) { e.printStackTrace(); } } } if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_4.isCurrentOrHigher() && isSingleFile && singleFile.hasAncestor(LocalFile.class)) { String comment = OSXFileUtils.getSpotlightComment(singleFile); JLabel commentLabel = new JLabel(i18n("comment")+":"); commentLabel.setAlignmentY(JLabel.TOP_ALIGNMENT); commentLabel.setVerticalAlignment(SwingConstants.TOP); labelPanel.addRow(commentLabel, new MultiLineLabel(comment), 6); } if (isSingleFile && singleFile.hasAncestor(LocalFile.class)) { List> infos = DesktopManager.getExtendedFileProperties(singleFile); infos.forEach(info -> labelPanel.addRow(info.first, info.second, 6)); } updateLabels(); fileDetailsPanel.add(labelPanel, BorderLayout.CENTER); YBoxPanel yPanel = new YBoxPanel(5); yPanel.add(fileDetailsPanel); contentPane.add(yPanel, BorderLayout.NORTH); btnOkCancel = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKPanel(btnOkCancel, getRootPane(), this), BorderLayout.SOUTH); // OK button will receive initial focus setInitialFocusComponent(btnOkCancel); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); start(); } private void updateLabels() { int nbFiles = job.getNbFilesRecurse(); int nbFolders = job.getNbFolders(); lblCounter.setText( (nbFiles > 0 ? i18n("nb_files", ""+nbFiles) : "") +(nbFiles > 0 && nbFolders > 0 ? ", ":"") +(nbFolders > 0 ? i18n("nb_folders", ""+nbFolders):"") ); lblSize.setText(SizeFormat.format(job.getTotalBytes(), SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_LONG | SizeFormat.INCLUDE_SPACE| SizeFormat.ROUND_TO_KB) + " (" + SizeFormat.format(job.getTotalBytes(), SizeFormat.DIGITS_FULL | SizeFormat.UNIT_LONG | SizeFormat.INCLUDE_SPACE) + ")"); //adding last modification time and date lblLastMod.setText(sdf.format(file.getLastModifiedDate())); // BasicFileAttributes attr = Files.readAttributes(Paths.get(file.getAbsolutePath())), // BasicFileAttributes.class); // createTimeLabel.setText(attr.creationTime().toString()); // // lastacces.setText(attr.lastAccessTime().toString()); long lastAccessTime; try { lastAccessTime = file.getLastAccessDate(); } catch (IOException e) { lastAccessTime = -1; } lblLastAccess.setText(lastAccessTime > 0 ? sdf.format(lastAccessTime) : i18n("unknown")); long createTime; try { createTime = file.getCreationDate(); } catch (IOException e) { createTime = -1; } lblCreateTime.setText(createTime > 0 ? sdf.format(createTime) : i18n("unknown")); lblCounter.repaint(REFRESH_RATE); lblSize.repaint(REFRESH_RATE); } protected AbstractFile createDestinationFile(AbstractFile destFolder, String destFileName) { AbstractFile destFile; do { // Loop for retry try { destFile = destFolder.getDirectChild(destFileName); break; } catch (IOException ignore) {} } while (true); return destFile; } static void renameFile(AbstractFile destFile, String newName){ AbstractFile destination = FileFactory.getFile(destFile.getParent()+"/"+newName); try { destFile.moveTo(destination); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void start() { job.start(); repaintThread = new Thread(this, "com.mucommander.ui.dialog.file.PropertiesDialog's Thread"); repaintThread.start(); } @Override public void run() { dial.setAnimated(true); while (repaintThread != null && job.getState()!= FileJob.State.FINISHED) { updateLabels(); try { Thread.sleep(REFRESH_RATE); } catch(InterruptedException ignore) {} } // Updates button labels and stops spinning dial. updateLabels(); btnOkCancel.setText(i18n("ok")); dial.setAnimated(false); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == btnOkCancel) { renameFile(file,textfield.getText()); } dispose(); } @Override public void windowClosed(WindowEvent e) { super.windowClosed(e); // Stop threads job.interrupt(); repaintThread = null; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/SplitFileDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.job.SplitFileJob; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.SplitFileAction; import com.mucommander.ui.combobox.EditableComboBox; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.text.DecimalFormat; import java.text.NumberFormat; /** * Dialog used to split a file into several parts. * * @author Mariusz Jakubowski */ public class SplitFileDialog extends JobDialog implements ActionListener { private static final int MAX_PARTS = 100; private static final String[] unitNames = new String[] { i18n("unit.bytes_short").toLowerCase(), i18n("unit.kb").toLowerCase(), i18n("unit.mb").toLowerCase(), i18n("unit.gb").toLowerCase() }; private static final int[] unitBytes = new int[] { 1, 1024, 1024*1024, 1024*1024*1024 }; private final static DecimalFormat DECIMAL_FORMAT = (DecimalFormat)NumberFormat.getInstance(); static { DECIMAL_FORMAT.setGroupingUsed(false); } private String MSG_AUTO = i18n("split_file_dialog.auto"); private AbstractFile file; private AbstractFile destFolder; private JButton btnSplit; private JButton btnClose; private FilePathField edtTargetDirectory; private JCheckBox cbGenerateCRC; private JTextField edtSize; private JSpinner spnParts; private boolean edtChange; /** * Creates a new split file dialog. * @param mainFrame the main frame * @param file a file to split * @param destFolder default destination folder */ public SplitFileDialog(MainFrame mainFrame, AbstractFile file, AbstractFile destFolder) { super(mainFrame, ActionProperties.getActionLabel(SplitFileAction.Descriptor.ACTION_ID), new FileSet()); this.file = file; this.destFolder = destFolder; initialize(); } /** * Initializes the dialog. */ protected void initialize() { Container content = getContentPane(); content.setLayout(new BorderLayout(0, 5)); XAlignedComponentPanel pnlMain = new XAlignedComponentPanel(10); pnlMain.addRow(i18n("split_file_dialog.file_to_split") + ":", new JLabel(file.getName()), 0); String size = SizeFormat.format(file.getSize(), SizeFormat.DIGITS_FULL | SizeFormat.UNIT_LONG | SizeFormat.INCLUDE_SPACE); pnlMain.addRow(i18n("size") + ":", new JLabel(size), 10); edtTargetDirectory = new FilePathField(destFolder.getAbsolutePath(), 40); pnlMain.addRow(i18n("split_file_dialog.target_directory") + ":", edtTargetDirectory, 5); XBoxPanel pnlSize = new XBoxPanel(); String[] sizes = new String[] { MSG_AUTO, "10 " + i18n("unit.mb"), "100 " + i18n("unit.mb"), "250 " + i18n("unit.mb"), "650 " + i18n("unit.mb"), "700 " + i18n("unit.mb") }; edtSize = new JTextField(); EditableComboBox cbSize = new EditableComboBox<>(edtSize, sizes); cbSize.setComboSelectionUpdatesTextField(true); cbSize.setSelectedIndex(1); edtSize.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { updatePartsNumber(); } }); cbSize.addComboBoxListener(source -> updatePartsNumber()); pnlSize.add(cbSize); pnlSize.addSpace(10); pnlSize.add(new JLabel(i18n("split_file_dialog.parts") + ":")); pnlSize.addSpace(5); spnParts = new JSpinner(new SpinnerNumberModel(1, 1, file.getSize(), 1)); spnParts.addChangeListener(e -> { if (!edtChange) { long parts = ((Number)spnParts.getValue()).longValue(); long newSize = file.getSize() / parts; if (file.getSize() % parts != 0) { newSize++; } if (getBytes() != newSize) { edtSize.setText(Long.toString(newSize)); } } }); pnlSize.add(spnParts); pnlMain.addRow(i18n("split_file_dialog.part_size") + ":", pnlSize, 0); cbGenerateCRC = new JCheckBox(i18n("split_file_dialog.generate_CRC")); cbGenerateCRC.setSelected(true); pnlMain.addRow("", cbGenerateCRC, 0); content.add(pnlMain, BorderLayout.CENTER); content.add(getPnlButtons(), BorderLayout.SOUTH); getRootPane().setDefaultButton(btnSplit); updatePartsNumber(); } /** * Creates bottom panel with buttons. */ private JPanel getPnlButtons() { btnSplit = new JButton(i18n("split")); btnClose = new JButton(i18n("cancel")); return DialogToolkit.createOKCancelPanel(btnSplit, btnClose, getRootPane(), this); } /** * Executes the split job. */ private void startJob() { long size = getBytes(); if (size < 1) { return; } String destPath = edtTargetDirectory.getText(); PathUtils.ResolvedDestination resolvedDest = PathUtils.resolveDestination(destPath, mainFrame.getActivePanel().getCurrentFolder()); // The path entered doesn't correspond to any existing folder if (resolvedDest == null || (files.size() > 1 && resolvedDest.getDestinationType() != PathUtils.ResolvedDestination.EXISTING_FOLDER)) { showErrorDialog(i18n("invalid_path", destPath), i18n("split_file_dialog.error_title")); return; } long parts = getParts(); if (parts > MAX_PARTS) { showErrorDialog(i18n("split_file_dialog.max_parts", Integer.toString(MAX_PARTS)), i18n("split_file_dialog.error_title")); return; } ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("progress_dialog.processing_files")); SplitFileJob job = new SplitFileJob(progressDialog, mainFrame, file, resolvedDest.getDestinationFolder(), size, (int)parts); job.setIntegrityCheckEnabled(cbGenerateCRC.isSelected()); progressDialog.start(job); } /** * Returns number of bytes entered in "Bytes per part" control. * @return number of bytes entered in "Bytes per part" control. */ private long getBytes() { String strVal = edtSize.getText().trim(); if (MSG_AUTO.equals(strVal)) { return file.getSize(); } String[] strArr = strVal.split(" "); if (strArr.length < 1 || strArr.length > 2) { return -1; } int unit = 1; if (strArr.length == 2) { unit = -1; strArr[1] = strArr[1].toLowerCase(); for (int i = 0; i < unitNames.length; i++) { if (unitNames[i].equals(strArr[1])) { unit = unitBytes[i]; break; } } if (unit == -1) { return -1; } } try { double size = DECIMAL_FORMAT.parse(strArr[0]).doubleValue(); size *= unit; return (long) size; } catch (Exception e) { return -1; } } /** * Returns number of parts this file will be splitted. * @return number of parts this file will be splitted. */ private long getParts() { long size = getBytes(); if (size < 1) { return -1; } return (long) Math.ceil((double)file.getSize() / (double)size); } /** * Updates number of parts displayed in "Parts" control based on * "Bytes per part" control. */ private void updatePartsNumber() { long parts = getParts(); if (parts < 1) { return; } edtChange = true; spnParts.setValue(parts); edtChange = false; } // ///////////////////////////////// // ActionListener implementation // // ///////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnClose) { dispose(); } else if (source == btnSplit) { dispose(); startJob(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/TransferDestinationDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.lang.reflect.InvocationTargetException; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.ui.main.statusbar.TaskWidget; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.job.TransferFileJob; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.FilePathField; /** * This class is an abstract dialog which allows the user to enter the destination of a transfer in a text field * and control some options such as the default action to perform when a file already exists in the destination, or * if the files should be checked for integrity. *

    * The initial path displayed in the text field is the one returned by {@link #computeInitialPath(FileSet)}. * When the dialog is confirmed by the user, either by pressing the 'OK' button or the 'Enter' key, the destination * path is resolved and checked with {@link #isValidDestination(PathUtils.ResolvedDestination, String)}. If the * path is a valid destination, a job instance is created using * {@link #createTransferFileJob(ProgressDialog, PathUtils.ResolvedDestination, int)} and started. If it isn't, * the user is notified with an error message. * * @author Maxence Bernard */ public abstract class TransferDestinationDialog extends JobDialog implements ActionListener, DocumentListener { /** * */ private static final Logger LOGGER = LoggerFactory.getLogger(TransferDestinationDialog.class); /** * Dialog size constraints */ protected final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(360, 0); /** * Dialog width should not exceed 360, height is not an issue (always the same) */ protected final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(400, 10000); static boolean enableBackgroundMode; protected String errorDialogTitle; private final boolean enableTransferOptions; private final YBoxPanel mainPanel; private final FilePathField pathField; private final SpinningDial spinningDial; private final JComboBox cbFileExistsAction = new TcComboBox<>(); private JCheckBox cbSkipErrors; private JCheckBox cbVerifyIntegrity; JCheckBox cbBackgroundMode; private final JButton btnOk; /** Background thread that is currently being executed, null if there is none. */ private Thread thread; /** * for background operations */ private TaskWidget taskWidget; private final static int[] DEFAULT_ACTIONS = { FileCollisionDialog.CANCEL_ACTION, FileCollisionDialog.SKIP_ACTION, FileCollisionDialog.OVERWRITE_ACTION, FileCollisionDialog.OVERWRITE_IF_OLDER_ACTION, FileCollisionDialog.RESUME_ACTION, FileCollisionDialog.RENAME_ACTION }; private final static String[] DEFAULT_ACTIONS_TEXT = { FileCollisionDialog.CANCEL_TEXT, FileCollisionDialog.SKIP_TEXT, FileCollisionDialog.OVERWRITE_TEXT, FileCollisionDialog.OVERWRITE_IF_OLDER_TEXT, FileCollisionDialog.RESUME_TEXT, FileCollisionDialog.RENAME_TEXT }; TransferDestinationDialog(MainFrame mainFrame, FileSet files, String title, String labelText, String okText, String errorDialogTitle, boolean enableTransferOptions) { super(mainFrame, title, files); this.errorDialogTitle = errorDialogTitle; this.enableTransferOptions = enableTransferOptions; mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(labelText+" :")); // create a path field with auto-completion capabilities pathField = new FilePathField(); JPanel borderPanel = new JPanel(new BorderLayout()); borderPanel.add(pathField, BorderLayout.CENTER); // Spinning dial displayed while I/O-bound operations are being performed in a separate thread spinningDial = new SpinningDial(false); borderPanel.add(new JLabel(spinningDial), BorderLayout.EAST); mainPanel.add(borderPanel); mainPanel.addSpace(10); pathField.getDocument().addDocumentListener(this); // Path field will receive initial focus setInitialFocusComponent(pathField); if (enableTransferOptions) { // Combo box that allows the user to choose the default action when a file already exists in destination mainPanel.add(new JLabel(i18n("destination_dialog.file_exists_action") + " :")); cbFileExistsAction.addItem(i18n("ask")); for (String s : DEFAULT_ACTIONS_TEXT) { cbFileExistsAction.addItem(s); } mainPanel.add(cbFileExistsAction); mainPanel.addSpace(10); cbSkipErrors = new JCheckBox(i18n("destination_dialog.skip_errors")); mainPanel.add(cbSkipErrors); cbVerifyIntegrity = new JCheckBox(i18n("destination_dialog.verify_integrity")); mainPanel.add(cbVerifyIntegrity); cbBackgroundMode = new JCheckBox(i18n("destination_dialog.background_mode")); cbBackgroundMode.setSelected(enableBackgroundMode); mainPanel.add(cbBackgroundMode); //mainPanel.addSpace(10); } getContentPane().add(mainPanel, BorderLayout.NORTH); // create file details button and OK/cancel buttons and lay them out a single row JPanel fileDetailsPanel = createFileDetailsPanel(); btnOk = new JButton(okText); // Prevent the dialog from being validated while the initial path is being set. setEnabledOkButtons(false); JButton cancelButton = new JButton(i18n("cancel")); YBoxPanel buttonsPanel = new YBoxPanel(10); buttonsPanel.add(createButtonsPanel(createFileDetailsButton(fileDetailsPanel), DialogToolkit.createButtonPanel(getRootPane(), this, btnOk, cancelButton) // DialogToolkit.createOKCancelPanel(okButton, btnCancel, getRootPane(), this) )); buttonsPanel.add(fileDetailsPanel); getContentPane().add(buttonsPanel, BorderLayout.SOUTH); // Set minimum/maximum dimension setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); // Dispose this dialog when the close window button is pressed setDefaultCloseOperation(DISPOSE_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { // Spawn a new thread that retrieves the initial path (I/O-bound) and sets the path field accordingly startThread(new InitialPathRetriever()); } @Override public void windowClosed(WindowEvent e) { // Interrupt any ongoing thread when the dialog has been closed, regardless of how it has been closed. interruptOngoingThread(); } }); } /** * Returns the main panel that contains the path field. * * @return the main panel that contains the path field. */ protected YBoxPanel getMainPanel() { return mainPanel; } /** * Returns the field where the destination path has to be entered. * * @return the field where the destination path has to be entered. */ FilePathField getPathField() { return pathField; } /** * Interrupts any ongoing thread and starts the given one. The spinning dial is set to 'animated'. * * @param thread the thread to start */ private synchronized void startThread(Thread thread) { // Interrupt any ongoing thread interruptOngoingThread(); // Spin the dial spinningDial.setAnimated(true); // Start the thread this.thread = thread; thread.start(); } /** * Interrupts the ongoing thread if there is one, does nothing otherwise. */ private synchronized void interruptOngoingThread() { if (thread != null) { LOGGER.trace("Calling interrupt() on " + thread); thread.interrupt(); // Set the current thread to null thread = null; } } /** * This method checks that the given resolved destination is valid. This implementation returns true * if the resolved destination is not null and, in case there is more than one file to process, if the * destination is a folder that exists. This method can safely be overridden by subclasses to change the behavior. *

    * Returning true will cause the job to go ahead and be started. Returning false will * pop up an error dialog that notifies the user that the path is incorrect. *

    * This method is called from a dedicated thread so that it can safely perform I/O operations without any chance * of locking the event thread. * * @param resolvedDest the resolved destination * @param destPath the path, as it was entered in the path field * @return true if the given resolved destination is valid */ protected boolean isValidDestination(PathUtils.ResolvedDestination resolvedDest, String destPath) { return (resolvedDest != null && (files.size() == 1 || resolvedDest.getDestinationType() == PathUtils.ResolvedDestination.EXISTING_FOLDER)); } /** * This method is called after the destination has been validated to start the job, with the resolved destination * that has been validated by {@link #isValidDestination(PathUtils.ResolvedDestination, String)}. * * @param resolvedDest the resolved destination */ private void startJob(PathUtils.ResolvedDestination resolvedDest) { int defaultFileExistsAction; boolean skipErrors; boolean verifyIntegrity; if (enableTransferOptions) { // Retrieve default action when a file exists in destination, default choice // (if not specified by the user) is 'Ask' defaultFileExistsAction = cbFileExistsAction.getSelectedIndex(); if (defaultFileExistsAction == 0) { defaultFileExistsAction = FileCollisionDialog.ASK_ACTION; } else { defaultFileExistsAction = DEFAULT_ACTIONS[defaultFileExistsAction - 1]; } // Note: we don't remember default action on purpose: we want the user to specify it each time, // it would be too dangerous otherwise. skipErrors = cbSkipErrors.isSelected(); verifyIntegrity = cbVerifyIntegrity.isSelected(); } else { defaultFileExistsAction = FileCollisionDialog.ASK_ACTION; skipErrors = false; verifyIntegrity = false; } ProgressDialog progressDialog = new ProgressDialog(mainFrame, getProgressDialogTitle(), taskWidget); if (getReturnFocusTo() != null) { progressDialog.returnFocusTo(getReturnFocusTo()); } TransferFileJob job = createTransferFileJob(progressDialog, resolvedDest, defaultFileExistsAction); if (job != null) { job.setAutoSkipErrors(skipErrors); job.setIntegrityCheckEnabled(verifyIntegrity); progressDialog.start(job); } } /** * Called when the path has changed while {@link InitialPathRetriever} is running. */ private void textUpdated() { synchronized(this) { if (thread != null && thread instanceof InitialPathRetriever) { // Interrupt InitialPathRetriever interruptOngoingThread(); // Enable setEnabledOkButtons(true); pathField.getDocument().removeDocumentListener(this); } } } @Override public void insertUpdate(DocumentEvent e) { textUpdated(); } @Override public void removeUpdate(DocumentEvent e) { textUpdated(); } @Override public void changedUpdate(DocumentEvent e) { } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnOk) { boolean backgroundMode = cbBackgroundMode != null && cbBackgroundMode.isSelected(); enableBackgroundMode = backgroundMode; // Disable the OK buttons and path field while the current path is being resolved setEnabledOkButtons(false); pathField.setEnabled(false); if (backgroundMode) { taskWidget = new TaskWidget(); //taskWidget.setText(okButton.getText()); mainFrame.getStatusBar().getTaskPanel().addTask(taskWidget); mainFrame.getStatusBar().revalidate(); mainFrame.getStatusBar().repaint(); } // Start resolving the path startThread(new PathResolver()); } else { // Cancel button dispose(); } } /** * Called when the dialog has just been created to compute the initial path, based on the user file selection. * *

    This method is called from a dedicated thread so that it can safely perform I/O operations without any chance * of locking the event thread. * * @param files files that were selected/marked by the user * @return a {@link PathFieldContent} containing the initial path to set in the path field */ protected abstract PathFieldContent computeInitialPath(FileSet files); /** * Called after the dialog has been confirmed by the user and the resolved destination has been * {@link #isValidDestination(PathUtils.ResolvedDestination, String) validated} to create the * {@link TransferFileJob} instance that will subsequently be started. * *

    This method is called from a dedicated thread so that it can safely perform I/O operations without any chance * of locking the event thread. * * @param progressDialog the progress dialog that will show the job's progression * @param resolvedDest the resolved and validated destination * @param defaultFileExistsAction the value of the 'default action when file exists' choice * @return the {@link TransferFileJob} instance that will subsequently be started */ protected abstract TransferFileJob createTransferFileJob(ProgressDialog progressDialog, PathUtils.ResolvedDestination resolvedDest, int defaultFileExistsAction); /** * Returns the title to be used in the progress dialog. * * @return the title to be used in the progress dialog. */ protected abstract String getProgressDialogTitle(); /** * Retrieves the initial path to be set in the path field by calling {@link TransferDestinationDialog#computeInitialPath(FileSet)}. * Since this operation can be I/O-bound, it is performed in a separate thread. */ private class InitialPathRetriever extends Thread { /** True if the thread has been interrupted */ private boolean interrupted; @Override public void run() { final PathFieldContent pathFieldContent = computeInitialPath(files); // Perform UI tasks in the AWT event thread try { SwingUtilities.invokeAndWait(() -> { spinningDial.setAnimated(false); if (!interrupted) { // Document change events are no longer needed pathField.getDocument().removeDocumentListener(TransferDestinationDialog.this); // Set the path field's text and selection pathFieldContent.feedToPathField(pathField); setEnabledOkButtons(true); } }); } catch (InterruptedException e) { LOGGER.trace("Interrupted", e); } catch (InvocationTargetException e) { LOGGER.debug("Caught exception", e); } // Set the current thread to null synchronized (TransferDestinationDialog.this) { if (thread == this) // This thread may have been interrupted already thread = null; } } /** * Overridden to trap interruptions ({@link #isInterrupted()} doesn't seem to be working as advertised). */ @Override public void interrupt() { super.interrupt(); this.interrupted = true; } } /** * Resolves the path entered in the path field into a {@link PathUtils.ResolvedDestination} instance and validates * it using {@link TransferDestinationDialog#isValidDestination(PathUtils.ResolvedDestination, String)}. * Since both of those operations can be I/O-bound, they are performed in a separate thread. *

    * If the destination is valid, the job is started using {@link TransferDestinationDialog#startJob(PathUtils.ResolvedDestination)} * and this dialog is disposed. Otherwise, a error dialog is displayed to notify the user that the path he has * entered is invalid and invite him to try again. */ private class PathResolver extends Thread { /** True if the thread has been interrupted */ private boolean interrupted; @Override public void run() { spinningDial.setAnimated(false); final String destPath = pathField.getText(); // Resolves destination folder (I/O bound) final PathUtils.ResolvedDestination resolvedDest = PathUtils.resolveDestination(destPath, mainFrame.getActivePanel().getCurrentFolder()); // Resolves destination folder (I/O bound) final boolean isValid = isValidDestination(resolvedDest, destPath); // Perform UI tasks in the AWT event thread SwingUtilities.invokeLater(() -> { if (interrupted) { dispose(); } else if (isValid) { dispose(); startJob(resolvedDest); } else { showErrorDialog(i18n("invalid_path", destPath), errorDialogTitle); // Re-enable the OK button and path field so that a new path can be entered setEnabledOkButtons(true); pathField.setEnabled(true); } }); // Set the current thread to null synchronized(TransferDestinationDialog.this) { if (thread == this) { // This thread may have been interrupted already thread = null; } } } /** * Overridden to trap interruptions ({@link #isInterrupted()} doesn't seem to be working as advertised). */ @Override public void interrupt() { super.interrupt(); this.interrupted = true; } } private void setEnabledOkButtons(boolean enabled) { btnOk.setEnabled(enabled); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/UnpackDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.file; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.job.TransferFileJob; import com.mucommander.job.UnpackJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.UnpackAction; import com.mucommander.ui.main.MainFrame; /** * Dialog that allows the user to choose the destination to unpack files to. * * @author Maxence Bernard */ public class UnpackDialog extends TransferDestinationDialog { /** * Creates and displays a new UnpackDialog. * * @param mainFrame the main frame this dialog is attached to * @param files the set of files to unpack */ public UnpackDialog(MainFrame mainFrame, FileSet files) { super(mainFrame, files, ActionProperties.getActionLabel(UnpackAction.Descriptor.ACTION_ID), i18n("unpack_dialog.destination"), i18n("unpack"), i18n("unpack_dialog.error_title"), true); } ////////////////////////////////////////////// // TransferDestinationDialog implementation // ////////////////////////////////////////////// @Override protected PathFieldContent computeInitialPath(FileSet files) { return new PathFieldContent(mainFrame.getInactivePanel().getCurrentFolder().getAbsolutePath(true)); } @Override protected TransferFileJob createTransferFileJob(ProgressDialog progressDialog, PathUtils.ResolvedDestination resolvedDest, int defaultFileExistsAction) { int destinationType = resolvedDest.getDestinationType(); if(destinationType==PathUtils.ResolvedDestination.EXISTING_FILE) { showErrorDialog(i18n("invalid_path", resolvedDest.getDestinationFile().getAbsolutePath())); return null; } return new UnpackJob( progressDialog, mainFrame, files, destinationType==PathUtils.ResolvedDestination.NEW_FILE?resolvedDest.getDestinationFile():resolvedDest.getDestinationFolder(), defaultFileExistsAction); } @Override protected String getProgressDialogTitle() { return i18n("unpack_dialog.unpacking"); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/file/package.html ================================================ Various dialogs used to display file related messages. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/help/ShortcutsDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.help; import com.mucommander.ui.action.ActionCategory; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowKeyboardShortcutsAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.text.KeyStrokeUtils; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.*; import java.util.List; /** * Dialog that displays shortcuts used in the application, sorted by topics. * * @author Maxence Bernard, Arik Hadas */ public class ShortcutsDialog extends FocusDialog implements ActionListener { private final static String QUICK_SEARCH_TITLE = "shortcuts_dialog.quick_search"; private final static Map QUICK_SEARCH_SHORTCUTS = new Hashtable<>() { { put("shortcuts_dialog.quick_search.start_search", ""); put("shortcuts_dialog.quick_search.jump_to_previous", "UP"); put("shortcuts_dialog.quick_search.jump_to_next", "DOWN"); put("shortcuts_dialog.quick_search.remove_last_char", "BACKSPACE"); put("shortcuts_dialog.quick_search.mark_jump_next", "INSERT"); put("shortcuts_dialog.quick_search.cancel_search", "ESCAPE"); } }; /** Comparator of actions according to their labels */ private static final Comparator ACTIONS_COMPARATOR = (id1, id2) -> { String label1 = ActionProperties.getActionLabel(id1); if (label1 == null) { return 1; } String label2 = ActionProperties.getActionLabel(id2); if (label2 == null) { return -1; } return label1.compareTo(label2); }; public ShortcutsDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(ShowKeyboardShortcutsAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); Container contentPane = getContentPane(); JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); // Separate the actions according to their categories. Map> categoryToItsActionsWithShortcutsIdsMap = createCategoryToItsActionsWithShortcutsMap(); //create tab and panel for each category for (ActionCategory category: categoryToItsActionsWithShortcutsIdsMap.keySet()) { // Get the list of actions from the above category which have shortcuts assigned to them LinkedList categoryActionsWithShortcuts = categoryToItsActionsWithShortcutsIdsMap.get(category); categoryActionsWithShortcuts.sort(ACTIONS_COMPARATOR); // If there is at least one action in the category with shortcuts assigned to it, add tab for the category if (!categoryActionsWithShortcuts.isEmpty()) { addTopic(tabbedPane, ""+category, categoryActionsWithShortcuts.iterator()); } } // create tab for quick-search category addTopic(tabbedPane, i18n(QUICK_SEARCH_TITLE), QUICK_SEARCH_SHORTCUTS); contentPane.add(tabbedPane, BorderLayout.CENTER); // Add an OK button JButton okButton = new JButton(i18n("ok")); contentPane.add(DialogToolkit.createOKPanel(okButton, getRootPane(), this), BorderLayout.SOUTH); // OK will be selected when enter is pressed getRootPane().setDefaultButton(okButton); // First tab will receive initial focus setInitialFocusComponent(tabbedPane); // Set a reasonable maximum size, scroll bars will be displayed if there is not enough space setMaximumSize(new Dimension(600, 360)); } private Map> createCategoryToItsActionsWithShortcutsMap() { // Hashtable that maps actions-category to LinkedList of actions (Ids) from the category that have shortcuts assigned to them Map> categoryToItsActionsWithShortcutsIdsMap = new Hashtable<>(); // Initialize empty LinkedList for each category for(ActionCategory category : ActionProperties.getActionCategories()) categoryToItsActionsWithShortcutsIdsMap.put(category, new LinkedList<>()); // Go over all action ids Iterator actionIds = ActionManager.getActionIds(); while (actionIds.hasNext()) { String actionId = actionIds.next(); ActionCategory category = ActionProperties.getActionCategory(actionId); // If the action has category and there is a primary shortcut assigned to it, add its id to the list of the category if (category != null && ActionKeymap.doesActionHaveShortcut(actionId)) { categoryToItsActionsWithShortcutsIdsMap.get(category).add(actionId); } } return categoryToItsActionsWithShortcutsIdsMap; } private void addTopic(JTabbedPane tabbedPane, String titleKey, Iterator descriptionsIterator) { XAlignedComponentPanel compPanel = new XAlignedComponentPanel(15); // Add all shortcuts and their description addShortcutList(compPanel, descriptionsIterator); // Panel needs to be vertically aligned to the top JPanel northPanel = new JPanel(new BorderLayout()); northPanel.add(compPanel, BorderLayout.NORTH); // Horizontal/vertical scroll bars will be displayed if needed JScrollPane scrollPane = new JScrollPane(northPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); scrollPane.setBorder(null); tabbedPane.addTab(titleKey, scrollPane); } private void addTopic(JTabbedPane tabbedPane, String titleKey, Map actionsToShortcutsMap) { XAlignedComponentPanel compPanel = new XAlignedComponentPanel(15); // Add all shortcuts and their description addShortcutList(compPanel, actionsToShortcutsMap); // Panel needs to be vertically aligned to the top JPanel northPanel = new JPanel(new BorderLayout()); northPanel.add(compPanel, BorderLayout.NORTH); // Horizontal/vertical scroll bars will be displayed if needed JScrollPane scrollPane = new JScrollPane(northPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); scrollPane.setBorder(null); tabbedPane.addTab(titleKey, scrollPane); } private void addShortcutList(XAlignedComponentPanel compPanel, Iterator muActionIdsIterator) { // Add all actions shortcut and label (or tooltip if available) while (muActionIdsIterator.hasNext()) { String actionId = muActionIdsIterator.next(); String shortcutsRep = getActionRepresentation(actionId); compPanel.addRow(shortcutsRep, new JLabel(ActionProperties.getActionDescription(actionId)), 5); } } @Nullable private String getActionRepresentation(String actionId) { KeyStroke shortcut = ActionKeymap.getAccelerator(actionId); String shortcutsRep = KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(shortcut); KeyStroke alternativeShortcut = ActionKeymap.getAlternateAccelerator(actionId); if (alternativeShortcut != null) { return shortcutsRep + " / " + KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(alternativeShortcut); } return shortcutsRep; } private void addShortcutList(XAlignedComponentPanel compPanel, Map actionsToShortcutsMap) { List vec = new ArrayList<>(actionsToShortcutsMap.keySet()); Collections.sort(vec); for (String action : vec) { compPanel.addRow(actionsToShortcutsMap.get(action), new JLabel(i18n(action)), 5); } } public void actionPerformed(ActionEvent e) { // OK disposes the dialog dispose(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/help/package.html ================================================ Various components used to display muCommander's help messages. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/package.html ================================================ Provides various helper classes for dealing with JDialogs. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/PreferencesDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.dialog.pref.component.PrefComponent; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.XBoxPanel; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.List; /** * Dialog meant to let users edit software preferences. * @author Maxence Bernard, Nicolas Rinaudo */ public abstract class PreferencesDialog extends FocusDialog implements ActionListener { /** Displays the different panels. */ private JTabbedPane tabbedPane; /** Stores the different panels. */ private List prefPanels; /** Apply button. */ private JButton btnApply; /** OK button. */ private JButton btnOk; /** Cancel button. */ private JButton btnCancel; /** * Creates a new preferences dialog. * @param parent parent of the dialog. * @param title title of the dialg. */ public PreferencesDialog(Frame parent, String title) { super(parent, title, parent); initUI(); } /** * Creates a new preferences dialog. * @param parent parent of the dialog. * @param title title of the dialg. */ public PreferencesDialog(Dialog parent, String title) { super(parent, title, parent); initUI(); } /** * Initializes the tabbed panel's UI. */ private void initUI() { // Initializes the tabbed pane. prefPanels = new ArrayList<>(); tabbedPane = new JTabbedPane(JTabbedPane.TOP); // Adds the tabbed pane. Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); contentPane.add(tabbedPane, BorderLayout.CENTER); // Buttons panel. XBoxPanel buttonsPanel = new XBoxPanel(); buttonsPanel.add(btnApply = new JButton(i18n("apply"))); buttonsPanel.addSpace(20); buttonsPanel.add(btnOk = new JButton(i18n("ok"))); buttonsPanel.add(btnCancel = new JButton(i18n("cancel"))); // Disable "commit buttons". btnOk.setEnabled(false); btnApply.setEnabled(false); // Buttons listening. btnApply.addActionListener(this); btnOk.addActionListener(this); btnCancel.addActionListener(this); // Aligns the button panel to the right. JPanel tempPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); tempPanel.add(buttonsPanel); contentPane.add(tempPanel, BorderLayout.SOUTH); // Selects OK when enter is pressed getRootPane().setDefaultButton(btnOk); } private Component getTabbedPanel(PreferencesPanel prefPanel, boolean scroll) { if (!scroll) { return prefPanel; } JScrollPane scrollPane = new JScrollPane(prefPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setBorder(null); return scrollPane; } /** * Adds the specified preferences panel to this dialog. * @param prefPanel panel to add. * @param iconName name of the icon that represents this dialog. * @param scroll whether this panel should be wrapped in a scroll panel. */ private void addPreferencesPanel(PreferencesPanel prefPanel, String iconName, boolean scroll) { tabbedPane.addTab(prefPanel.getTitle(), IconManager.getIcon(IconManager.IconSet.PREFERENCES, iconName), getTabbedPanel(prefPanel, scroll)); prefPanels.add(prefPanel); } /** * Adds a new preferences panel and creates a new tab with an icon. * @param prefPanel panel to add. * @param iconName name of the icon that represents this dialog. */ protected void addPreferencesPanel(PreferencesPanel prefPanel, String iconName) { addPreferencesPanel(prefPanel, iconName, true); } /** * Adds the specified preferences panel to this dialog. * @param prefPanel panel to add. * @param scroll whether this panel should be wrapped in a scroll panel. */ protected void addPreferencesPanel(PreferencesPanel prefPanel, boolean scroll) { tabbedPane.addTab(prefPanel.getTitle(), getTabbedPanel(prefPanel, scroll)); prefPanels.add(prefPanel); } /** * Adds the specified preferences panel to this dialog. * @param prefPanel panel to add. */ protected void addPreferencesPanel(PreferencesPanel prefPanel) { addPreferencesPanel(prefPanel, true); } /** * Calls {@link PreferencesPanel#commit()} on all registered preference panels. */ public void commit() { // Ask pref panels to commit changes for (PreferencesPanel panel : prefPanels) { panel.commit(); } setCommitButtonsEnabled(false); } /** * Notifies all panels that changes are about to be commited. *

    * This gives preference panels a chance to display warning or errors before changes are * committed. * * @return true if all preference panels are ok with commiting the changes, false otherwise. */ public boolean checkCommit() { // Ask pref panels to commit changes for (PreferencesPanel panel : prefPanels) { if (!panel.checkCommit()) { return false; } } return true; } /** * Sets the currently active tab. * @param index index of the tab to select. */ public void setActiveTab(int index) { tabbedPane.setSelectedIndex(index); } /** * Reacts to buttons being pushed. */ public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // Commit changes if (source == btnOk || source == btnApply) { if (!checkCommit()) { return; } commit(); } // Dispose dialog if (source == btnOk || source == btnCancel) { dispose(); } } /** * Returns the index of the currently selected configuration panel. * @return the index of the currently selected configuration panel. */ protected int getSelectedPanelIndex() { return tabbedPane.getSelectedIndex(); } /** * This function set the "commit buttons", i.e apply & ok buttons, enabled\disabled * according to the given parameter. * * @param enable - parameter that indicated if the commit button will turn to be * enabled (true) or disabled (false). */ public void setCommitButtonsEnabled(boolean enable) { btnOk.setEnabled(enable); btnApply.setEnabled(enable); } /** * Function that will be called when the user change a value in a PrefComponent in this dialog. * * @param component - the PrefComponent that its value was changed. */ public abstract void componentChanged(PrefComponent component); } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/PreferencesPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref; import com.mucommander.commons.conf.ValueList; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import lombok.Getter; import javax.swing.*; /** * Abstract preferences panel. * @author Maxence Bernard */ public abstract class PreferencesPanel extends JPanel { /** Preferences dialog that contains this panel. */ protected PreferencesDialog parent; /** Panel's title. * -- GETTER -- * Returns the panel's title. */ @Getter protected String title; /** * Creates a new preferences panel. * @param parent dialog that contains this panel. * @param title panel's title. */ public PreferencesPanel(PreferencesDialog parent, String title) { super(); this.title = title; this.parent = parent; } /** * This method is called by PreferencesDialog after the user pressed 'OK' * to save new preferences. */ protected abstract void commit(); /** * Checks whether this panel's data can be committed or whether it contains an error. * @return true if the panel's data can be committed, false otherwise. */ boolean checkCommit() { return true; } protected static String getVariable(TcPreference preference) { return TcConfigurations.getPreferences().getVariable(preference); } protected static boolean getVariable(TcPreference preference, boolean value) { return TcConfigurations.getPreferences().getVariable(preference, value); } protected static String getVariable(TcPreference preference, String value) { return TcConfigurations.getPreferences().getVariable(preference, value); } protected static float getVariable(TcPreference preference, float value) { return TcConfigurations.getPreferences().getVariable(preference, value); } protected static int getVariable(TcPreference preference, int value) { return TcConfigurations.getPreferences().getVariable(preference, value); } protected static ValueList getListVariable(TcPreference preference, String separator) { return TcConfigurations.getPreferences().getListVariable(preference, separator); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefCheckBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.dialog.pref.PreferencesDialog; import javax.swing.*; /** * @author Arik Hadas */ public class PrefCheckBox extends JCheckBox implements PrefComponent { public interface ChangeListener { boolean onChange(JCheckBox checkBox); } private final ChangeListener changeListener; public PrefCheckBox(String description, ChangeListener changeListener) { super(description); this.changeListener = changeListener; } public void addDialogListener(final PreferencesDialog dialog) { addItemListener(e -> dialog.componentChanged(PrefCheckBox.this)); } @Override public boolean hasChanged() { return changeListener != null && changeListener.onChange(this); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefComboBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.ui.dialog.pref.PreferencesDialog; import java.util.List; /** * @author Arik Hadas, Oleg Trifonov */ public abstract class PrefComboBox extends TcComboBox implements PrefComponent { protected PrefComboBox() { super(); } protected PrefComboBox(List items) { super(); addItems(items); } protected PrefComboBox(E[] items) { super(); addItems(items); } private void addItems(List items) { for (E item : items) { addItem(item); } } private void addItems(E[] items) { for (E item : items) { addItem(item); } } public void addDialogListener(final PreferencesDialog dialog) { addItemListener(e -> dialog.componentChanged(PrefComboBox.this)); } @Override public E getSelectedItem() { @SuppressWarnings({"unchecked"}) E selected = (E)super.getSelectedItem(); return selected; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefComponent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.dialog.pref.PreferencesDialog; /** * Interface for components in preferences panels in which changes can be trackered. * * @author Arik Hadas */ public interface PrefComponent { /** * * @param dialog - parent dialog of the component parent panel. */ void addDialogListener(final PreferencesDialog dialog); /** * This function checks if the component's value was changed from the value that is saved * in MuConfiguration. * * @return true if component's value differ from the value at MuConfiguration, else otherwise. */ boolean hasChanged(); } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefEncodingSelectBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.dialog.DialogOwner; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.encoding.EncodingListener; import com.mucommander.ui.encoding.EncodingSelectBox; /** * @author Maxence Bernard */ public abstract class PrefEncodingSelectBox extends EncodingSelectBox implements PrefComponent { protected PrefEncodingSelectBox(DialogOwner dialogOwner, String selectedEncoding) { super(dialogOwner, selectedEncoding); } public void addDialogListener(final PreferencesDialog dialog) { EncodingListener listener = (source, oldEncoding, newEncoding) -> dialog.componentChanged(PrefEncodingSelectBox.this); addEncodingListener(listener); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefFilePathField.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.autocomplete.BasicAutocompleterTextComponent; import com.mucommander.ui.autocomplete.CompleterFactory; import com.mucommander.ui.autocomplete.TextFieldCompletion; /** * @author Arik Hadas */ public abstract class PrefFilePathField extends PrefTextField { public PrefFilePathField(String text) { super(text); enableAutoCompletion(); } /** * Adds auto-completion capabilities to this text field. */ private void enableAutoCompletion() { new TextFieldCompletion(new BasicAutocompleterTextComponent(this), CompleterFactory.getPathCompleter()); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefRadioButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.dialog.pref.PreferencesDialog; import javax.swing.*; /** * @author Arik Hadas */ public abstract class PrefRadioButton extends JRadioButton implements PrefComponent { protected PrefRadioButton(String description) { super(description); } public void addDialogListener(final PreferencesDialog dialog) { addItemListener(e -> dialog.componentChanged(PrefRadioButton.this)); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefTable.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.dialog.pref.PreferencesDialog; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.event.TableModelListener; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import java.awt.*; /** * @author Arik Hadas */ public abstract class PrefTable extends JTable implements PrefComponent { private TableModelListener dialogListener; public PrefTable() { super(); } public PrefTable(TableModel model) { super(model); } /** * This function sets the widths of the table's columns according to the given array. * * @param percentages - array that contains the width of each column in percentage * from the width of the whole table. */ public void setPreferredColumnWidths(double[] percentages) { final Dimension tableDim = this.getPreferredSize(); double total = 0; int nbColumns = getColumnModel().getColumnCount(); for (int i = 0; i < nbColumns; ++i) { total += percentages[i]; } for (int i = 0; i < nbColumns; ++i) { TableColumn column = getColumnModel().getColumn(i); column.setPreferredWidth((int) (tableDim.width * (percentages[i] / total))); } } public void addDialogListener(final PreferencesDialog dialog) { getModel().addTableModelListener(dialogListener = e -> dialog.componentChanged(PrefTable.this)); } @Override public void setModel(@NotNull TableModel model) { if (dialogListener != null) { model.addTableModelListener(dialogListener); } super.setModel(model); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/component/PrefTextField.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.component; import com.mucommander.ui.dialog.pref.PreferencesDialog; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; /** * @author Arik Hadas */ public abstract class PrefTextField extends JTextField implements PrefComponent { public PrefTextField(int columns) { super(columns); } public PrefTextField(String text) { super(text); } public void addDialogListener(final PreferencesDialog dialog) { getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { dialog.componentChanged(PrefTextField.this); } public void insertUpdate(DocumentEvent e) { dialog.componentChanged(PrefTextField.this); } public void removeUpdate(DocumentEvent e) { dialog.componentChanged(PrefTextField.this); } }); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/AppearancePanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.*; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.UIManager; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.ui.widgets.render.BasicComboBoxRenderer; import com.mucommander.utils.FileIconsCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.extension.ClassFinder; import com.mucommander.extension.ExtensionManager; import com.mucommander.extension.LookAndFeelFilter; import com.mucommander.job.FileCollisionChecker; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.dialog.pref.component.PrefCheckBox; import com.mucommander.ui.dialog.pref.component.PrefComboBox; import com.mucommander.ui.dialog.pref.theme.ThemeEditorDialog; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import static com.mucommander.conf.TcPreference.*; /** * 'Appearance' preferences panel. * @author Maxence Bernard, Nicolas Rinaudo */ class AppearancePanel extends PreferencesPanel implements ActionListener, Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(AppearancePanel.class); // - Look and feel fields ------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** Combo box containing the list of available look&feels. */ private PrefComboBox lookAndFeelComboBox; /** All available look&feels. */ private UIManager.LookAndFeelInfo[] lookAndFeels; /** 'Use brushed metal look' checkbox */ private PrefCheckBox brushedMetalCheckBox; /** Triggers look and feel importing. */ private JButton importLookAndFeelButton; /** Triggers look and feel deletion. */ private JButton deleteLookAndFeelButton; /** Used to notify the user that the system is working. */ private SpinningDial dial; /** File from which to import looks and feels. */ private AbstractFile lookAndFeelLibrary; // - Icon size fields ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Displays the list of available sizes for toolbar icons. */ private PrefComboBox toolbarIconsSizeComboBox; /** Displays the list of available sizes for command bar icons. */ private PrefComboBox commandBarIconsSizeComboBox; /** Displays the list of available sizes for file icons. */ private PrefComboBox fileIconsSizeComboBox; /** All icon sizes label. */ private final static String[] ICON_SIZES = {"100%", "125%", "150%", "175%", "200%", "300%"}; /** All icon sizes scale factors. */ private final static float[] ICON_SCALE_FACTORS = {1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 3.0f}; // - Icons --------------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Icon used to identify 'locked' themes. */ private ImageIcon lockIcon; /** Transparent icon used to align non-locked themes with the others. */ private ImageIcon transparentIcon; // - Theme fields -------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Lists all available themes. */ private PrefComboBox themeComboBox; /** Triggers the theme editor. */ private JButton btnEditTheme; /** Triggers the theme duplication dialog. */ private JButton btnDuplicate; /** Triggers the theme import dialog. */ private JButton btnImportTheme; /** Triggers the theme export dialog. */ private JButton btnExportTheme; /** Triggers the theme rename dialog. */ private JButton btnRenameTheme; /** Triggers the theme delete dialog. */ private JButton btnDeleteTheme; /** Used to display the currently selected theme's type. */ private JLabel lblType; /** Whether to ignore theme combobox related events. */ private boolean ignoreComboChanges; /** Last folder that was selected in import or export operations. */ private AbstractFile lastSelectedFolder; // - Editor Theme fields -------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Lists all available editor themes. */ private PrefComboBox syntaxThemeComboBox; // - Misc. fields -------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** System icon combobox. */ private PrefComboBox useSystemFileIconsComboBox; /** Identifier of 'yes' actions in question dialogs. */ private final static int YES_ACTION = 0; /** Identifier of 'no' actions in question dialogs. */ private final static int NO_ACTION = 1; /** Identifier of 'cancel' actions in question dialogs. */ private final static int CANCEL_ACTION = 2; /** All known custom look and feels. */ private List customLookAndFeels; /** * Creates a new appearance panel with the specified parent. * @param parent dialog in which this panel is placed. */ AppearancePanel(PreferencesDialog parent) { super(parent, Translator.get("prefs_dialog.appearance_tab")); initUI(); initializeCustomLookAndFeels(); } private void initUI() { YBoxPanel mainPanel; mainPanel = new YBoxPanel(); // Look and feel. mainPanel.add(createLookAndFeelPanel()); mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); // Themes. mainPanel.add(createThemesPanel()); mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); // Syntax highlighting. mainPanel.add(createSyntaxHighlightThemePanel()); mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); // System icons. mainPanel.add(createSystemIconsPanel()); mainPanel.add(Box.createVerticalGlue()); // Icon size. mainPanel.add(createIconSizePanel()); mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); setLayout(new BorderLayout()); add(mainPanel, BorderLayout.NORTH); lookAndFeelComboBox.addDialogListener(parent); themeComboBox.addDialogListener(parent); syntaxThemeComboBox.addDialogListener(parent); useSystemFileIconsComboBox.addDialogListener(parent); toolbarIconsSizeComboBox.addDialogListener(parent); commandBarIconsSizeComboBox.addDialogListener(parent); fileIconsSizeComboBox.addDialogListener(parent); if (brushedMetalCheckBox != null) { brushedMetalCheckBox.addDialogListener(parent); } } /** * Populates the look&feel combo box with all available look&feels. */ private void populateLookAndFeels() { lookAndFeelComboBox.removeAllItems(); initializeAvailableLookAndFeels(); // Populates the combo box. int currentIndex = -1; String currentName = UIManager.getLookAndFeel().getClass().getName(); for (int i = 0; i < lookAndFeels.length; i++) { // Looks for the currently selected look&feel. if (lookAndFeels[i].getClassName().equals(currentName)) { currentIndex = i; } lookAndFeelComboBox.addItem(lookAndFeels[i].getName()); } // Sets the initial selection. if (currentIndex < 0) { currentIndex = 0; } lookAndFeelComboBox.setSelectedIndex(currentIndex); } /** * Creates the look and feel panel. * @return the look and feel panel. */ private JPanel createLookAndFeelPanel() { // Creates the panel. JPanel lnfPanel = new YBoxPanel(); lnfPanel.setAlignmentX(LEFT_ALIGNMENT); lnfPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.look_and_feel"))); // Creates the look and feel combo box. lookAndFeelComboBox = new PrefComboBox<>() { public boolean hasChanged() { String lnf = getVariable(LOOK_AND_FEEL); int selectedIndex = getSelectedIndex(); return selectedIndex >= 0 && !lookAndFeels[selectedIndex].getClassName().equals(lnf); } }; lookAndFeelComboBox.setRenderer(new BasicComboBoxRenderer<>() { @Override public Component getListCellRendererComponent(JList list, String value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (index < 0) { return label; } // All look and feels that are not modifiable must be flagged with a lock icon. label.setIcon(isLookAndFeelModifiable(lookAndFeels[index]) ? transparentIcon : lockIcon); return label; } }); // Populates the look and feel combo box. populateLookAndFeels(); // Initializes buttons and event listening. importLookAndFeelButton = new JButton(Translator.get("prefs_dialog.import") + "..."); deleteLookAndFeelButton = new JButton(Translator.get("delete")); importLookAndFeelButton.addActionListener(this); deleteLookAndFeelButton.addActionListener(this); resetLookAndFeelButtons(); lookAndFeelComboBox.addActionListener(this); // Adds the look and feel list and the action buttons to the panel. JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); flowPanel.add(lookAndFeelComboBox); flowPanel.add(importLookAndFeelButton); flowPanel.add(deleteLookAndFeelButton); flowPanel.add(new JLabel(dial = new SpinningDial())); lnfPanel.add(flowPanel); // For Mac OS X only, creates the 'brushed metal' checkbox. // At the time of writing, the 'brushed metal' look causes the JVM to crash randomly under Leopard (10.5) // so we disable brushed metal on that OS version but leave it for earlier versions where it works fine. // See http://www.mucommander.com/forums/viewtopic.php?f=4&t=746 for more info about this issue. if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_4.isCurrentOrLower()) { // 'Use brushed metal look' option brushedMetalCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.use_brushed_metal"), checkBox -> !String.valueOf(checkBox.isSelected()).equals(getVariable(USE_BRUSHED_METAL))); brushedMetalCheckBox.setSelected(getVariable(USE_BRUSHED_METAL, TcPreferences.DEFAULT_USE_BRUSHED_METAL)); flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); flowPanel.add(brushedMetalCheckBox); lnfPanel.add(flowPanel); } return lnfPanel; } /** * Creates the icon size panel. * @return the icon size panel. */ private JPanel createIconSizePanel() { ProportionalGridPanel gridPanel = new ProportionalGridPanel(2); gridPanel.add(new JLabel(Translator.get("prefs_dialog.toolbar_icons"))); gridPanel.add(toolbarIconsSizeComboBox = createIconSizeCombo(TOOLBAR_ICON_SCALE, TcPreferences.DEFAULT_TOOLBAR_ICON_SCALE)); gridPanel.add(new JLabel(Translator.get("prefs_dialog.command_bar_icons"))); gridPanel.add(commandBarIconsSizeComboBox = createIconSizeCombo(COMMAND_BAR_ICON_SCALE, TcPreferences.DEFAULT_COMMAND_BAR_ICON_SCALE)); gridPanel.add(new JLabel(Translator.get("prefs_dialog.file_icons"))); gridPanel.add(fileIconsSizeComboBox = createIconSizeCombo(TABLE_ICON_SCALE, TcPreferences.DEFAULT_TABLE_ICON_SCALE)); JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEADING)); flowPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.icons_size"))); flowPanel.add(gridPanel); return flowPanel; } /** * Creates the themes panel. * @return the themes panel. */ private JPanel createThemesPanel() { JPanel gridPanel = new ProportionalGridPanel(4); // Creates the various panel's buttons. btnEditTheme = new JButton(Translator.get("edit") + "..."); btnImportTheme = new JButton(Translator.get("prefs_dialog.import") + "..."); btnExportTheme = new JButton(Translator.get("prefs_dialog.export") + "..."); btnRenameTheme = new JButton(Translator.get("rename")); btnDeleteTheme = new JButton(Translator.get("delete")); btnDuplicate = new JButton(Translator.get("duplicate")); btnEditTheme.addActionListener(this); btnImportTheme.addActionListener(this); btnExportTheme.addActionListener(this); btnRenameTheme.addActionListener(this); btnDeleteTheme.addActionListener(this); btnDuplicate.addActionListener(this); // Creates the panel's 'type label'. lblType = new JLabel(""); // Creates the theme combo box. themeComboBox = new PrefComboBox<>() { public boolean hasChanged() { return !ThemeManager.isCurrentTheme(getSelectedItem()); } }; themeComboBox.addActionListener(this); // Sets the combobox's renderer. lockIcon = IconManager.getIcon(IconManager.IconSet.PREFERENCES, "lock.png"); transparentIcon = new ImageIcon(new BufferedImage(lockIcon.getIconWidth(), lockIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB)); themeComboBox.setRenderer(new BasicComboBoxRenderer<>() { @Override public Component getListCellRendererComponent(JList list, Theme theme, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel) super.getListCellRendererComponent(list, theme, index, isSelected, cellHasFocus); if (ThemeManager.isCurrentTheme(theme)) { label.setText(theme.getName() + " (" + Translator.get("theme.current") + ")"); } else { label.setText(theme.getName()); } label.setIcon(theme.getType() == Theme.Type.PREDEFINED ? lockIcon : transparentIcon); return label; } }); // Initialises the content of the combo box. populateThemes(ThemeManager.getCurrentTheme()); gridPanel.add(themeComboBox); gridPanel.add(btnEditTheme); gridPanel.add(btnImportTheme); gridPanel.add(btnExportTheme); gridPanel.add(lblType); gridPanel.add(btnRenameTheme); gridPanel.add(btnDeleteTheme); gridPanel.add(btnDuplicate); JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEADING)); flowPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.themes"))); flowPanel.add(gridPanel); return flowPanel; } private JPanel createSyntaxHighlightThemePanel() { JPanel gridPanel = new ProportionalGridPanel(1); syntaxThemeComboBox = new PrefComboBox<>(ThemeManager.predefinedSyntaxThemeNames()) { @Override public boolean hasChanged() { String selectedTheme = getSelectedItem(); return !ThemeManager.getCurrentSyntaxThemeName().equalsIgnoreCase(selectedTheme); } }; syntaxThemeComboBox.addActionListener(this); syntaxThemeComboBox.setSelectedItem(ThemeManager.getCurrentSyntaxThemeName()); gridPanel.add(syntaxThemeComboBox); JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEADING)); flowPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.syntax_themes"))); flowPanel.add(gridPanel); return flowPanel; } private void populateThemes(Theme selectedTheme) { ignoreComboChanges = true; themeComboBox.removeAllItems(); // Creates new theme instances for all but the currentTheme (current as the currently active one - not // the currently selected one!) so we might need to find the new instance of our previously selected theme. Iterator themes = ThemeManager.availableThemes(); Theme selectedThemeAvailableInstance = null; while (themes.hasNext()) { final Theme availableTheme = themes.next(); themeComboBox.addItem(availableTheme); if (availableTheme.equals(selectedTheme)) { selectedThemeAvailableInstance = availableTheme; } } ignoreComboChanges = false; if (selectedThemeAvailableInstance != null) { themeComboBox.setSelectedItem(selectedThemeAvailableInstance); } else { LOGGER.warn("selected theme not available anymore"); themeComboBox.setSelectedIndex(0); } } /** * Creates the system icons panel. * @return the system icons panel. */ private JPanel createSystemIconsPanel() { // 'Use system file icons' combo box this.useSystemFileIconsComboBox = new PrefComboBox<>() { public boolean hasChanged() { String systemIconsPolicy = switch (useSystemFileIconsComboBox.getSelectedIndex()) { case 0 -> FileIcons.USE_SYSTEM_ICONS_NEVER; case 1 -> FileIcons.USE_SYSTEM_ICONS_APPLICATIONS; default -> FileIcons.USE_SYSTEM_ICONS_ALWAYS; }; return !systemIconsPolicy.equals(getVariable(USE_SYSTEM_FILE_ICONS, systemIconsPolicy)); } }; useSystemFileIconsComboBox.addItem(Translator.get("prefs_dialog.use_system_file_icons.never")); useSystemFileIconsComboBox.addItem(Translator.get("prefs_dialog.use_system_file_icons.applications")); useSystemFileIconsComboBox.addItem(Translator.get("prefs_dialog.use_system_file_icons.always")); String systemIconsPolicy = FileIcons.getSystemIconsPolicy(); useSystemFileIconsComboBox.setSelectedIndex(FileIcons.USE_SYSTEM_ICONS_ALWAYS.equals(systemIconsPolicy) ? 2 : FileIcons.USE_SYSTEM_ICONS_APPLICATIONS.equals(systemIconsPolicy) ? 1 : 0); JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.use_system_file_icons"))); panel.add(useSystemFileIconsComboBox); return panel; } /** * Creates a combo box that allows to choose a size for a certain type of icon. The returned combo box is filled * with allowed choices, and the current configuration value is selected. * * @param defaultValue the default value for the icon scale factor if the configuration variable has no value * @return a combo box that allows to choose a size for a certain type of icon */ private PrefComboBox createIconSizeCombo(final TcPreference preference, float defaultValue) { PrefComboBox iconSizeCombo = new PrefComboBox<>(ICON_SIZES) { public boolean hasChanged() { return !String.valueOf(ICON_SCALE_FACTORS[getSelectedIndex()]).equals(getVariable(preference)); } }; float scaleFactor = getVariable(preference, defaultValue); int index = 0; for (int i = 0; i < ICON_SCALE_FACTORS.length; i++) { if (scaleFactor == ICON_SCALE_FACTORS[i]) { index = i; break; } } iconSizeCombo.setSelectedIndex(index); return iconSizeCombo; } @Override protected void commit() { final TcPreferencesAPI pref = TcConfigurations.getPreferences(); // Look and Feel if (pref.setVariable(LOOK_AND_FEEL, lookAndFeels[lookAndFeelComboBox.getSelectedIndex()].getClassName())) { resetLookAndFeelButtons(); SwingUtilities.updateComponentTreeUI(parent); } if (brushedMetalCheckBox != null) { pref.setVariable(USE_BRUSHED_METAL, brushedMetalCheckBox.isSelected()); } // Set ToolBar's icon size float scaleFactor = ICON_SCALE_FACTORS[toolbarIconsSizeComboBox.getSelectedIndex()]; pref.setVariable(TOOLBAR_ICON_SCALE, scaleFactor); // Set CommandBar's icon size scaleFactor = ICON_SCALE_FACTORS[commandBarIconsSizeComboBox.getSelectedIndex()]; pref.setVariable(COMMAND_BAR_ICON_SCALE , scaleFactor); // Set file icon size scaleFactor = ICON_SCALE_FACTORS[fileIconsSizeComboBox.getSelectedIndex()]; // Set scale factor in FileIcons first so that it has the new value when ConfigurationListener instances call it FileIcons.setScaleFactor(scaleFactor); FileIconsCache.getInstance().clear(); pref.setVariable(TABLE_ICON_SCALE , scaleFactor); // Sets the current theme. if (!ThemeManager.isCurrentTheme(themeComboBox.getSelectedItem())) { ThemeManager.setCurrentTheme(themeComboBox.getSelectedItem()); resetThemeButtons(themeComboBox.getSelectedItem()); themeComboBox.repaint(); } // Sets the current syntax theme. final String syntaxThemeName = syntaxThemeComboBox.getSelectedItem(); if (!ThemeManager.getCurrentSyntaxThemeName().equalsIgnoreCase(syntaxThemeName)) { ThemeManager.setCurrentSyntaxTheme(syntaxThemeName); syntaxThemeComboBox.repaint(); } // Set system icons policy int comboIndex = useSystemFileIconsComboBox.getSelectedIndex(); String systemIconsPolicy = comboIndex == 0 ? FileIcons.USE_SYSTEM_ICONS_NEVER : comboIndex == 1 ? FileIcons.USE_SYSTEM_ICONS_APPLICATIONS : FileIcons.USE_SYSTEM_ICONS_ALWAYS; FileIcons.setSystemIconsPolicy(systemIconsPolicy); pref.setVariable(USE_SYSTEM_FILE_ICONS, systemIconsPolicy); } // - Look and feel actions -------------------------------------------------- // -------------------------------------------------------------------------- /** * Initializes the list of custom look&feels. */ private void initializeCustomLookAndFeels() { customLookAndFeels = getListVariable(CUSTOM_LOOK_AND_FEELS, TcPreferences.CUSTOM_LOOK_AND_FEELS_SEPARATOR); } /** * Initializes the list of available look&feels. */ private void initializeAvailableLookAndFeels() { // Loads all available look and feels. lookAndFeels = UIManager.getInstalledLookAndFeels(); // Sorts them. Arrays.sort(lookAndFeels, new Comparator<>() { public int compare(UIManager.LookAndFeelInfo a, UIManager.LookAndFeelInfo b) { return a.getName().compareTo(b.getName()); } public boolean equals(Object a) { return false; } }); } /** * Returns true if the specified class name is that of a custom look and feel. * @return true if the specified class name is that of a custom look and feel, false otherwise. */ private boolean isCustomLookAndFeel(String className) { return customLookAndFeels != null && customLookAndFeels.contains(className); } /** * Returns true if the specified look and feel is modifiable. *

    * To be modifiable, a look and feel must meet all of the following conditions: *

      *
    • It must be a custom look and feel.
    • *
    • It cannot be the application's current look and feel.
    • *
    * * @return true if the specified look and feel is modifiable, false otherwise. */ private boolean isLookAndFeelModifiable(UIManager.LookAndFeelInfo laf) { return isCustomLookAndFeel(laf.getClassName()) && !laf.getClassName().equals(UIManager.getLookAndFeel().getClass().getName()); } /** * Resets the enabled status of the various look and feel buttons depending on the current selection. */ private void resetLookAndFeelButtons() { // If the dial is animated, we're currently loading look&feels and should ignore this call. if (dial == null || !dial.isAnimated()) { int selectedIndex = lookAndFeelComboBox.getSelectedIndex(); if (selectedIndex >= 0) { deleteLookAndFeelButton.setEnabled(isLookAndFeelModifiable(lookAndFeels[selectedIndex])); } } } /** * Uninstalls the specified look and feel. * @param selection look and feel to uninstall. */ private void uninstallLookAndFeel(UIManager.LookAndFeelInfo selection) { UIManager.LookAndFeelInfo[] buffer; // New array of installed look and feels. int bufferIndex; // Current index in buffer. // Copies the content of lookAndFeels into buffer, skipping over the look and feel to uninstall. buffer = new UIManager.LookAndFeelInfo[lookAndFeels.length - 1]; bufferIndex = 0; for (UIManager.LookAndFeelInfo lookAndFeel : lookAndFeels) { if (!selection.getClassName().equals(lookAndFeel.getClassName())) { buffer[bufferIndex] = lookAndFeel; bufferIndex++; } } // Resets the list of installed look and feels. UIManager.setInstalledLookAndFeels(lookAndFeels = buffer); } /** * Deletes the specified look and feel from the list of custom look and feels. * @param selection currently selection look and feel. */ private void deleteCustomLookAndFeel(UIManager.LookAndFeelInfo selection) { if (customLookAndFeels != null) { if (customLookAndFeels.remove(selection.getClassName())) { TcConfigurations.getPreferences().setVariable(CUSTOM_LOOK_AND_FEELS, customLookAndFeels, TcPreferences.CUSTOM_LOOK_AND_FEELS_SEPARATOR); } } } /** * Deletes the currently selected look and feel. *

    * After receiving user confirmation, this method will: *

      *
    • Remove the look and feel from the combobox.
    • *
    • Remove the look and feel from UIManager's list of installed look and feels.
    • *
    • Remove the look and feel from the list of custom look and feels.
    • *
    */ private void deleteSelectedLookAndFeel() { UIManager.LookAndFeelInfo selection; // Currently selected look and feel. selection = lookAndFeels[lookAndFeelComboBox.getSelectedIndex()]; // Asks the user whether he's sure he wants to delete the selected look and feel. if (new QuestionDialog(parent, null, Translator.get("prefs_dialog.delete_look_and_feel", selection.getName()), parent, new String[] {Translator.get("yes"), Translator.get("no")}, new int[] {YES_ACTION, NO_ACTION}, 0).getActionValue() != YES_ACTION) return; // Removes the selected look and feel from the combo box. lookAndFeelComboBox.removeItem(selection.getName()); // Removes the selected look and feel from the list of installed look and feels. uninstallLookAndFeel(selection); // Removes the selected look and feel from the list of custom look and feels. deleteCustomLookAndFeel(selection); } /** * Updates the different look&feel related UI widgets depending on whether they are busy or not. * @param loading whether look&feels are loading. */ private void setLookAndFeelsLoading(boolean loading) { // Starts / stops the loading animation. dial.setAnimated(loading); // Disables / enables the import button and the combo box. importLookAndFeelButton.setEnabled(!loading); deleteLookAndFeelButton.setEnabled(!loading); lookAndFeelComboBox.setEnabled(!loading); // A special case must be made for the delete button // as it might not need to be re-enabled. if (loading) { deleteLookAndFeelButton.setEnabled(false); } else { resetLookAndFeelButtons(); } } /** * Tries to import the specified library in the extensions folder. *

    * If there is already a file with the same name in the extensions folder, * this method will ask the user for confirmation before overwriting it. * * @param library library to import in the extensions folder. * @return true if the library was imported, false if the user cancelled the operation. * @throws IOException if an I/O error occurred while importing the library */ private boolean importLookAndFeelLibrary(AbstractFile library) throws IOException { // Tries to import the file, but if a version of it is already present in the extensions folder, // asks the user for confirmation. AbstractFile destFile = ExtensionManager.getExtensionsFile(library.getName()); int collision = FileCollisionChecker.checkForCollision(library, destFile); if (collision != FileCollisionChecker.NO_COLLISION) { // Do not offer the multiple files mode options such as 'skip' and 'apply to all' int action = new FileCollisionDialog(parent, parent, collision, library, destFile, false, false).getActionValue(); // User chose to overwrite the file if (action == FileCollisionDialog.OVERWRITE_ACTION) { // Simply continue and file will be overwritten } else if (action==FileCollisionDialog.OVERWRITE_IF_OLDER_ACTION) { // Overwrite if the source is more recent than the destination if (library.getLastModifiedDate() <= destFile.getLastModifiedDate()) return false; // Simply continue and file will be overwritten } else { return false; // User chose to cancel or closed the dialog } } return ExtensionManager.importLibrary(library, true); } public void run() { List> newLookAndFeels; setLookAndFeelsLoading(true); try { // Identifies all the look&feels contained by the new library and adds them to the list of custom // If no look&feel was found, notifies the user. if ((newLookAndFeels = new ClassFinder().find(lookAndFeelLibrary, new LookAndFeelFilter())).isEmpty()) InformationDialog.showWarningDialog(this, Translator.get("prefs_dialog.no_look_and_feel")); else if (importLookAndFeelLibrary(lookAndFeelLibrary)) { if (customLookAndFeels == null) customLookAndFeels = new Vector<>(); // Adds all new instances to the list of custom look&feels. for (Class newLookAndFeel : newLookAndFeels) { String currentName = newLookAndFeel.getName(); if (!customLookAndFeels.contains(currentName)) { customLookAndFeels.add(currentName); try { WindowManager.installLookAndFeel(currentName); } catch (Throwable e) { e.printStackTrace(); } } } if (customLookAndFeels.isEmpty()) { customLookAndFeels = null; } else { TcConfigurations.getPreferences().setVariable(CUSTOM_LOOK_AND_FEELS, customLookAndFeels, TcPreferences.CUSTOM_LOOK_AND_FEELS_SEPARATOR); } populateLookAndFeels(); } } catch(Exception e) { LOGGER.debug("Exception caught", e); InformationDialog.showErrorDialog(this); } setLookAndFeelsLoading(false); } private void importLookAndFeel() { // Initializes the file chooser. JFileChooser chooser = createFileChooser(); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.addChoosableFileFilter(new ExtensionFileFilter("jar", Translator.get("prefs_dialog.jar_file"))); chooser.setDialogTitle(Translator.get("prefs_dialog.import_look_and_feel")); chooser.setDialogType(JFileChooser.OPEN_DIALOG); if (chooser.showDialog(parent, Translator.get("prefs_dialog.import")) == JFileChooser.APPROVE_OPTION) { AbstractFile file = FileFactory.getFile(chooser.getSelectedFile().getAbsolutePath()); if (file == null) { return; } lastSelectedFolder = file.getParent(); // Makes sure the file actually exists - JFileChooser apparently doesn't enforce that properly in all look&feels. if (!file.exists()) { InformationDialog.showErrorDialog(this, Translator.get("this_file_does_not_exist", file.getName())); return; } // Imports the JAR in a separate thread. lookAndFeelLibrary = file; new Thread(this).start(); } } // - Theme actions ---------------------------------------------------------- // -------------------------------------------------------------------------- private void setTypeLabel(Theme theme) { String label; if (theme.getType() == Theme.Type.USER) { label = Translator.get("theme.custom"); } else if(theme.getType() == Theme.Type.PREDEFINED) { label = Translator.get("theme.built_in"); } else { label = Translator.get("theme.add_on"); } lblType.setText(Translator.get("prefs_dialog.theme_type", label)); } private void resetThemeButtons(Theme theme) { if (ignoreComboChanges) { return; } setTypeLabel(theme); if (theme.getType() != Theme.Type.CUSTOM) { btnRenameTheme.setEnabled(false); btnDeleteTheme.setEnabled(false); } else { btnRenameTheme.setEnabled(true); btnDeleteTheme.setEnabled(!ThemeManager.isCurrentTheme(theme)); } } /** * Renames the specified theme. * @param theme theme to rename. */ private void renameTheme(Theme theme) { ThemeNameDialog dialog = new ThemeNameDialog(parent, theme.getName()); if (dialog.wasValidated()) { // If the rename operation was a success, makes sure the theme is located at its proper position. try { ThemeManager.renameCustomTheme(theme, dialog.getText()); themeComboBox.removeItem(theme); insertTheme(theme); } catch(Exception e) { // Otherwise, notifies the user. InformationDialog.showErrorDialog(this, Translator.get("prefs_dialog.rename_failed", theme.getName())); } } } /** * Deletes the specified theme. * @param theme theme to delete. */ private void deleteTheme(Theme theme) { // Asks the user whether he's sure he wants to delete the selected theme. if (new QuestionDialog(parent, null, Translator.get("prefs_dialog.delete_theme", theme.getName()), parent, new String[] {Translator.get("yes"), Translator.get("no")}, new int[] {YES_ACTION, NO_ACTION}, 0).getActionValue() != YES_ACTION) return; // Deletes the selected theme and removes it from the list. try { ThemeManager.deleteCustomTheme(theme.getName()); themeComboBox.removeItem(theme); } catch(Exception e) { InformationDialog.showErrorDialog(this); } } /** * Starts the theme editor on the specified theme. * @param theme to edit. */ private void editTheme(Theme theme) { // If the edited theme was modified, we must re-populate the list. final Theme modifiedTheme = new ThemeEditorDialog(parent, theme).editTheme(); if (modifiedTheme != null) { populateThemes(modifiedTheme); parent.setCommitButtonsEnabled(true); } } /** * Creates a file chooser initialized on the last selected folder. */ private JFileChooser createFileChooser() { if (lastSelectedFolder == null) { return new JFileChooser(); } return new JFileChooser((java.io.File)lastSelectedFolder.getUnderlyingFileObject()); } private void insertTheme(Theme theme) { int i; int count = themeComboBox.getItemCount(); for (i = 0; i < count; i++) { if((themeComboBox.getItemAt(i)).getName().compareTo(theme.getName()) >= 0) { themeComboBox.insertItemAt(theme, i); break; } } if (i == count) { themeComboBox.addItem(theme); } themeComboBox.setSelectedItem(theme); } /** * Imports a new theme in muCommander. */ private void importTheme() { // Initializes the file chooser. JFileChooser chooser = createFileChooser(); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.addChoosableFileFilter(new ExtensionFileFilter("xml", Translator.get("prefs_dialog.xml_file"))); chooser.setDialogTitle(Translator.get("prefs_dialog.import_theme")); chooser.setDialogType(JFileChooser.OPEN_DIALOG); if (chooser.showDialog(parent, Translator.get("prefs_dialog.import")) == JFileChooser.APPROVE_OPTION) { // Makes sure the file actually exists - JFileChooser apparently doesn't enforce that properly in all look&feels. AbstractFile file = FileFactory.getFile(chooser.getSelectedFile().getAbsolutePath()); if (file == null) { return; } lastSelectedFolder = file.getParent(); if (!file.exists()) { InformationDialog.showErrorDialog(this, Translator.get("this_file_does_not_exist", file.getName())); return; } // Imports the theme and makes sure it appears in the combobox. try { insertTheme(ThemeManager.importTheme((java.io.File)file.getUnderlyingFileObject())); } catch(Exception ex) { // Notifies the user that something went wrong. InformationDialog.showErrorDialog(this, Translator.get("prefs_dialog.error_in_import", file.getName())); } } } /** * Exports the specified theme. * @param theme theme to export. */ private void exportTheme(Theme theme) { JFileChooser chooser = createFileChooser(); chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); chooser.addChoosableFileFilter(new ExtensionFileFilter("xml", Translator.get("prefs_dialog.xml_file"))); chooser.setDialogTitle(Translator.get("prefs_dialog.export_theme", theme.getName())); if (chooser.showDialog(parent, Translator.get("prefs_dialog.export")) == JFileChooser.APPROVE_OPTION) { AbstractFile file = FileFactory.getFile(chooser.getSelectedFile().getAbsolutePath()); lastSelectedFolder = file.getParent(); // Makes sure the file's extension is .xml. try { if (!"xml".equalsIgnoreCase(file.getExtension())) // Note: getExtension() may return null if no extension file = lastSelectedFolder.getDirectChild(file.getName()+".xml"); int collision = FileCollisionChecker.checkForCollision(null, file); if (collision != FileCollisionChecker.NO_COLLISION) { // Do not offer the multiple files mode options such as 'skip' and 'apply to all' int action = new FileCollisionDialog(parent, parent, collision, null, file, false, false).getActionValue(); // User chose to overwrite the file if (action == FileCollisionDialog.OVERWRITE_ACTION) { // Simply continue and file will be overwritten } else { // User chose to cancel or closed the dialog return; } } // Exports the theme. ThemeManager.exportTheme(theme, (java.io.File)file.getUnderlyingFileObject()); // If it was exported to the custom themes folder, reload the theme combobox to reflect the // changes. if (lastSelectedFolder.equals(ThemeManager.getCustomThemesFolder())) { populateThemes(theme); } } catch(Exception exception) { // Notifies users of errors. InformationDialog.showErrorDialog(this, Translator.get("write_error"), Translator.get("cannot_write_file", file.getName())); } } } /** * Duplicates the specified theme. */ private void duplicateTheme(Theme theme) { try { insertTheme(ThemeManager.duplicateTheme(theme)); } catch(Exception e) { InformationDialog.showErrorDialog(this); } } // - Listener code ---------------------------------------------------------- // -------------------------------------------------------------------------- /** * Called when a button is pressed. */ public void actionPerformed(ActionEvent e) { Theme theme = themeComboBox.getSelectedItem(); // Theme combobox selection changed. if(e.getSource() == themeComboBox) resetThemeButtons(theme); // Look and feel combobox selection changed. else if(e.getSource() == lookAndFeelComboBox) resetLookAndFeelButtons(); // Delete look and feel button has been pressed. else if(e.getSource() == deleteLookAndFeelButton) deleteSelectedLookAndFeel(); // Import look and feel button has been pressed. else if(e.getSource() == importLookAndFeelButton) importLookAndFeel(); // Rename button was pressed. else if(e.getSource() == btnRenameTheme) renameTheme(theme); // Delete button was pressed. else if(e.getSource() == btnDeleteTheme) deleteTheme(theme); // Edit button was pressed. else if(e.getSource() == btnEditTheme) editTheme(theme); // Import button was pressed. else if(e.getSource() == btnImportTheme) importTheme(); // Export button was pressed. else if(e.getSource() == btnExportTheme) exportTheme(theme); // Export button was pressed. else if(e.getSource() == btnDuplicate) duplicateTheme(theme); } // - File IMAGE_FILTER ------------------------------------------------------------ // -------------------------------------------------------------------------- /** * Filter used to only display XML files in the JFileChooser. * @author Nicolas Rinaudo */ private static class ExtensionFileFilter extends javax.swing.filechooser.FileFilter { /** Extension to match. */ private final String extension; /** Filter's description. */ private final String description; /** * Creates a new extension file IMAGE_FILTER that will match files with the specified extension. * @param extension extension to match. */ ExtensionFileFilter(String extension, String description) { this.extension = extension; this.description = description; } /** * Returns true if the specified file should be displayed in the chooser. */ @Override public boolean accept(File file) { // Directories are always displayed. if (file.isDirectory()) { return true; } // If the file has an extension, and it matches .xml, return true. // Otherwise, return false. String ext = AbstractFile.getExtension(file.getName()); return extension.equalsIgnoreCase(ext); } @Override public String getDescription() { return description; } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/FoldersPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.File; import javax.swing.*; import javax.swing.plaf.basic.BasicTextFieldUI; import javax.swing.text.JTextComponent; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.dialog.pref.component.PrefCheckBox; import com.mucommander.ui.dialog.pref.component.PrefComboBox; import com.mucommander.ui.dialog.pref.component.PrefFilePathField; import com.mucommander.ui.dialog.pref.component.PrefRadioButton; import com.mucommander.ui.layout.SpringUtilities; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.WindowManager; import static com.mucommander.conf.TcPreference.*; /** * 'Folders' preferences panel. * * @author Maxence Bernard, Mariuz Jakubowski */ class FoldersPanel extends PreferencesPanel implements ItemListener, KeyListener, ActionListener { // Startup folders private PrefRadioButton lastFoldersRadioButton; private PrefRadioButton customFoldersRadioButton; private PrefFilePathFieldWithDefaultValue leftCustomFolderTextField; private JButton leftCustomFolderButton; private PrefFilePathFieldWithDefaultValue rightCustomFolderTextField; private JButton rightCustomFolderButton; private QuickSearchTimeoutCombobox comboQuickSearchTimeout; /** Show hidden files? */ private PrefCheckBox cbShowHiddenFiles; /** Show Mac OS X .DS_Store? */ private PrefCheckBox cbShowDSStoreFiles; /** Show system folders ? */ private PrefCheckBox cbShowSystemFolders; /** Display compact file size ? */ private PrefCheckBox cbCompactSize; /** Follow symlinks when changing directory ? */ private PrefCheckBox cbFollowSymlinks; /** Always show single tab's header ? */ private PrefCheckBox cbShowTabHeader; /** Show quick search matches first in file panels */ private PrefCheckBox cbShowQuickSearchMatchesFirst; /** Calculate folder size on mark action */ private PrefCheckBox cbCalculateFolderSizeOnMark; FoldersPanel(PreferencesDialog parent) { super(parent, Translator.get("prefs_dialog.folders_tab")); setLayout(new BorderLayout()); // Startup folders panel YBoxPanel startupFolderPanel = new YBoxPanel(); startupFolderPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.startup_folders"))); // Last folders or custom folders selections lastFoldersRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.last_folder")) { public boolean hasChanged() { return !(isSelected() ? TcPreferences.STARTUP_FOLDERS_LAST : TcPreferences.STARTUP_FOLDERS_CUSTOM).equals( getVariable(STARTUP_FOLDERS)); } }; customFoldersRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.custom_folder")) { public boolean hasChanged() { return !(isSelected() ? TcPreferences.STARTUP_FOLDERS_CUSTOM : TcPreferences.STARTUP_FOLDERS_LAST).equals( getVariable(STARTUP_FOLDERS)); } }; startupFolderPanel.add(lastFoldersRadioButton); startupFolderPanel.addSpace(5); startupFolderPanel.add(customFoldersRadioButton); ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(lastFoldersRadioButton); buttonGroup.add(customFoldersRadioButton); customFoldersRadioButton.addItemListener(this); // Custom folders specification JLabel leftFolderLabel = new JLabel(Translator.get("prefs_dialog.left_folder")); leftFolderLabel.setAlignmentX(LEFT_ALIGNMENT); JLabel rightFolderLabel = new JLabel(Translator.get("prefs_dialog.right_folder")); rightFolderLabel.setAlignmentX(LEFT_ALIGNMENT); // Panel that contains the text field and button for specifying custom left folder XBoxPanel leftCustomFolderSpecifyingPanel = new XBoxPanel(5); leftCustomFolderSpecifyingPanel.setAlignmentX(LEFT_ALIGNMENT); // create a path field with auto-completion capabilities leftCustomFolderTextField = new PrefFilePathFieldWithDefaultValue(true); leftCustomFolderTextField.addKeyListener(this); leftCustomFolderSpecifyingPanel.add(leftCustomFolderTextField); leftCustomFolderButton = new JButton("..."); leftCustomFolderButton.addActionListener(this); leftCustomFolderSpecifyingPanel.add(leftCustomFolderButton); // Panel that contains the text field and button for specifying custom right folder XBoxPanel rightCustomFolderSpecifyingPanel = new XBoxPanel(5); rightCustomFolderSpecifyingPanel.setAlignmentX(LEFT_ALIGNMENT); // create a path field with auto-completion capabilities rightCustomFolderTextField = new PrefFilePathFieldWithDefaultValue(false); rightCustomFolderTextField.addKeyListener(this); rightCustomFolderSpecifyingPanel.add(rightCustomFolderTextField); rightCustomFolderButton = new JButton("..."); rightCustomFolderButton.addActionListener(this); rightCustomFolderSpecifyingPanel.add(rightCustomFolderButton); JPanel container = new JPanel(new SpringLayout()); container.add(leftFolderLabel); container.add(leftCustomFolderSpecifyingPanel); container.add(rightFolderLabel); container.add(rightCustomFolderSpecifyingPanel); //Lay out the panel. SpringUtilities.makeCompactGrid(container, 2, 2, // rows, cols 20, 6, // initX, initY 6, 6); // xPad, yPad startupFolderPanel.add(container); if (getVariable(STARTUP_FOLDERS, "").equals(TcPreferences.STARTUP_FOLDERS_LAST)) { lastFoldersRadioButton.setSelected(true); setCustomFolderComponentsEnabled(false); } else { customFoldersRadioButton.setSelected(true); } // -------------------------------------------------------------------------------------------------------------- YBoxPanel northPanel = new YBoxPanel(); northPanel.add(startupFolderPanel); northPanel.addSpace(5); // ------- Quick search panel -------- JPanel pnlQuickSearch = new JPanel(new SpringLayout()); pnlQuickSearch.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.quick_search"))); comboQuickSearchTimeout = new QuickSearchTimeoutCombobox(); JLabel lblQuickSearchTimeout = new JLabel(Translator.get("prefs_dialog.quick_search_timeout")); lblQuickSearchTimeout.setAlignmentX(LEFT_ALIGNMENT); pnlQuickSearch.add(lblQuickSearchTimeout); pnlQuickSearch.add(comboQuickSearchTimeout); cbShowQuickSearchMatchesFirst = new PrefCheckBox(Translator.get("prefs_dialog.show_quick_search_matches_first"), checkBox -> checkBox.isSelected() != getVariable(SHOW_QUICK_SEARCH_MATCHES_FIRST, TcPreferences.DEFAULT_SHOW_QUICK_SEARCH_MATCHES_FIRST)); cbShowQuickSearchMatchesFirst.setSelected(getVariable(SHOW_QUICK_SEARCH_MATCHES_FIRST, TcPreferences.DEFAULT_SHOW_QUICK_SEARCH_MATCHES_FIRST)); pnlQuickSearch.add(cbShowQuickSearchMatchesFirst); pnlQuickSearch.add(Box.createHorizontalGlue()); SpringUtilities.makeCompactGrid(pnlQuickSearch, 2, 2, // rows, cols 6, 6, // initX, initY 6, 6); // xPad, yPad northPanel.add(pnlQuickSearch); northPanel.addSpace(10); // ----- checkboxes ---- cbShowHiddenFiles = new PrefCheckBox(Translator.get("prefs_dialog.show_hidden_files"), checkBox -> checkBox.isSelected() != getVariable(SHOW_HIDDEN_FILES, TcPreferences.DEFAULT_SHOW_HIDDEN_FILES)); cbShowHiddenFiles.setSelected(getVariable(SHOW_HIDDEN_FILES, TcPreferences.DEFAULT_SHOW_HIDDEN_FILES)); northPanel.add(cbShowHiddenFiles); // Mac OS X-only options if (OsFamily.MAC_OS_X.isCurrent()) { // Monitor cbShowHiddenFiles state to disable 'show .DS_Store files' option // when 'Show hidden files' is disabled, as .DS_Store files are hidden files cbShowHiddenFiles.addItemListener(this); cbShowDSStoreFiles = new PrefCheckBox(Translator.get("prefs_dialog.show_ds_store_files"), checkBox -> checkBox.isSelected() != getVariable(SHOW_DS_STORE_FILES, TcPreferences.DEFAULT_SHOW_DS_STORE_FILES)); cbShowDSStoreFiles.setSelected(getVariable(SHOW_DS_STORE_FILES, TcPreferences.DEFAULT_SHOW_DS_STORE_FILES)); cbShowDSStoreFiles.setEnabled(cbShowHiddenFiles.isSelected()); // Shift the check box to the right to indicate that it is a sub-option northPanel.add(cbShowDSStoreFiles, 20); } if (OsFamily.MAC_OS_X.isCurrent() || OsFamily.WINDOWS.isCurrent()) { cbShowSystemFolders = new PrefCheckBox(Translator.get("prefs_dialog.show_system_folders"), checkBox -> checkBox.isSelected() != getVariable(SHOW_SYSTEM_FOLDERS, TcPreferences.DEFAULT_SHOW_SYSTEM_FOLDERS)); cbShowSystemFolders.setSelected(getVariable(SHOW_SYSTEM_FOLDERS, TcPreferences.DEFAULT_SHOW_SYSTEM_FOLDERS)); northPanel.add(cbShowSystemFolders); } cbCompactSize = new PrefCheckBox(Translator.get("prefs_dialog.compact_file_size"), checkBox -> checkBox.isSelected() != getVariable(DISPLAY_COMPACT_FILE_SIZE, TcPreferences.DEFAULT_DISPLAY_COMPACT_FILE_SIZE)); cbCompactSize.setSelected(getVariable(DISPLAY_COMPACT_FILE_SIZE, TcPreferences.DEFAULT_DISPLAY_COMPACT_FILE_SIZE)); northPanel.add(cbCompactSize); cbFollowSymlinks = new PrefCheckBox(Translator.get("prefs_dialog.follow_symlinks_when_cd"), checkBox -> checkBox.isSelected() != getVariable(CD_FOLLOWS_SYMLINKS, TcPreferences.DEFAULT_CD_FOLLOWS_SYMLINKS)); cbFollowSymlinks.setSelected(getVariable(CD_FOLLOWS_SYMLINKS, TcPreferences.DEFAULT_CD_FOLLOWS_SYMLINKS)); northPanel.add(cbFollowSymlinks); cbShowTabHeader = new PrefCheckBox(Translator.get("prefs_dialog.show_tab_header"), checkBox -> checkBox.isSelected() != getVariable(SHOW_TAB_HEADER, TcPreferences.DEFAULT_SHOW_TAB_HEADER)); cbShowTabHeader.setSelected(getVariable(SHOW_TAB_HEADER, TcPreferences.DEFAULT_SHOW_TAB_HEADER)); northPanel.add(cbShowTabHeader); cbCalculateFolderSizeOnMark = new PrefCheckBox(Translator.get("prefs_dialog.calculate_folder_size_on_mark"), checkBox -> checkBox.isSelected() != getVariable(CALCULATE_FOLDER_SIZE_ON_MARK, TcPreferences.DEFAULT_CALCULATE_FOLDER_SIZE_ON_MARK)); cbCalculateFolderSizeOnMark.setSelected(getVariable(CALCULATE_FOLDER_SIZE_ON_MARK, TcPreferences.DEFAULT_CALCULATE_FOLDER_SIZE_ON_MARK)); northPanel.add(cbCalculateFolderSizeOnMark); add(northPanel, BorderLayout.NORTH); lastFoldersRadioButton.addDialogListener(parent); customFoldersRadioButton.addDialogListener(parent); rightCustomFolderTextField.addDialogListener(parent); leftCustomFolderTextField.addDialogListener(parent); cbShowHiddenFiles.addDialogListener(parent); cbCompactSize.addDialogListener(parent); cbFollowSymlinks.addDialogListener(parent); cbShowTabHeader.addDialogListener(parent); cbShowQuickSearchMatchesFirst.addDialogListener(parent); cbCalculateFolderSizeOnMark.addDialogListener(parent); comboQuickSearchTimeout.addDialogListener(parent); if (OsFamily.MAC_OS_X.isCurrent()) { cbShowDSStoreFiles.addDialogListener(parent); } if (OsFamily.MAC_OS_X.isCurrent() || OsFamily.WINDOWS.isCurrent()) { cbShowSystemFolders.addDialogListener(parent); } } private void setCustomFolderComponentsEnabled(boolean enabled) { leftCustomFolderTextField.setEnabled(enabled); leftCustomFolderButton.setEnabled(enabled); rightCustomFolderTextField.setEnabled(enabled); rightCustomFolderButton.setEnabled(enabled); } ///////////////////////////////////// // PreferencesPanel implementation // ///////////////////////////////////// @Override protected void commit() { final TcPreferencesAPI pref = TcConfigurations.getPreferences(); pref.setVariable(STARTUP_FOLDERS, lastFoldersRadioButton.isSelected() ? TcPreferences.STARTUP_FOLDERS_LAST : TcPreferences.STARTUP_FOLDERS_CUSTOM); pref.setVariable(LEFT_CUSTOM_FOLDER, leftCustomFolderTextField.getFilePath()); pref.setVariable(RIGHT_CUSTOM_FOLDER, rightCustomFolderTextField.getFilePath()); pref.setVariable(DISPLAY_COMPACT_FILE_SIZE, cbCompactSize.isSelected()); pref.setVariable(CD_FOLLOWS_SYMLINKS, cbFollowSymlinks.isSelected()); pref.setVariable(SHOW_TAB_HEADER, cbShowTabHeader.isSelected()); pref.setVariable(SHOW_QUICK_SEARCH_MATCHES_FIRST, cbShowQuickSearchMatchesFirst.isSelected()); pref.setVariable(CALCULATE_FOLDER_SIZE_ON_MARK, cbCalculateFolderSizeOnMark.isSelected()); pref.setVariable(QUICK_SEARCH_TIMEOUT, comboQuickSearchTimeout.getMilliseconds()); // If one of the show/hide file filters have changed, refresh current folders of current MainFrame boolean refreshFolders = pref.setVariable(SHOW_HIDDEN_FILES, cbShowHiddenFiles.isSelected()); if (OsFamily.MAC_OS_X.isCurrent()) { refreshFolders |= pref.setVariable(SHOW_DS_STORE_FILES, cbShowDSStoreFiles.isSelected()); } if (OsFamily.MAC_OS_X.isCurrent() || OsFamily.WINDOWS.isCurrent()) { refreshFolders |= pref.setVariable(SHOW_SYSTEM_FOLDERS, cbShowSystemFolders.isSelected()); } if (refreshFolders) { WindowManager.tryRefreshCurrentFolders(); } } ///////////////////////////////// // ItemListener implementation // ///////////////////////////////// public void itemStateChanged(ItemEvent event) { Object source = event.getSource(); // Disable 'show .DS_Store files' option when 'Show hidden files' is disabled, as .DS_Store files are hidden files if (source == cbShowHiddenFiles) { cbShowDSStoreFiles.setEnabled(cbShowHiddenFiles.isSelected()); } else if (source == customFoldersRadioButton) { setCustomFolderComponentsEnabled(customFoldersRadioButton.isSelected()); } } //////////////////////////////// // KeyListener implementation // //////////////////////////////// /** * Catches key events to automatically select custom folder radio button if it was not already selected. */ public void keyTyped(KeyEvent e) { Object source = e.getSource(); if (source==leftCustomFolderTextField || source==rightCustomFolderTextField) { if (!customFoldersRadioButton.isSelected()) { customFoldersRadioButton.setSelected(true); } } } public void keyPressed(KeyEvent e) { } public void keyReleased(KeyEvent e) { } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// /** * Opens dialog for selecting starting folder. */ public void actionPerformed(ActionEvent e) { Object source = e.getSource(); JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.setDialogTitle(Translator.get("choose_folder")); chooser.setDialogType(JFileChooser.OPEN_DIALOG); if (chooser.showDialog(parent, Translator.get("choose")) == JFileChooser.APPROVE_OPTION) { File file = chooser.getSelectedFile(); if (source == leftCustomFolderButton) { leftCustomFolderTextField.setText(file.getPath()); if (!customFoldersRadioButton.isSelected()) { customFoldersRadioButton.setSelected(true); } } else if (source == rightCustomFolderButton) { rightCustomFolderTextField.setText(file.getPath()); if (!customFoldersRadioButton.isSelected()) { customFoldersRadioButton.setSelected(true); } } } } public class PrefFilePathFieldWithDefaultValue extends PrefFilePathField { private boolean isLeft; private final String HOME_FOLDER_PATH = System.getProperty("user.home"); PrefFilePathFieldWithDefaultValue(boolean isLeft) { super(isLeft ? getVariable(LEFT_CUSTOM_FOLDER, "") : getVariable(RIGHT_CUSTOM_FOLDER, "")); this.isLeft = isLeft; // setUI(new HintTextFieldUI(HOME_FOLDER_PATH, true)); } public boolean hasChanged() { return isLeft ? !getText().equals(getVariable(LEFT_CUSTOM_FOLDER)) : !getText().equals(getVariable(RIGHT_CUSTOM_FOLDER)); } String getFilePath() { String text = super.getText(); return text.trim().isEmpty() ? HOME_FOLDER_PATH : text; } private static class HintTextFieldUI extends BasicTextFieldUI implements FocusListener { private String hint; private boolean hideOnFocus; private Color color; public Color getColor() { return color; } public void setColor(Color color) { this.color = color; repaint(); } private void repaint() { if (getComponent() != null) { getComponent().repaint(); } } public boolean isHideOnFocus() { return hideOnFocus; } public void setHideOnFocus(boolean hideOnFocus) { this.hideOnFocus = hideOnFocus; repaint(); } public String getHint() { return hint; } public void setHint(String hint) { this.hint = hint; repaint(); } public HintTextFieldUI(String hint) { this(hint,false); } HintTextFieldUI(String hint, boolean hideOnFocus) { this(hint,hideOnFocus, Color.gray); } HintTextFieldUI(String hint, boolean hideOnFocus, Color color) { this.hint = hint; this.hideOnFocus = hideOnFocus; this.color = color; } @Override protected void paintSafely(Graphics g) { super.paintSafely(g); JTextComponent comp = getComponent(); if (hint != null && comp.getText().isEmpty() && (!(hideOnFocus && comp.hasFocus()))) { g.setColor(color != null ? color : comp.getForeground().brighter().brighter().brighter()); int padding = (comp.getHeight() - comp.getFont().getSize())/2; g.drawString(hint, 3, comp.getHeight()-padding-1); } } public void focusGained(FocusEvent e) { if (hideOnFocus) { repaint(); } } public void focusLost(FocusEvent e) { if (hideOnFocus) { repaint(); } } @Override protected void installListeners() { super.installListeners(); getComponent().addFocusListener(this); } @Override protected void uninstallListeners() { super.uninstallListeners(); getComponent().removeFocusListener(this); } } } private class QuickSearchTimeoutCombobox extends PrefComboBox { QuickSearchTimeoutCombobox() { super(); addItem(Translator.get("prefs_dialog.quick_search_timeout_never")); int selectedIndex = 0; long prefVal = getPrefValue(); int step = 1; for (int i = 1; i <= 60; i += step) { if (i == 5) { step = 5; } else if (i == 30) { step = 10; } addItem(i + " " + Translator.get("prefs_dialog.quick_search_timeout_sec")); if (i == prefVal/1000) { selectedIndex = getItemCount()-1; } } setSelectedIndex(selectedIndex); } long getPrefValue() { return TcConfigurations.getPreferences().getVariable(TcPreference.QUICK_SEARCH_TIMEOUT, TcPreferences.DEFAULT_QUICK_SEARCH_TIMEOUT); } long getMilliseconds() { if (getSelectedIndex() == 0) { return 0; } String val = getSelectedItem(); return Integer.parseInt(val.substring(0, val.indexOf(' '))) * 1000; } @Override public boolean hasChanged() { return getMilliseconds() != getPrefValue(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/GeneralPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.BorderLayout; import java.awt.Component; import java.awt.FlowLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.ButtonGroup; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.PlainDocument; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.CustomDateFormat; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.dialog.pref.component.PrefCheckBox; import com.mucommander.ui.dialog.pref.component.PrefComboBox; import com.mucommander.ui.dialog.pref.component.PrefRadioButton; import com.mucommander.ui.dialog.pref.component.PrefTextField; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.widgets.render.BasicComboBoxRenderer; /** * 'General' preferences panel. * * @author Maxence Bernard */ class GeneralPanel extends PreferencesPanel implements ItemListener, ActionListener, DocumentListener { // Language private final List languages; private final PrefComboBox languageComboBox; // Date/time format private final PrefRadioButton time12RadioButton; private final PrefComboBox dateFormatComboBox; private final PrefTextField dateSeparatorField; private final PrefCheckBox showSecondsCheckBox; private final PrefCheckBox showCenturyCheckBox; private final JLabel previewLabel; private final Date exampleDate; private final static String DAY = Translator.get("prefs_dialog.day"); private final static String MONTH = Translator.get("prefs_dialog.month"); private final static String YEAR = Translator.get("prefs_dialog.year"); private final static String[] DATE_FORMAT_LABELS = { MONTH+"/"+DAY+"/"+YEAR, DAY+"/"+MONTH+"/"+YEAR, YEAR+"/"+MONTH+"/"+DAY, MONTH+"/"+YEAR+"/"+DAY, DAY+"/"+YEAR+"/"+MONTH, YEAR+"/"+DAY+"/"+MONTH }; private final static String[] DATE_FORMATS = { "MM/dd/yy", "dd/MM/yy", "yy/MM/dd", "MM/yy/dd", "dd/yy/MM", "yy/dd/MM" }; private final static String[] DATE_FORMATS_WITH_CENTURY = { "MM/dd/yyyy", "dd/MM/yyyy", "yyyy/MM/dd", "MM/yyyy/dd", "dd/yyyy/MM", "yyyy/dd/MM" }; private final static String HOUR_12_TIME_FORMAT = "hh:mm a"; private final static String HOUR_12_TIME_FORMAT_WITH_SECONDS = "hh:mm:ss a"; private final static String HOUR_24_TIME_FORMAT = "HH:mm"; private final static String HOUR_24_TIME_FORMAT_WITH_SECONDS = "HH:mm:ss"; private final static String YYYY = "yyyy"; private class LangComboBox extends PrefComboBox { LangComboBox() { // Use a custom combo box renderer to display language icons class LanguageComboBoxRenderer extends BasicComboBoxRenderer { @Override public Component getListCellRendererComponent(JList list, Locale value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); label.setText(Translator.get("language." + value.toLanguageTag())); label.setIcon(IconManager.getIcon(IconManager.IconSet.LANGUAGE, value.toLanguageTag() + ".png")); return label; } } setRenderer(new LanguageComboBoxRenderer()); // Add combo items and select current language (defaults to EN if current language can't be found) for (Locale language : languages) { addItem(language); } Locale currentLang = Locale.forLanguageTag(getVariable(TcPreference.LANGUAGE)); setSelectedItem(currentLang); } public boolean hasChanged() { String lang = languages.get(getSelectedIndex()).toLanguageTag(); return !lang.equals(getVariable(TcPreference.LANGUAGE)); } } GeneralPanel(PreferencesDialog parent) { super(parent, Translator.get("prefs_dialog.general_tab")); setLayout(new BorderLayout()); YBoxPanel mainPanel = new YBoxPanel(); // Language JPanel languagePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); languagePanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.language"))); this.languages = Translator.getAvailableLanguages(); languageComboBox = new LangComboBox(); languagePanel.add(languageComboBox); mainPanel.add(languagePanel); mainPanel.addSpace(10); // Date & time format panel YBoxPanel dateTimeFormatPanel = new YBoxPanel(); dateTimeFormatPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.date_time"))); JPanel gridPanel = new JPanel(new GridLayout(1, 2)); YBoxPanel dateFormatPanel = new YBoxPanel(); dateFormatPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.date"))); // Date format combo dateFormatComboBox = new PrefComboBox() { public boolean hasChanged() { return !getDateFormatString().equals(getVariable(TcPreference.DATE_FORMAT)); } }; String dateFormat = getVariable(TcPreference.DATE_FORMAT); String separator = getVariable(TcPreference.DATE_SEPARATOR, TcPreferences.DEFAULT_DATE_SEPARATOR); int dateFormatIndex = getDateFormatIndex(dateFormat, separator); dateFormatComboBox.setSelectedIndex(dateFormatIndex); dateFormatComboBox.addItemListener(this); JPanel tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(dateFormatComboBox); tempPanel.add(Box.createHorizontalGlue()); dateFormatPanel.add(tempPanel); // Date separator field tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(Translator.get("prefs_dialog.date_separator")+": ")); dateSeparatorField = new PrefTextField(1) { public boolean hasChanged() { return !getText().equals(getVariable(TcPreference.DATE_SEPARATOR)); } }; // Limit the number of characters in the text field to 1 and enforces only non-alphanumerical characters PlainDocument doc = new PlainDocument() { @Override public void insertString(int param, String str, javax.swing.text.AttributeSet attributeSet) throws javax.swing.text.BadLocationException { // Limit field to 1 character max if (str != null && this.getLength() + str.length() > 1) { return; } // Reject letters and digits, as they don't make much sense, plus letters would be misinterpreted by SimpleDateFormat if (!strContainsLetterOrDigit(str)) { super.insertString(param, str, attributeSet); } } }; dateSeparatorField.setDocument(doc); dateSeparatorField.setText(separator); doc.addDocumentListener(this); tempPanel.add(dateSeparatorField); tempPanel.add(Box.createHorizontalGlue()); dateFormatPanel.add(tempPanel); showCenturyCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.show_century"), checkBox -> checkBox.isSelected() != getVariable(TcPreference.DATE_FORMAT).contains(YYYY)); showCenturyCheckBox.setSelected(dateFormat.contains(YYYY)); showCenturyCheckBox.addItemListener(this); dateFormatPanel.add(showCenturyCheckBox); dateFormatPanel.addSpace(10); gridPanel.add(dateFormatPanel); // Time format YBoxPanel timeFormatPanel = new YBoxPanel(); timeFormatPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.time"))); time12RadioButton = new PrefRadioButton(Translator.get("prefs_dialog.time_12_hour")) { public boolean hasChanged() { String timeFormat = getVariable(TcPreference.TIME_FORMAT); return isSelected() != (timeFormat.equals(HOUR_12_TIME_FORMAT) || timeFormat.equals(HOUR_12_TIME_FORMAT_WITH_SECONDS)); } }; time12RadioButton.addActionListener(this); PrefRadioButton time24RadioButton = new PrefRadioButton(Translator.get("prefs_dialog.time_24_hour")) { public boolean hasChanged() { String timeFormat = getVariable(TcPreference.TIME_FORMAT); return isSelected() != (timeFormat.equals(HOUR_24_TIME_FORMAT) || timeFormat.equals(HOUR_24_TIME_FORMAT_WITH_SECONDS)); } }; time24RadioButton.addActionListener(this); String timeFormat = getVariable(TcPreference.TIME_FORMAT); if (timeFormat.equals(HOUR_12_TIME_FORMAT) || timeFormat.equals(HOUR_12_TIME_FORMAT_WITH_SECONDS)) { time12RadioButton.setSelected(true); } else { time24RadioButton.setSelected(true); } ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(time12RadioButton); buttonGroup.add(time24RadioButton); timeFormatPanel.add(time12RadioButton); timeFormatPanel.add(time24RadioButton); timeFormatPanel.addSpace(10); showSecondsCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.show_seconds"), checkBox -> checkBox.isSelected() != getVariable(TcPreference.TIME_FORMAT).contains(":ss")); showSecondsCheckBox.setSelected(timeFormat.contains(":ss")); showSecondsCheckBox.addItemListener(this); timeFormatPanel.add(showSecondsCheckBox); timeFormatPanel.addSpace(10); gridPanel.add(timeFormatPanel); dateTimeFormatPanel.add(gridPanel); // Date/time preview tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(Translator.get("example")+": ")); previewLabel = new JLabel(); Calendar calendar = Calendar.getInstance(); calendar.set(calendar.get(Calendar.YEAR)-1, Calendar.DECEMBER, 31, 23, 59); exampleDate = calendar.getTime(); updatePreviewLabel(); tempPanel.add(previewLabel); dateTimeFormatPanel.add(tempPanel); mainPanel.add(dateTimeFormatPanel); add(mainPanel, BorderLayout.NORTH); languageComboBox.addDialogListener(parent); time12RadioButton.addDialogListener(parent); time24RadioButton.addDialogListener(parent); dateFormatComboBox.addDialogListener(parent); dateSeparatorField.addDialogListener(parent); showSecondsCheckBox.addDialogListener(parent); showCenturyCheckBox.addDialogListener(parent); } private int getDateFormatIndex(String dateFormat, String separator) { int dateFormatIndex = 0; String buffer = dateFormat.replace(separator.charAt(0), '/'); for (int i = 0; i < DATE_FORMATS.length; i++) { dateFormatComboBox.addItem(DATE_FORMAT_LABELS[i]); if (buffer.equals(DATE_FORMATS[i]) || buffer.equals(DATE_FORMATS_WITH_CENTURY[i])) { dateFormatIndex = i; } } return dateFormatIndex; } private static boolean strContainsLetterOrDigit(String str) { if (str == null) { return false; } int len = str.length(); for (int i = 0; i < len; i++) { if (Character.isLetterOrDigit(str.charAt(i))) { return true; } } return false; } private String getTimeFormatString() { boolean showSeconds = showSecondsCheckBox.isSelected(); if (time12RadioButton.isSelected()) { return showSeconds ? HOUR_12_TIME_FORMAT_WITH_SECONDS : HOUR_12_TIME_FORMAT; } else { return showSeconds ? HOUR_24_TIME_FORMAT_WITH_SECONDS : HOUR_24_TIME_FORMAT; } } private String getDateFormatString() { int selectedIndex = dateFormatComboBox.getSelectedIndex(); return CustomDateFormat.replaceDateSeparator(showCenturyCheckBox.isSelected()?DATE_FORMATS_WITH_CENTURY[selectedIndex]:DATE_FORMATS[selectedIndex], dateSeparatorField.getText()); } private void updatePreviewLabel() { SimpleDateFormat dateFormat = new SimpleDateFormat(getDateFormatString() + " " + getTimeFormatString()); previewLabel.setText(dateFormat.format(exampleDate)); previewLabel.repaint(); } /////////////////////// // PrefPanel methods // /////////////////////// @Override protected void commit() { TcConfigurations.getPreferences().setVariable(TcPreference.LANGUAGE, languageComboBox.getSelectedItem().toLanguageTag()); TcConfigurations.getPreferences().setVariable(TcPreference.DATE_FORMAT, getDateFormatString()); TcConfigurations.getPreferences().setVariable(TcPreference.DATE_SEPARATOR, dateSeparatorField.getText()); TcConfigurations.getPreferences().setVariable(TcPreference.TIME_FORMAT, getTimeFormatString()); } ////////////////////////// // ItemListener methods // ////////////////////////// public void itemStateChanged(ItemEvent e) { updatePreviewLabel(); } //////////////////////////// // ActionListener methods // //////////////////////////// public void actionPerformed(ActionEvent e) { updatePreviewLabel(); } ////////////////////////////// // DocumentListener methods // ////////////////////////////// public void changedUpdate(DocumentEvent e) { updatePreviewLabel(); } public void insertUpdate(DocumentEvent e) { updatePreviewLabel(); } public void removeUpdate(DocumentEvent e) { updatePreviewLabel(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/GeneralPreferencesDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.*; import java.util.LinkedHashSet; import java.util.Set; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcSnapshot; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.component.PrefComponent; import com.mucommander.ui.dialog.pref.theme.ThemeEditorDialog; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.theme.ThemeManager; /** * This is the main preferences dialog that contains all preferences panels organized by tabs. * @author Maxence Bernard, Nicolas Rinaudo */ public class GeneralPreferencesDialog extends PreferencesDialog { /** Used to ensure we only have the one preferences dialog open at any given time. */ private static GeneralPreferencesDialog singleton; /** Stores the components in the dialog that were changed and their current value is different from their saved value at MuConfiguration **/ private final Set modifiedComponents = new LinkedHashSet<>(); /* Dialog's minimum dimensions. */ // private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(580, 300); /* Dialog's maximum dimensions. */ // private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(1024, 800); // - Available tabs --------------------------------------------------------- // -------------------------------------------------------------------------- // TODO use enum /** Identifier of the 'general' tab. */ public static final int GENERAL_TAB = 0; /** Identifier of the 'folders' tab. */ public static final int FOLDERS_TAB = 1; /** Identifier of the 'appearance' tab. */ public static final int APPEARANCE_TAB = 2; /** Identifier of the 'shortcuts' tab. */ public static final int SHORTCUTS_TAB = 3; /** Identifier of the 'mail' tab. */ public static final int MAIL_TAB = 4; /** Identifier of the 'misc' tab. */ public static final int MISC_TAB = 5; // - Tab icons -------------------------------------------------------------- // -------------------------------------------------------------------------- /** Name of the icon used by the 'general' tab. */ private static final String GENERAL_ICON = "general.png"; /** Name of the icon used by the 'folders' tab. */ private static final String FOLDERS_ICON = "folders.png"; /** Name of the icon used by the 'appearance' tab. */ private static final String APPEARANCE_ICON = "appearance.png"; /** Name of the icon used by the 'mail' tab. */ private static final String MAIL_ICON = "mail.png"; /** Name of the icon used by the 'misc' tab. */ private static final String MISC_ICON = "misc.png"; /** Name of the icon used by the 'shortcuts' tab. */ private static final String SHORTCUTS_ICON = "shortcuts.png"; /** Index of the tab that was last selected by the user. */ private static int lastTabIndex = 0; /** * Creates a new instance of the GeneralPreferencesDialog. */ private GeneralPreferencesDialog() { super(WindowManager.getCurrentMainFrame().getJFrame(), Translator.get("prefs_dialog.title")); // Adds the preference tabs. addPreferencesPanel(new GeneralPanel(this), GENERAL_ICON); addPreferencesPanel(new FoldersPanel(this), FOLDERS_ICON); addPreferencesPanel(new AppearancePanel(this), APPEARANCE_ICON); addPreferencesPanel(new ShortcutsPanel(this), SHORTCUTS_ICON); addPreferencesPanel(new MailPanel(this), MAIL_ICON); addPreferencesPanel(new MiscPanel(this), MISC_ICON); // Sets the dialog's size. Dimension screenSize = TcSnapshot.getScreenSize(); Dimension minimumSize = new Dimension(580, 300); Dimension maximumSize = new Dimension(screenSize); if (screenSize.getWidth() >= 1024 && screenSize.getHeight() > 700) { minimumSize.setSize(640, 480); } setMinimumSize(minimumSize); setMaximumSize(maximumSize); // Restores the last selected index. setActiveTab(lastTabIndex); // prepare ThemeEditorDialog // because first construction of this object can take time (about 500-1000 ms) // we make that to reduce delay on show theme editor dialog new Thread(() -> new ThemeEditorDialog((Dialog) null, ThemeManager.getCurrentTheme())).start(); } /** * Commits the changes and writes the configuration file if necessary. */ @Override public void commit() { super.commit(); try { TcConfigurations.savePreferences(); } catch(Exception e) { InformationDialog.showErrorDialog(this); } } /** * Releases the singleton. */ @Override public void dispose() { releaseSingleton(getSelectedPanelIndex()); super.dispose(); } // - Singleton management --------------------------------------------------- // -------------------------------------------------------------------------- /** * Returns an instance of GeneralPreferencesDialog. *

    * This will not necessarily create a new instance - if a dialog is already in use, it * will be returned. This is an attempt to ensure that the preferences dialog is not opened * more than once. * * @return an instance of GeneralPreferencesDialog. */ public static synchronized GeneralPreferencesDialog getDialog() { // If no instance already exists, create a new one. if(singleton == null) singleton = new GeneralPreferencesDialog(); return singleton; } /** * Releases the singleton. *

    * After this method has been called, calls to {@link #getDialog()} will * result in creating a new instance of GeneralPreferencesDialog. * * @param lastTab index of the last selected panel. */ private static synchronized void releaseSingleton(int lastTab) { singleton = null; lastTabIndex = lastTab; } @Override public void componentChanged(PrefComponent component) { if (component.hasChanged()) { modifiedComponents.add(component); } else { modifiedComponents.remove(component); } setCommitButtonsEnabled(!modifiedComponents.isEmpty()); } @Override public void setCommitButtonsEnabled(boolean enable) { super.setCommitButtonsEnabled(enable); // if "commit buttons" are disabled that's mean that there is no change in any component // located in this dialog => we can clear the list of modified components in this dialog. if (!enable) { modifiedComponents.clear(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/MailPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.BorderLayout; import javax.swing.BorderFactory; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.dialog.pref.component.PrefTextField; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; /** * 'Mail' preferences panel. * * @author Maxence Bernard */ class MailPanel extends PreferencesPanel { /** Name of the user */ private final PrefTextField nameField; /** Email address of the user */ private final PrefTextField emailField; /** IP/hostname to the SMTP server */ private final PrefTextField smtpField; /** TCP port to the SMTP server */ private final PrefTextField portField; MailPanel(PreferencesDialog parent) { super(parent, Translator.get("prefs_dialog.mail_tab")); setLayout(new BorderLayout()); YBoxPanel mainPanel = new YBoxPanel(5); mainPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.mail_settings"))); // Text fields panel XAlignedComponentPanel compPanel = new XAlignedComponentPanel(); // Name field nameField = new PrefTextField(getVariable(TcPreference.MAIL_SENDER_NAME, "")) { public boolean hasChanged() { return !nameField.getText().equals(getVariable(TcPreference.MAIL_SENDER_NAME, "")); } }; compPanel.addRow(Translator.get("prefs_dialog.mail_name"), nameField, 10); // Email field emailField = new PrefTextField(getVariable(TcPreference.MAIL_SENDER_ADDRESS, "")) { public boolean hasChanged() { return !emailField.getText().equals(getVariable(TcPreference.MAIL_SENDER_ADDRESS, "")); } }; compPanel.addRow(Translator.get("prefs_dialog.mail_address"), emailField, 10); // SMTP field smtpField = new PrefTextField(getVariable(TcPreference.SMTP_SERVER, "")) { public boolean hasChanged() { return !smtpField.getText().equals(getVariable(TcPreference.SMTP_SERVER, "")); } }; compPanel.addRow(Translator.get("prefs_dialog.mail_server"), smtpField, 10); // SMTP port field portField = new PrefTextField(getVariable(TcPreference.SMTP_PORT, ""+TcPreferences.DEFAULT_SMTP_PORT)) { public boolean hasChanged() { return !portField.getText().equals(String.valueOf(getVariable(TcPreference.SMTP_PORT, TcPreferences.DEFAULT_SMTP_PORT))); } }; compPanel.addRow(Translator.get("server_connect_dialog.port"), portField, 10); mainPanel.add(compPanel, BorderLayout.NORTH); add(mainPanel, BorderLayout.NORTH); nameField.addDialogListener(parent); emailField.addDialogListener(parent); smtpField.addDialogListener(parent); portField.addDialogListener(parent); } @Override protected void commit() { final TcPreferencesAPI pref = TcConfigurations.getPreferences(); pref.setVariable(TcPreference.MAIL_SENDER_NAME, nameField.getText()); pref.setVariable(TcPreference.MAIL_SENDER_ADDRESS, emailField.getText()); pref.setVariable(TcPreference.SMTP_SERVER, smtpField.getText()); pref.setVariable(TcPreference.SMTP_PORT, portField.getText()); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/MiscPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.util.Objects; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import com.mucommander.bonjour.BonjourDirectory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreferences; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.desktop.DesktopManager; import com.mucommander.desktop.macos.OSXApplications; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.DialogOwner; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.dialog.pref.component.PrefCheckBox; import com.mucommander.ui.dialog.pref.component.PrefEncodingSelectBox; import com.mucommander.ui.dialog.pref.component.PrefFilePathField; import com.mucommander.ui.dialog.pref.component.PrefRadioButton; import com.mucommander.ui.dialog.pref.component.PrefTextField; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.notifier.AbstractNotifier; import static com.mucommander.conf.TcPreference.*; import static com.mucommander.conf.TcPreferences.TERMINAL_CUSTOM; import static com.mucommander.conf.TcPreferences.TERMINAL_DEFAULT; import static com.mucommander.conf.TcPreferences.TERMINAL_ITERM; /** * 'Misc' preferences panel. * * @author Maxence Bernard */ class MiscPanel extends PreferencesPanel implements ItemListener { /** Custom shell command text field */ private final PrefTextField customShellField; /** 'Use custom shell' radio button */ private final PrefRadioButton useCustomShellRadioButton; /** 'Check for updates on startup' checkbox */ private final PrefCheckBox checkForUpdatesCheckBox; /** 'Show confirmation dialog on quit' checkbox */ private final PrefCheckBox quitConfirmationCheckBox; /** 'Show splash screen' checkbox */ private final PrefCheckBox showSplashScreenCheckBox; /** 'Enable system notifications' checkbox */ private PrefCheckBox systemNotificationsCheckBox; /** 'Enable Bonjour services discovery' checkbox */ private final PrefCheckBox bonjourDiscoveryCheckBox; /** Shell encoding auto-detect checkbox */ private PrefCheckBox shellEncodingAutoDetectCheckbox; /** Shell encoding select box. */ private PrefEncodingSelectBox shellEncodingSelectBox; /** Custom external terminal command text field */ private final PrefTextField customExternalTerminalField; /** 'Use custom external terminal' radio button */ private final PrefRadioButton useCustomExternalTerminalRadioButton; private final PrefRadioButton useITermExternalTerminalRadioButton; /** Custom builtin terminal command text field */ private final PrefTextField customTerminalShellField; /** 'Use custom builtin terminal shell' radio button */ private final PrefRadioButton useCustomTerminalShellRadioButton; private JPanel createShellEncodingPanel(PreferencesDialog parent) { JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEADING)); shellEncodingAutoDetectCheckbox = new PrefCheckBox(Translator.get("prefs_dialog.auto_detect_shell_encoding"), checkBox -> checkBox.isSelected() != getVariable(AUTODETECT_SHELL_ENCODING, TcPreferences.DEFAULT_AUTODETECT_SHELL_ENCODING)); boolean autoDetect = getVariable(AUTODETECT_SHELL_ENCODING, TcPreferences.DEFAULT_AUTODETECT_SHELL_ENCODING); shellEncodingAutoDetectCheckbox.setSelected(autoDetect); shellEncodingAutoDetectCheckbox.addItemListener(this); panel.add(shellEncodingAutoDetectCheckbox); shellEncodingSelectBox = new PrefEncodingSelectBox(new DialogOwner(parent), getVariable(SHELL_ENCODING)) { public boolean hasChanged() { return !getVariable(SHELL_ENCODING).equals(getSelectedEncoding()); } }; shellEncodingSelectBox.setEnabled(!autoDetect); panel.add(shellEncodingSelectBox); return panel; } MiscPanel(PreferencesDialog parent) { super(parent, Translator.get("prefs_dialog.misc_tab")); setLayout(new BorderLayout()); YBoxPanel northPanel = new YBoxPanel(); JRadioButton useDefaultShellRadioButton = new JRadioButton(Translator.get("prefs_dialog.default_shell") + ':'); useCustomShellRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.custom_shell") + ':') { public boolean hasChanged() { return isSelected() != getVariable(USE_CUSTOM_SHELL, TcPreferences.DEFAULT_USE_CUSTOM_SHELL); } }; // Use system default or custom shell ? if (getVariable(USE_CUSTOM_SHELL, TcPreferences.DEFAULT_USE_CUSTOM_SHELL)) { useCustomShellRadioButton.setSelected(true); } else { useDefaultShellRadioButton.setSelected(true); } useCustomShellRadioButton.addItemListener(this); ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(useDefaultShellRadioButton); buttonGroup.add(useCustomShellRadioButton); // Shell panel XAlignedComponentPanel shellPanel = new XAlignedComponentPanel(); shellPanel.setLabelLeftAligned(true); shellPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.shell"))); // create a path field with auto-completion capabilities customShellField = new PrefFilePathField(getVariable(CUSTOM_SHELL, "")) { public boolean hasChanged() { return isEnabled() && !getText().equals(getVariable(CUSTOM_SHELL)); } }; customShellField.setEnabled(useCustomShellRadioButton.isSelected()); shellPanel.addRow(useDefaultShellRadioButton, new JLabel(DesktopManager.getDefaultShell()), 5); shellPanel.addRow(useCustomShellRadioButton, customShellField, 10); shellPanel.addRow(Translator.get("prefs_dialog.shell_encoding"), createShellEncodingPanel(parent), 5); northPanel.add(shellPanel, 5); northPanel.addSpace(10); // External Terminal panel XAlignedComponentPanel externalTerminalPanel = new XAlignedComponentPanel(); externalTerminalPanel.setLabelLeftAligned(true); externalTerminalPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.external_terminal"))); PrefRadioButton useDefaultExternalTerminalRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.default_terminal") + ':') { @Override public boolean hasChanged() { return isSelected() != (getTerminalType() == TERMINAL_DEFAULT); } }; useCustomExternalTerminalRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.custom_terminal") + ':') { public boolean hasChanged() { return isSelected() != (getTerminalType() == TERMINAL_CUSTOM); } }; // create a path field with auto-completion capabilities customExternalTerminalField = new PrefFilePathField(getVariable(CUSTOM_EXTERNAL_TERMINAL, "")) { public boolean hasChanged() { return isEnabled() && !getText().equals(getVariable(CUSTOM_EXTERNAL_TERMINAL)); } }; externalTerminalPanel.addRow(useDefaultExternalTerminalRadioButton, new JLabel(DesktopManager.getDefaultTerminalAppCommand()), 5); if (OsFamily.getCurrent() == OsFamily.MAC_OS_X && OSXApplications.iTermInstalled()) { useITermExternalTerminalRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.iterm_terminal") + ':') { public boolean hasChanged() { return isSelected() != (getTerminalType() == TERMINAL_ITERM); } }; JLabel textField = new JLabel("open -a iTerm ."); externalTerminalPanel.addRow(useITermExternalTerminalRadioButton, textField, 10); useITermExternalTerminalRadioButton.addItemListener(this); } else { useITermExternalTerminalRadioButton = null; } externalTerminalPanel.addRow(useCustomExternalTerminalRadioButton, customExternalTerminalField, 10); northPanel.add(externalTerminalPanel, 5); northPanel.addSpace(10); // Use system default or custom external terminal ? switch (getTerminalType()) { case TERMINAL_CUSTOM: useCustomExternalTerminalRadioButton.setSelected(true); case TERMINAL_DEFAULT: useDefaultExternalTerminalRadioButton.setSelected(true); case TERMINAL_ITERM: Objects.requireNonNullElse(useITermExternalTerminalRadioButton, useDefaultExternalTerminalRadioButton).setSelected(true); } customExternalTerminalField.setEnabled(useCustomExternalTerminalRadioButton.isSelected()); useCustomExternalTerminalRadioButton.addItemListener(this); useDefaultShellRadioButton.addItemListener(this); buttonGroup = new ButtonGroup(); buttonGroup.add(useDefaultExternalTerminalRadioButton); buttonGroup.add(useCustomExternalTerminalRadioButton); if (useITermExternalTerminalRadioButton != null) { buttonGroup.add(useITermExternalTerminalRadioButton); } // Builtin Terminal panel XAlignedComponentPanel terminalPanel = new XAlignedComponentPanel(); terminalPanel.setLabelLeftAligned(true); terminalPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("prefs_dialog.builtin_terminal"))); JRadioButton useDefaultTerminalShellRadioButton = new JRadioButton(Translator.get("prefs_dialog.default_shell") + ':'); useCustomTerminalShellRadioButton = new PrefRadioButton(Translator.get("prefs_dialog.custom_shell") + ':') { public boolean hasChanged() { return isSelected() != getVariable(TERMINAL_USE_CUSTOM_SHELL, TcPreferences.DEFAULT_TERMINAL_USE_CUSTOM_SHELL); } }; // create a path field with auto-completion capabilities customTerminalShellField = new PrefFilePathField(getVariable(TERMINAL_SHELL, "")) { public boolean hasChanged() { return isEnabled() && !getText().equals(getVariable(TERMINAL_SHELL)); } }; terminalPanel.addRow(useDefaultTerminalShellRadioButton, new JLabel(DesktopManager.getDefaultTerminalShellCommand()), 5); terminalPanel.addRow(useCustomTerminalShellRadioButton, customTerminalShellField, 10); northPanel.add(terminalPanel, 5); northPanel.addSpace(10); // Use system default or custom builtin terminal ? if (getVariable(TERMINAL_USE_CUSTOM_SHELL, TcPreferences.DEFAULT_TERMINAL_USE_CUSTOM_SHELL)) useCustomTerminalShellRadioButton.setSelected(true); else useDefaultTerminalShellRadioButton.setSelected(true); customTerminalShellField.setEnabled(useCustomTerminalShellRadioButton.isSelected()); useCustomTerminalShellRadioButton.addItemListener(this); buttonGroup = new ButtonGroup(); buttonGroup.add(useCustomTerminalShellRadioButton); buttonGroup.add(useDefaultTerminalShellRadioButton); // 'Show splash screen' option showSplashScreenCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.show_splash_screen"), checkBox -> checkBox.isSelected() != getVariable(SHOW_SPLASH_SCREEN, TcPreferences.DEFAULT_SHOW_SPLASH_SCREEN)); showSplashScreenCheckBox.setSelected(getVariable(SHOW_SPLASH_SCREEN, TcPreferences.DEFAULT_SHOW_SPLASH_SCREEN)); northPanel.add(showSplashScreenCheckBox); // 'Check for updates on startup' option checkForUpdatesCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.check_for_updates_on_startup"), checkBox -> checkBox.isSelected() != getVariable(CHECK_FOR_UPDATE, TcPreferences.DEFAULT_CHECK_FOR_UPDATE)); checkForUpdatesCheckBox.setSelected(getVariable(CHECK_FOR_UPDATE, TcPreferences.DEFAULT_CHECK_FOR_UPDATE)); northPanel.add(checkForUpdatesCheckBox); // 'Show confirmation dialog on quit' option quitConfirmationCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.confirm_on_quit"), checkBox -> checkBox.isSelected() != getVariable(CONFIRM_ON_QUIT, TcPreferences.DEFAULT_CONFIRM_ON_QUIT)); quitConfirmationCheckBox.setSelected(getVariable(CONFIRM_ON_QUIT, TcPreferences.DEFAULT_CONFIRM_ON_QUIT)); northPanel.add(quitConfirmationCheckBox); // 'Enable system notifications' option, displayed only if current platform supports system notifications if (AbstractNotifier.isAvailable()) { systemNotificationsCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.enable_system_notifications")+" ("+AbstractNotifier.getNotifier().getPrettyName()+")", checkBox -> checkBox.isSelected() != getVariable(ENABLE_SYSTEM_NOTIFICATIONS, TcPreferences.DEFAULT_ENABLE_SYSTEM_NOTIFICATIONS)); systemNotificationsCheckBox.setSelected(getVariable(ENABLE_SYSTEM_NOTIFICATIONS, TcPreferences.DEFAULT_ENABLE_SYSTEM_NOTIFICATIONS)); northPanel.add(systemNotificationsCheckBox); } // 'Enable Bonjour services discovery' option bonjourDiscoveryCheckBox = new PrefCheckBox(Translator.get("prefs_dialog.enable_bonjour_discovery"), checkBox -> checkBox.isSelected() != getVariable(ENABLE_BONJOUR_DISCOVERY, TcPreferences.DEFAULT_ENABLE_BONJOUR_DISCOVERY)); bonjourDiscoveryCheckBox.setSelected(getVariable(ENABLE_BONJOUR_DISCOVERY, TcPreferences.DEFAULT_ENABLE_BONJOUR_DISCOVERY)); northPanel.add(bonjourDiscoveryCheckBox); add(northPanel, BorderLayout.NORTH); customShellField.addDialogListener(parent); useCustomShellRadioButton.addDialogListener(parent); useCustomExternalTerminalRadioButton.addDialogListener(parent); useDefaultExternalTerminalRadioButton.addDialogListener(parent); if (useITermExternalTerminalRadioButton != null) { useITermExternalTerminalRadioButton.addDialogListener(parent); } customExternalTerminalField.addDialogListener(parent); useCustomTerminalShellRadioButton.addDialogListener(parent); customTerminalShellField.addDialogListener(parent); checkForUpdatesCheckBox.addDialogListener(parent); quitConfirmationCheckBox.addDialogListener(parent); showSplashScreenCheckBox.addDialogListener(parent); bonjourDiscoveryCheckBox.addDialogListener(parent); shellEncodingAutoDetectCheckbox.addDialogListener(parent); shellEncodingSelectBox.addDialogListener(parent); if (systemNotificationsCheckBox != null) { systemNotificationsCheckBox.addDialogListener(parent); } } private static int getTerminalType() { return getVariable(EXTERNAL_TERMINAL_TYPE, TcPreferences.DEFAULT_TERMINAL_TYPE); } @Override public void itemStateChanged(ItemEvent e) { Object source = e.getSource(); if (source == useCustomShellRadioButton) { customShellField.setEnabled(useCustomShellRadioButton.isSelected()); } else if (source == shellEncodingAutoDetectCheckbox) { shellEncodingSelectBox.setEnabled(!shellEncodingAutoDetectCheckbox.isSelected()); } else if (source == useCustomExternalTerminalRadioButton) { customExternalTerminalField.setEnabled(useCustomExternalTerminalRadioButton.isSelected()); } else if (source == useCustomTerminalShellRadioButton) { customTerminalShellField.setEnabled(useCustomTerminalShellRadioButton.isSelected()); } } @Override protected void commit() { TcPreferencesAPI pref = TcConfigurations.getPreferences(); pref.setVariable(CHECK_FOR_UPDATE, checkForUpdatesCheckBox.isSelected()); // Saves the shell data. pref.setVariable(USE_CUSTOM_SHELL, useCustomShellRadioButton.isSelected()); pref.setVariable(CUSTOM_SHELL, customShellField.getText()); // Saves the shell encoding data. boolean isAutoDetect = shellEncodingAutoDetectCheckbox.isSelected(); pref.setVariable(AUTODETECT_SHELL_ENCODING, isAutoDetect); if (!isAutoDetect) { pref.setVariable(SHELL_ENCODING, shellEncodingSelectBox.getSelectedEncoding()); } if (useCustomExternalTerminalRadioButton.isSelected()) { pref.setVariable(EXTERNAL_TERMINAL_TYPE, TERMINAL_CUSTOM); } else if (useITermExternalTerminalRadioButton != null && useITermExternalTerminalRadioButton.isSelected()) { pref.setVariable(EXTERNAL_TERMINAL_TYPE, TERMINAL_ITERM); } else { pref.setVariable(EXTERNAL_TERMINAL_TYPE, TERMINAL_DEFAULT); } pref.setVariable(CUSTOM_EXTERNAL_TERMINAL, customExternalTerminalField.getText()); pref.setVariable(TERMINAL_USE_CUSTOM_SHELL, useCustomTerminalShellRadioButton.isSelected()); pref.setVariable(TERMINAL_SHELL, customTerminalShellField.getText()); pref.setVariable(CONFIRM_ON_QUIT, quitConfirmationCheckBox.isSelected()); pref.setVariable(SHOW_SPLASH_SCREEN, showSplashScreenCheckBox.isSelected()); boolean enabled; if (systemNotificationsCheckBox != null) { enabled = systemNotificationsCheckBox.isSelected(); pref.setVariable(ENABLE_SYSTEM_NOTIFICATIONS, enabled); AbstractNotifier.getNotifier().setEnabled(enabled); } enabled = bonjourDiscoveryCheckBox.isSelected(); pref.setVariable(ENABLE_BONJOUR_DISCOVERY, enabled); BonjourDirectory.setActive(enabled); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/ShortcutsPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import com.mucommander.commons.util.StringUtils; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.*; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.text.KeyStrokeUtils; import com.mucommander.ui.theme.ThemeCache; import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.util.Locale; import static com.mucommander.conf.TcPreference.*; /** * 'Shortcuts' preferences panel. * * @author Arik Hadas, Johann Schmitz */ public class ShortcutsPanel extends PreferencesPanel { private static class Filter extends ShortcutsTable.ActionFilter { private ActionCategory actionCategory; private String text; Filter() { this.actionCategory = ActionCategory.ALL; } @Override public boolean accept(String actionId) { ActionDescriptor descriptor = ActionProperties.getActionDescriptor(actionId); boolean containsText = text == null || text.isEmpty() || descriptor.getDescription().toLowerCase().contains(text) || descriptor.getLabel().toLowerCase().contains(text); return actionCategory.contains(actionId) && containsText; } void setActionCategory(ActionCategory actionCategory) { this.actionCategory = actionCategory; } void setText(String text) { this.text = text != null ? text.toLowerCase() : null; } } private final Filter filter = new Filter(); // The table with action mappings private ShortcutsTable shortcutsTable; // Area in which tooltip texts and error messages are shown below the table private TooltipBar tooltipBar; ShortcutsPanel(PreferencesDialog parent) { super(parent, Translator.get("shortcuts_panel" + ".title")); initUI(); setPreferredSize(new Dimension(0, 0)); shortcutsTable.addDialogListener(parent); } private void initUI() { setLayout(new BorderLayout()); tooltipBar = new TooltipBar(); shortcutsTable = new ShortcutsTable(tooltipBar); add(createNorthPanel(), BorderLayout.NORTH); add(createCenterPanel(), BorderLayout.CENTER); add(createSouthPanel(), BorderLayout.SOUTH); } /** * Returns a panel that contains combo-box of action categories which is used for filtering * the actions shown at the shortcuts editor table. */ private JPanel createNorthPanel() { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createEmptyBorder()); panel.add(createFilteringPanel(), BorderLayout.WEST); return panel; } /** * Returns a panel that contains the shortcuts editor table. */ private JPanel createCenterPanel() { JPanel panel = new JPanel(new GridLayout(1,0)); shortcutsTable.setPreferredColumnWidths(new double[] {0.6, 0.2, 0.2}); panel.add(new JScrollPane(shortcutsTable)); return panel; } /** * Returns a panel that contain the tooltip bar and the shortcuts editor buttons below it. */ private JPanel createSouthPanel() { JPanel panel = new JPanel(); panel.setBorder(BorderFactory.createEmptyBorder()); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); JPanel tooltipBarPanel = new JPanel(new BorderLayout()); tooltipBarPanel.add(tooltipBar, BorderLayout.WEST); panel.add(tooltipBarPanel); panel.add(createButtonsPanel()); return panel; } private JPanel createButtonsPanel() { JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); panel.setBorder(BorderFactory.createEmptyBorder(0,0,0,5)); RemoveButton removeButton = new RemoveButton(); final JButton restoreDefaultButton = new JButton(); restoreDefaultButton.setAction(new AbstractAction(Translator.get("shortcuts_panel" + ".restore_defaults")) { public void actionPerformed(ActionEvent e) { shortcutsTable.restoreDefaults(); } }); panel.add(removeButton); panel.add(restoreDefaultButton); return panel; } private JPanel createFilteringPanel() { JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); panel.setBorder(BorderFactory.createEmptyBorder()); panel.add(new JLabel(Translator.get("shortcuts_panel.show") + ":")); final TcComboBox combo = new TcComboBox<>(); combo.addItem(ActionCategory.ALL); for (ActionCategory category : ActionProperties.getActionCategories()) { combo.addItem(category); } combo.addActionListener(e -> { filter.setActionCategory((ActionCategory)combo.getSelectedItem()); shortcutsTable.updateModel(filter); tooltipBar.showDefaultMessage(); }); combo.setSelectedIndex(0); panel.add(combo); panel.add(new JLabel(Translator.get("shortcuts_panel.search") + ":")); final JTextField searchText = new JTextField(16); panel.add(searchText); JTextField shortcutText = new JTextField(15); resetShortcutFilterText(shortcutText); shortcutText.setHorizontalAlignment(JTextField.CENTER); shortcutText.setEditable(false); shortcutText.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED]); shortcutText.setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED][ThemeCache.PLAIN_FILE]); // It is required to disable the traversal keys in order to support keys combination that include the TAB key setFocusTraversalKeysEnabled(false); panel.add(shortcutText); addCategoryFilter(searchText, combo, shortcutText); addSearchTextFilter(searchText, combo, shortcutText); addShortcutFilter(shortcutText); return panel; } private void resetShortcutFilterText(JTextField shortcutText) { shortcutText.setText(Translator.get("shortcuts_table.type_in_a_shortcut")); } private void addShortcutFilter(final JTextField shortcutText) { shortcutText.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent keyEvent) { int keyCode = keyEvent.getKeyCode(); if (keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_ALT || keyCode == KeyEvent.VK_META) { return; } final KeyStroke pressedKeyStroke = KeyStroke.getKeyStrokeForEvent(keyEvent); shortcutText.setText(KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(pressedKeyStroke)); updateFilter(pressedKeyStroke); keyEvent.consume(); } public void keyReleased(KeyEvent e) {e.consume();} public void keyTyped(KeyEvent e) {e.consume();} }); } private void resetShortcutFilterWhenFocusGained(JComponent componentGainingFocus, final JTextField searchText, final JComboBox categoryCombo, final JTextField shortcutText) { componentGainingFocus.addFocusListener(new FocusListener() { @Override public void focusLost(FocusEvent e) {} @Override public void focusGained(FocusEvent e) { updateFilter(searchText, categoryCombo, shortcutText); } }); } private void addCategoryFilter(final JTextField searchText, final JComboBox combo, final JTextField shortcutText) { combo.addActionListener(e -> updateFilter(searchText, combo, shortcutText)); resetShortcutFilterWhenFocusGained(combo, searchText, combo, shortcutText); } private void addSearchTextFilter(final JTextField searchText, final JComboBox categoryCombo, final JTextField shortcutText) { searchText.getDocument().addDocumentListener(new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { updateFilter(searchText, categoryCombo, shortcutText); } @Override public void insertUpdate(DocumentEvent e) { updateFilter(searchText, categoryCombo, shortcutText); } @Override public void changedUpdate(DocumentEvent e) { } }); resetShortcutFilterWhenFocusGained(searchText, searchText, categoryCombo, shortcutText); } private void updateFilter(final JTextField searchText, final JComboBox categoryCombo, JTextField shortcutText) { final ActionCategory selectedActionCategory = (ActionCategory) categoryCombo.getSelectedItem(); final String filterText = searchText.getText(); shortcutsTable.updateModel(new ShortcutsTable.ActionFilter() { final Locale currentLang = Locale.forLanguageTag(getVariable(LANGUAGE)); @Override public boolean accept(String actionId) { return selectedActionCategory.contains(actionId) && ( StringUtils.isNullOrBlank(filterText) || StringUtils.containsIgnoreCase(ActionProperties.getActionLabel(actionId), filterText, currentLang) || StringUtils.containsIgnoreCase(ActionProperties.getActionTooltip(actionId), filterText, currentLang) // also search for id's to find typical english computer terms even in non english languages || StringUtils.containsIgnoreCase(ActionProperties.getActionLabelKey(actionId), filterText, currentLang) || StringUtils.containsIgnoreCase(actionId, filterText, currentLang)); } }); resetShortcutFilterText(shortcutText); tooltipBar.showDefaultMessage(); } private void updateFilter(final KeyStroke pressedKeyStroke) { shortcutsTable.updateModel(shortcutsTable.createCurrentAcceleratorsActionFilter(pressedKeyStroke)); } /////////////////////// // PrefPanel methods // /////////////////////// @Override protected void commit() { shortcutsTable.commitChanges(); ActionKeymapIO.setModified(); } static class TooltipBar extends JLabel { private String lastActionTooltipShown; private final String DEFAULT_MESSAGE; private static final int MESSAGE_SHOWING_TIME = 3000; private MessageRemoverThread currentRemoverThread; TooltipBar() { DEFAULT_MESSAGE = Translator.get("shortcuts_panel.default_message"); Font tableFont = UIManager.getFont("TableHeader.font"); setFont(new Font(tableFont.getName(), Font.BOLD, tableFont.getSize())); setHorizontalAlignment(JLabel.LEFT); setBorder(BorderFactory.createEmptyBorder(3, 5, 3, 5)); setText(DEFAULT_MESSAGE); } void showActionTooltip(String text) { setText(lastActionTooltipShown = text == null ? " " : text); } void showDefaultMessage() { setText(DEFAULT_MESSAGE); } void showErrorMessage(String text) { setText(text); createMessageRemoverThread(); } private void createMessageRemoverThread() { if (currentRemoverThread != null) currentRemoverThread.neutralize(); (currentRemoverThread = new MessageRemoverThread()).start(); } private class MessageRemoverThread extends Thread { private boolean stopped = false; void neutralize() { stopped = true; } @Override public void run() { try { Thread.sleep(MESSAGE_SHOWING_TIME); } catch (InterruptedException ignore) { } if (!stopped) { showActionTooltip(lastActionTooltipShown); } } } } private class RemoveButton extends JButton implements ListSelectionListener, TableModelListener { RemoveButton() { setEnabled(false); setAction(new AbstractAction(Translator.get("remove")) { public void actionPerformed(ActionEvent e) { shortcutsTable.setValueAt(ShortcutsTable.DELETE, shortcutsTable.getSelectedRow(), shortcutsTable.getSelectedColumn()); shortcutsTable.repaint(); shortcutsTable.requestFocus(); } }); shortcutsTable.getSelectionModel().addListSelectionListener(this); shortcutsTable.getColumnModel().getSelectionModel().addListSelectionListener(this); shortcutsTable.getModel().addTableModelListener(this); } public void valueChanged(ListSelectionEvent e) { updateButtonState(); } public void tableChanged(TableModelEvent e) { updateButtonState(); } private void updateButtonState() { int column = shortcutsTable.getSelectedColumn(); int row = shortcutsTable.getSelectedRow(); boolean canRemove = (column == ShortcutsTable.ACCELERATOR_COLUMN_INDEX || column == ShortcutsTable.ALTERNATE_ACCELERATOR_COLUMN_INDEX) && row != -1 && shortcutsTable.getValueAt(shortcutsTable.getSelectedRow(), column) != null; setEnabled(canRemove); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/ShortcutsTable.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.image.BufferedImage; import java.util.*; import java.util.List; import javax.swing.BorderFactory; import javax.swing.DefaultCellEditor; import javax.swing.ImageIcon; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableModel; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.commons.util.Pair; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionDescriptor; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.dialog.pref.component.PrefTable; import com.mucommander.ui.dialog.pref.general.ShortcutsPanel.TooltipBar; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.table.CellLabel; import com.mucommander.ui.table.CenteredTableHeaderRenderer; import com.mucommander.ui.text.KeyStrokeUtils; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.ui.theme.ThemeListener; /** * This class is the table in which the actions and their shortcuts are * present in the ShortcutsPanel. * * @author Arik Hadas, Johann Schmitz (johann@j-schmitz.net) */ public class ShortcutsTable extends PrefTable implements KeyListener, ListSelectionListener, FocusListener { private static final Logger LOGGER = LoggerFactory.getLogger(ShortcutsTable.class); /** Base width and height of icons for a scale factor of 1 */ private final static int BASE_ICON_DIMENSION = 16; private static final Stroke DOTTED_BORDER_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER, 2.0f, new float[]{2.0f}, 0); /** Transparent icon used to align non-locked themes with the others. */ private static final ImageIcon transparentIcon = new ImageIcon(new BufferedImage(BASE_ICON_DIMENSION, BASE_ICON_DIMENSION, BufferedImage.TYPE_INT_ARGB)); /** Private object used to indicate that a delete operation was made */ public static final Object DELETE = new Object(); private final ShortcutsTableData data; /** Comparator of actions according to their labels */ private static final Comparator ACTIONS_COMPARATOR = (id1, id2) -> { String label1 = ActionProperties.getActionLabel(id1); if (label1 == null) return 1; String label2 = ActionProperties.getActionLabel(id2); if (label2 == null) return -1; return label1.compareTo(label2); }; /** Last selected row in the table */ private int lastSelectedRow = -1; /** The bar below the table in which messages can be displayed */ private final TooltipBar tooltipBar; /** Number of mouse clicks required to enter cell's editing state */ private static final int NUM_OF_CLICKS_TO_ENTER_EDITING_STATE = 2; /** Column indexes */ private static final int ACTION_DESCRIPTION_COLUMN_INDEX = 0; static final int ACCELERATOR_COLUMN_INDEX = 1; static final int ALTERNATE_ACCELERATOR_COLUMN_INDEX = 2; /** Number of columns in the table */ private static final int NUM_OF_COLUMNS = 3; /** After the following time (msec) that cell is being in editing state * and no pressing was made, the editing state is canceled */ private static final int CELL_EDITING_STATE_PERIOD = 3000; /** Thread that cancel cell's editing state after CELL_EDITING_STATE_PERIOD time */ private CancelEditingStateThread cancelEditingStateThread; private final ShortcutsTableCellRenderer cellRenderer; ShortcutsTable(TooltipBar tooltipBar) { super(); this.tooltipBar = tooltipBar; setModel(new KeymapTableModel(data = new ShortcutsTableData())); cellRenderer = new ShortcutsTableCellRenderer(); setShowGrid(false); setIntercellSpacing(new Dimension(0,0)); setRowHeight(Math.max(getRowHeight(), BASE_ICON_DIMENSION + 2 * CellLabel.CELL_BORDER_HEIGHT)); getTableHeader().setReorderingAllowed(false); setRowSelectionAllowed(false); setAutoCreateColumnsFromModel(false); setCellSelectionEnabled(false); setColumnSelectionAllowed(false); setDragEnabled(false); FontMetrics fm = getFontMetrics(getFont()); int fontHeight = fm.getHeight(); setRowHeight(fontHeight); if (!usesTableHeaderRenderingProperties()) { CenteredTableHeaderRenderer renderer = new CenteredTableHeaderRenderer(); getColumnModel().getColumn(ACTION_DESCRIPTION_COLUMN_INDEX).setHeaderRenderer(renderer); getColumnModel().getColumn(ACCELERATOR_COLUMN_INDEX).setHeaderRenderer(renderer); getColumnModel().getColumn(ALTERNATE_ACCELERATOR_COLUMN_INDEX).setHeaderRenderer(renderer); } putClientProperty("terminateEditOnFocusLost", Boolean.TRUE); addKeyListener(this); addFocusListener(this); getSelectionModel().addListSelectionListener(this); getColumnModel().getSelectionModel().addListSelectionListener(this); } /** * Paints a dotted border of the specified width, height and {@link Color}, and using the given {@link Graphics} * object. * * @param g Graphics object to use for painting * @param width border width * @param height border height * @param color border color */ private static void paintDottedBorder(Graphics g, int width, int height, Color color) { Graphics2D g2 = (Graphics2D) g; g2.setStroke(DOTTED_BORDER_STROKE); g2.setColor(color); g2.drawLine(0, 0, width, 0); g2.drawLine(0, height - 1, width, height - 1); g2.drawLine(0, 0, 0, height - 1); g2.drawLine(width-1, 0, width-1, height - 1); } private static boolean usesTableHeaderRenderingProperties() { return OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher(); } /** * Assumes table is contained in a JScrollPane. Scrolls the * cell (rowIndex, vColIndex) so that it is visible within the viewport. */ private void scrollToVisible(int rowIndex, int vColIndex) { if (!(getParent() instanceof JViewport viewport)) { return; } // This rectangle is relative to the table where the // northwest corner of cell (0,0) is always (0,0). Rectangle rect = getCellRect(rowIndex, vColIndex, true); // The location of the viewport relative to the table Point pt = viewport.getViewPosition(); // Translate the cell location so that it is relative // to the view, assuming the northwest corner of the // view is (0,0) rect.setLocation(rect.x-pt.x, rect.y-pt.y); // Scroll the area into view viewport.scrollRectToVisible(rect); } /** * create thread that will cancel the editing state of the given TableCellEditor * after CELL_EDITING_STATE_PERIOD time in which with no pressing was made. */ private void createCancelEditingStateThread(TableCellEditor cellEditor) { if (cancelEditingStateThread != null) { cancelEditingStateThread.neutralize(); } (cancelEditingStateThread = new CancelEditingStateThread(cellEditor)).start(); } @Override public TableCellRenderer getCellRenderer(int row, int column) { return cellRenderer; } @Override public void valueChanged(ListSelectionEvent e) { super.valueChanged(e); // Selection might be changed, update tooltip int selectedRow = getSelectedRow(); if (selectedRow == -1) {// no row is selected tooltipBar.showDefaultMessage(); } else { tooltipBar.showActionTooltip(data.getCurrentTooltip()); } } void updateModel(ActionFilter filter) { data.filter(filter); } ActionFilter createCurrentAcceleratorsActionFilter(KeyStroke accelerator) { return data.new CurrentActionAccceleratorsFilter(accelerator); } /** * Override this method so that calls for SetModel function outside this class * won't get to setModel(KeymapTableModel model) function. */ @Override public void setModel(@NotNull TableModel model) { super.setModel(model); } public boolean hasChanged() { return data.hasChanged(); } @Override public TableCellEditor getCellEditor(int row, int column) { return new KeyStrokeCellEditor(new RecordingKeyStrokeField((KeyStroke) getValueAt(row, column))); } /** * This method updates ActionKeymap with the modified shortcuts. */ void commitChanges() { data.submitChanges(); } void restoreDefaults() { data.restoreDefaultAccelerators(); } @Override public void focusGained(FocusEvent e) { int currentSelectedRow = getSelectedRow(); if (lastSelectedRow != currentSelectedRow) tooltipBar.showActionTooltip(data.getCurrentTooltip()); lastSelectedRow = currentSelectedRow; } public void focusLost(FocusEvent e) { } @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_ENTER) { if (editCellAt(getSelectedRow(), getSelectedColumn())) getEditorComponent().requestFocusInWindow(); e.consume(); } else if (keyCode == KeyEvent.VK_DELETE || keyCode == KeyEvent.VK_BACK_SPACE) { setValueAt(DELETE, getSelectedRow(), getSelectedColumn()); repaint(); e.consume(); } else if (keyCode != KeyEvent.VK_LEFT && keyCode != KeyEvent.VK_RIGHT && keyCode != KeyEvent.VK_UP && keyCode != KeyEvent.VK_DOWN && keyCode != KeyEvent.VK_HOME && keyCode != KeyEvent.VK_END && keyCode != KeyEvent.VK_F2 && keyCode != KeyEvent.VK_ESCAPE) e.consume(); } public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) {} public static abstract class ActionFilter { public abstract boolean accept(String actionId); } /** * Helper Classes */ private class KeyStrokeCellEditor extends DefaultCellEditor implements TableCellEditor { RecordingKeyStrokeField rec; KeyStrokeCellEditor(RecordingKeyStrokeField rec) { super(rec); this.rec = rec; rec.setSelectionColor(rec.getBackground()); rec.setSelectedTextColor(rec.getForeground()); rec.getDocument().addDocumentListener(new DocumentListener() { public void insertUpdate(DocumentEvent e) { // quit editing state after text is written to the text field. stopCellEditing(); } public void changedUpdate(DocumentEvent e) {} public void removeUpdate(DocumentEvent e) {} }); setClickCountToStart(NUM_OF_CLICKS_TO_ENTER_EDITING_STATE); createCancelEditingStateThread(this); } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int col) { return rec; } @Override public Object getCellEditorValue() { return rec.getLastKeyStroke(); } } private static class CancelEditingStateThread extends Thread { private boolean stopped = false; private final TableCellEditor cellEditor; CancelEditingStateThread(TableCellEditor cellEditor) { this.cellEditor = cellEditor; } void neutralize() { stopped = true; } @Override public void run() { try { Thread.sleep(CELL_EDITING_STATE_PERIOD); } catch (InterruptedException ignore) {} if (!stopped && cellEditor != null) cellEditor.stopCellEditing(); } } private class KeymapTableModel extends DefaultTableModel { private final ShortcutsTableData tableData; private KeymapTableModel(ShortcutsTableData data) { super(data.getTableData(), new String[] {Translator.get("shortcuts_table.action_description"), Translator.get("shortcuts_table.shortcut"), Translator.get("shortcuts_table.alternate_shortcut")}); this.tableData = data; } @Override public boolean isCellEditable(int row, int column) { return switch (column) { case ACTION_DESCRIPTION_COLUMN_INDEX -> false; case ACCELERATOR_COLUMN_INDEX, ALTERNATE_ACCELERATOR_COLUMN_INDEX -> true; default -> false; }; } @Override public Object getValueAt(int row, int column) { return tableData.getTableData(row, column); } @Override public void setValueAt(Object value, int row, int column) { // if no keystroke was pressed if (value == null) return; // if the user pressed a keystroke that is used to indicate a delete operation should be made else if (value == DELETE) value = null; KeyStroke typedKeyStroke = (KeyStroke) value; switch (column){ case ACCELERATOR_COLUMN_INDEX: tableData.setAccelerator(typedKeyStroke, row); break; case ALTERNATE_ACCELERATOR_COLUMN_INDEX: tableData.setAlternativeAccelerator(typedKeyStroke, row); break; default: LOGGER.debug("Unexpected column index: " + column); } fireTableCellUpdated(row, column); LOGGER.trace("Value: " + value + ", row: " + row + ", col: " + column); } } private class RecordingKeyStrokeField extends JTextField implements KeyListener { // The last KeyStroke that was entered to the field. // Before any keystroke is entered, it contains the keystroke appearing in the cell before entering the editing state. private KeyStroke lastKeyStroke; RecordingKeyStrokeField(KeyStroke currentKeyStroke) { super(Translator.get("shortcuts_table.type_in_a_shortcut")); lastKeyStroke = currentKeyStroke; setBorder(BorderFactory.createEmptyBorder()); setHorizontalAlignment(JTextField.CENTER); setEditable(false); setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED]); setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED][ThemeCache.PLAIN_FILE]); addKeyListener(this); // It is required to disable the traversal keys in order to support keys combination that include the TAB key setFocusTraversalKeysEnabled(false); } /** * * @return the last KeyStroke the user entered to the field. */ KeyStroke getLastKeyStroke() { return lastKeyStroke; } //////////////////////// // Overridden methods // //////////////////////// @Override protected void paintBorder(Graphics g) { paintDottedBorder(g, getWidth(), getHeight(), ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]); } ///////////////////////////// //// KeyListener methods //// ///////////////////////////// public void keyPressed(KeyEvent keyEvent) { LOGGER.trace("keyModifiers={} keyCode={}", keyEvent.getModifiersEx(), keyEvent.getKeyCode()); int keyCode = keyEvent.getKeyCode(); if(keyCode==KeyEvent.VK_SHIFT || keyCode==KeyEvent.VK_CONTROL || keyCode==KeyEvent.VK_ALT || keyCode==KeyEvent.VK_META) return; KeyStroke pressedKeyStroke = KeyStroke.getKeyStrokeForEvent(keyEvent); if (pressedKeyStroke.equals(lastKeyStroke)) { TableCellEditor activeCellEditor = getCellEditor(); if (activeCellEditor!= null) activeCellEditor.stopCellEditing(); } else { String actionId; if ((actionId = data.contains(pressedKeyStroke)) != null) { String errorMessage = "The shortcut [" + KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(pressedKeyStroke) + "] is already assigned to '" + ActionProperties.getActionDescription(actionId) + "'"; tooltipBar.showErrorMessage(errorMessage); createCancelEditingStateThread(getCellEditor()); } else { lastKeyStroke = pressedKeyStroke; setText(KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(lastKeyStroke)); } } keyEvent.consume(); } public void keyReleased(KeyEvent e) {e.consume();} public void keyTyped(KeyEvent e) {e.consume();} } private class ShortcutsTableData { public class CurrentActionAccceleratorsFilter extends ActionFilter { private final KeyStroke accelerator; CurrentActionAccceleratorsFilter(KeyStroke accelerator) { this.accelerator = accelerator; } @Override public boolean accept(String actionId) { Map actionProperties = db.get(actionId); return ShortcutsTableData.this.equals(accelerator, actionProperties.get(ShortcutsTableData.this.accelerator)) || ShortcutsTableData.this.equals(accelerator, actionProperties.get(ShortcutsTableData.this.alt_accelerator)); } } private Object[][] data; private String[] actionIds; private String[] descriptions; private final Integer description = 0; private final Integer accelerator = 1; private final Integer alt_accelerator = 2; private final Integer tooltips = 3; private final List allActionIds; private final HashMap> db; ShortcutsTableData() { allActionIds = new ArrayList<>(); Iterator iterator = ActionManager.getActionIds(); while(iterator.hasNext()) allActionIds.add(iterator.next()); allActionIds.sort(ACTIONS_COMPARATOR); final int nbActions = allActionIds.size(); db = new HashMap<>(nbActions); int nbRows = allActionIds.size(); data = new Object[nbRows][NUM_OF_COLUMNS]; final Insets insets = new Insets(0, 4, 0, 4); for(String actionId : allActionIds) { ActionDescriptor actionDescriptor = ActionProperties.getActionDescriptor(actionId); Map actionProperties = new HashMap<>(); ImageIcon actionIcon = actionDescriptor.getIcon(); if (actionIcon == null) actionIcon = transparentIcon; String actionLabel = actionDescriptor.getLabel(); // 0 -> action's icon & name pair actionProperties.put(description, new Pair<>(IconManager.getPaddedIcon(actionIcon, insets), actionLabel)); // 1 -> action's accelerator actionProperties.put(accelerator, ActionKeymap.getAccelerator(actionId)); // 2 -> action's alternate accelerator actionProperties.put(alt_accelerator, ActionKeymap.getAlternateAccelerator(actionId)); // 3 -> action's description actionProperties.put(tooltips, actionDescriptor.getDescription()); db.put(actionId, actionProperties); } } public void filter(ActionFilter filter) { List filteredActionIds = filter(allActionIds, filter); // Build the table data int nbRows = filteredActionIds.size(); actionIds = new String[nbRows]; descriptions = new String[nbRows]; data = new Object[nbRows][NUM_OF_COLUMNS]; for (int i = 0; i < nbRows; ++i) { String actionId = filteredActionIds.get(i); actionIds[i] = actionId; ActionDescriptor actionDescriptor = ActionProperties.getActionDescriptor(actionId); data[i][ACTION_DESCRIPTION_COLUMN_INDEX] = db.get(actionId).get(this.description); KeyStroke accelerator = (KeyStroke) db.get(actionId).get(this.accelerator); setAccelerator(accelerator, i); KeyStroke alternativeAccelerator = (KeyStroke) db.get(actionId).get(this.alt_accelerator); setAlternativeAccelerator(alternativeAccelerator, i); descriptions[i] = actionDescriptor.getDescription(); } ShortcutsTable.this.clearSelection(); ((DefaultTableModel) getModel()).setRowCount(data.length); ShortcutsTable.this.repaint(); ShortcutsTable.this.scrollToVisible(0, 0); } Object[][] getTableData() { return data; } Object getTableData(int row, int col) { return data[row][col]; } String getCurrentTooltip() { return descriptions[getSelectedRow()]; } String getActionId(int row) { return actionIds[row]; } boolean hasChanged() { for (String actionId : db.keySet()) { Map actionProperties = db.get(actionId); if (!equals(actionProperties.get(this.accelerator), ActionKeymap.getAccelerator(actionId)) || !equals(actionProperties.get(this.alt_accelerator), ActionKeymap.getAlternateAccelerator(actionId))) return true; } return false; } void restoreDefaultAccelerators() { for (String actionId : allActionIds) { (db.get(actionId)).put(this.accelerator, ActionProperties.getDefaultAccelerator(actionId)); (db.get(actionId)).put(this.alt_accelerator, ActionProperties.getDefaultAlternativeAccelerator(actionId)); } int nbRows = actionIds.length; for (int i=0; i actionProperties = db.get(actionId); KeyStroke accelerator = (KeyStroke) actionProperties.get(this.accelerator); KeyStroke alternateAccelerator = (KeyStroke) actionProperties.get(this.alt_accelerator); // If action's accelerators differ from its saved accelerators, register them. if (!equals(accelerator, ActionKeymap.getAccelerator(actionId)) || !equals(alternateAccelerator, ActionKeymap.getAlternateAccelerator(actionId))) ActionKeymap.changeActionAccelerators(actionId, accelerator, alternateAccelerator); } } public String contains(KeyStroke accelerator) { if (accelerator != null) { for (String actionId : db.keySet()) { if (accelerator.equals(db.get(actionId).get(this.accelerator)) || accelerator.equals(db.get(actionId).get(this.alt_accelerator))) return actionId; } } return null; } private void setAccelerator(KeyStroke accelerator, int row) { data[row][ACCELERATOR_COLUMN_INDEX] = accelerator; db.get(getActionId(row)).put(this.accelerator, accelerator); } private void setAlternativeAccelerator(KeyStroke altAccelerator, int row) { data[row][ALTERNATE_ACCELERATOR_COLUMN_INDEX] = altAccelerator; db.get(getActionId(row)).put(this.alt_accelerator, altAccelerator); } private List filter(List actionIds, ActionFilter filter) { List filteredActionsList = new LinkedList<>(); for (String actionId : actionIds) { // Discard actions that are parameterized, and those that are rejected by the IMAGE_FILTER if (!ActionProperties.getActionDescriptor(actionId).isParameterized() && filter.accept(actionId)) filteredActionsList.add(actionId); } return filteredActionsList; } private boolean equals(Object obj1, Object obj2) { if (obj1 == null) return obj2 == null; return obj1.equals(obj2); } } private class ShortcutsTableCellRenderer implements TableCellRenderer, ThemeListener { /** Custom JLabel that render specific column cells */ private final DotBorderedCellLabel[] cellLabels = new DotBorderedCellLabel[NUM_OF_COLUMNS]; ShortcutsTableCellRenderer() { for(int i=0; i description = (Pair) value; label.setIcon(description.first); label.setText(description.second); // set cell's foreground color label.setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL][ThemeCache.PLAIN_FILE]); } // Any other column else { final KeyStroke key = (KeyStroke) value; String text = key == null ? "" : KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(key); // If component's preferred width is bigger than column width then the component is not entirely // visible so we set a tooltip text that will display the whole text when mouse is over the component if (table.getColumnModel().getColumn(vColIndex).getWidth() < label.getPreferredSize().getWidth()) { label.setToolTipText(text); } else { // Have to set it to null otherwise the defaultRender sets the tooltip text to the last one specified label.setToolTipText(null); } // Set label's text label.setText(text); // set cell's foreground color if (key != null) { boolean customized = switch (columnId) { case ACCELERATOR_COLUMN_INDEX -> !key.equals(ActionProperties.getDefaultAccelerator(data.getActionId(rowIndex))); case ALTERNATE_ACCELERATOR_COLUMN_INDEX -> !key.equals(ActionProperties.getDefaultAlternativeAccelerator(data.getActionId(rowIndex))); default -> false; }; label.setForeground(ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL][customized ? ThemeCache.PLAIN_FILE : ThemeCache.HIDDEN_FILE]); } } // set outline for the focused cell label.setOutline(hasFocus ? ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED] : null); // set cell's background color label.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][rowIndex % 2 == 0 ? ThemeCache.NORMAL : ThemeCache.ALTERNATE]); return label; } // - Theme listening ------------------------------------------------------------- // ------------------------------------------------------------------------------- /** * Receives theme color changes notifications. */ public void colorChanged(ColorChangedEvent event) { } /** * Receives theme font changes notifications. */ public void fontChanged(FontChangedEvent event) { if (event.getFontId() == Theme.FILE_TABLE_FONT) { setCellLabelsFont(ThemeCache.tableFont); } } } /** * CellLabel with a dotted outline. */ private static class DotBorderedCellLabel extends CellLabel { @Override protected void paintOutline(Graphics g) { paintDottedBorder(g, getWidth(), getHeight(), outlineColor); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/ThemeNameDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.general; import com.mucommander.ui.button.ButtonChoicePanel; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.layout.YBoxPanel; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * Dialog used to ask a new theme name to the user. * @author Nicolas Rinaudo */ public class ThemeNameDialog extends FocusDialog implements ActionListener { // - UI fields ----------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Field in which the user will enter the new name. */ private JTextField nameField; /** Ok button. */ private JButton okButton; /** Cancel button. */ private JButton cancelButton; /** Whether the dialog was closed by the ok button or by cancelling it. */ private boolean wasValidated; /** Maximum dimensions for the dialog. */ private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(480,10000); /** * Creates a new name dialog with the specified owner. * @param owner component that will own this dialog. * @param name current name. */ public ThemeNameDialog(Frame owner, String name) { super(owner, i18n("rename"), owner); init(name); } /** * Creates a new name dialog with the specified owner. * @param owner component that will own this dialog. * @param name current name. */ public ThemeNameDialog(Dialog owner, String name) { super(owner, i18n("rename"), owner); init(name); } /** * Creates the panel in which we'll store the label and name field. * @param name current name. * @return the panel in which we'll store the label and name field. */ private JPanel createNamePanel(String name) { XAlignedComponentPanel panel = new XAlignedComponentPanel(5); nameField = new JTextField(); nameField.setText(name); nameField.setSelectionStart(0); nameField.setSelectionEnd(name.length()); panel.addRow(i18n("name"), nameField, 0); return panel; } /** * Initializes the dialog's UI. */ private void init(String name) { setMaximumSize(MAXIMUM_DIALOG_DIMENSION); // Creates the name panel. YBoxPanel panel = new YBoxPanel(); panel.add(createNamePanel(name)); // Creates the button panel. panel.add(new ButtonChoicePanel(new JButton[] { okButton = new JButton(i18n("ok")), cancelButton = new JButton(i18n("cancel")) }, 2, getRootPane())); okButton.addActionListener(this); cancelButton.addActionListener(this); getContentPane().add(panel, BorderLayout.NORTH); pack(); } /** * Returns the name entered by the user. * @return the name entered by the user. */ public String getText() {return nameField.getText();} /** * Called when the dialog is closed through the ESC button. */ @Override public void cancel() { wasValidated = false; super.cancel(); } /** * Shows the dialog and returns true if it was validated by the user. * @return true if it was validated by the user, false otherwise. */ public boolean wasValidated() { showDialog(); return wasValidated; } /** * Called when OK or Cancel have been pressed. * @param e describes the event. */ public void actionPerformed(ActionEvent e) { if (e.getSource() == okButton) { wasValidated = true; } else if (e.getSource() == cancelButton) { wasValidated = false; } dispose(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/general/package.html ================================================ UI components used to let users modify their preferences. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/package.html ================================================ API for preference dialog boxes. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/ColorButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.ui.chooser.*; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.theme.ThemeData; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; /** * @author Maxence Bernard, Nicolas Rinaudo */ class ColorButton extends JPanel implements ActionListener, ColorChangeListener { /** ThemeData from which to retrieve the color chooser's values. */ private final ThemeData themeData; /** Identifier of the color that's being edited. */ private final int colorId; /** Dialog on which the color chooser should be centered and modal to. */ private final PreferencesDialog parent; /** The preview component that is repainted when the current color changes (can be null) */ private final JComponent previewComponent; /** Name of the preview component's property that gets updated with the current color of this button (can be null) */ private final String previewColorPropertyName; private java.util.List updatedPreviewComponents; /** Button's border. */ private final Border border = BorderFactory.createEtchedBorder(); /** The color button */ private final JButton button; /** Current color displayed in the button */ private Color currentColor; public ColorButton(PreferencesDialog parent, ThemeData themeData, int colorId) { this(parent, themeData, colorId, null, null); } ColorButton(PreferencesDialog parent, ThemeData themeData, int colorId, String previewColorPropertyName) { this(parent, themeData, colorId, previewColorPropertyName, null); } ColorButton(PreferencesDialog parent, ThemeData themeData, int colorId, String previewColorPropertyName, JComponent previewComponent) { FlowLayout flowLayout = new FlowLayout(); flowLayout.setHgap(0); flowLayout.setVgap(0); setLayout(flowLayout); this.themeData = themeData; this.colorId = colorId; this.parent = parent; this.previewComponent = previewComponent; this.previewColorPropertyName = previewColorPropertyName; if (previewColorPropertyName != null && previewComponent != null) { addUpdatedPreviewComponent(previewComponent); } button = new JButton() { @Override public Dimension getPreferredSize() {return new Dimension(70, 30);} @Override public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); // Fill the button with the specified color g.setColor((ColorButton.this).currentColor); g.fillRect(0, 0, width, height); // Paint custom border border.paintBorder(this, g, 0, 0, width, height); } }; button.addActionListener(this); button.setBorderPainted(true); add(button); // Add a ColorPicker only if this component is supported by the current environment if (ColorPicker.isSupported()) { ColorPicker colorPicker = new ColorPicker(); colorPicker.addColorChangeListener(this); add(colorPicker); } setCurrentColor(themeData.getColor(colorId), false); } void addUpdatedPreviewComponent(JComponent previewComponent) { if (previewColorPropertyName == null) { return; } if (updatedPreviewComponents == null) { updatedPreviewComponents = new ArrayList<>(); } updatedPreviewComponents.add(previewComponent); previewComponent.putClientProperty(previewColorPropertyName, currentColor); } int getColorId() { return colorId; } Color getCurrentColor() { return currentColor; } private void setCurrentColor(Color color, boolean initiatedByUser) { currentColor = color; if (themeData.isColorDifferent(colorId, currentColor)) { initiatedByUser &= themeData.setColor(colorId, currentColor); } button.repaint(); if (updatedPreviewComponents != null && previewColorPropertyName != null) { for (JComponent updatedPreviewComponent : updatedPreviewComponents) { updatedPreviewComponent.putClientProperty(previewColorPropertyName, color); } } if (initiatedByUser) { parent.componentChanged(null); } } private ColorChooser createColorChooser() { if(previewComponent!=null && previewColorPropertyName!=null && (previewComponent instanceof PreviewLabel)) { try { return new ColorChooser(currentColor, (PreviewLabel)((PreviewLabel)previewComponent).clone(), previewColorPropertyName); } catch(CloneNotSupportedException ignored) {} } return new ColorChooser(currentColor, new PreviewLabel(), PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME); } @Override public void actionPerformed(ActionEvent e) { ColorChooser chooser = createColorChooser(); ColorChooser.createDialog(parent, chooser).showDialog(); setCurrentColor(chooser.getColor(), true); } @Override public void colorChanged(ColorChangeEvent event) {setCurrentColor(event.getColor(), true);} } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/CopyFilePanelColorsButton.java ================================================ package com.mucommander.ui.dialog.pref.theme; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import javax.swing.JButton; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.ColorChangeEvent; import com.mucommander.ui.dialog.pref.PreferencesPanel; /** * CopyColorsButton is a {@link JButton} that copies the values of {@link ColorButton}s from one panel to * another. Colors are mapped from active to inactive colors and vice versa by {@link FilePanelColorIds}. */ class CopyFilePanelColorsButton extends JButton { private static final long serialVersionUID = 1L; /** * {@link ColorButton}s of the source file panel by their colorId */ private final Map sourceColorButtons = new HashMap<>(); /** * {@link ColorButton}s of the target file panel by their colorId */ private final Map targetColorButtons = new HashMap<>(); /** * Constructs an instance that copies all colors from the other panel set with {@link #setSource(PreferencesPanel)} to the * corresponding {@link ColorButton}s in targetColorButtonsContainer. * * @param targetColorButtonsContainer */ CopyFilePanelColorsButton(Container targetColorButtonsContainer, boolean isTargetActive) { super(); findColorButtons(targetColorButtonsContainer, targetColorButtons); FilePanelColorIds colorIds = new FilePanelColorIds(); addActionListener(e -> { for (Integer targetColorId : targetColorButtons.keySet()) { final boolean isSourceActive = !isTargetActive; int sourceColorId = isTargetActive ? colorIds.getIdByActive(isSourceActive, targetColorId) : colorIds.getIdByInactive(isSourceActive, targetColorId); ColorButton sourceButton = sourceColorButtons.get(sourceColorId); if (sourceButton != null) { Color sourceColor = sourceButton.getCurrentColor(); ColorButton targetButton = targetColorButtons.get(targetColorId); targetButton.colorChanged(new ColorChangeEvent(this, sourceColor)); } } }); } /** * Sets the source container having all the {@link ColorButton}s to copy the colors from. * * @param otherPanelsColorButtonsContainer source container */ public void setSource(PreferencesPanel otherPanelsColorButtonsContainer) { findColorButtons(otherPanelsColorButtonsContainer, sourceColorButtons); setText(Translator.get("theme_editor.copy_colors", otherPanelsColorButtonsContainer.getTitle())); } /** * Finds all {@link ColorButton}s in otherPanelsColorButtonsContainer and its descendants and stores * them in colorButtons. * * @param otherPanelsColorButtonsContainer * @param colorButtons */ private void findColorButtons(final Container otherPanelsColorButtonsContainer, final Map colorButtons) { final Queue children = new LinkedList<>(); children.add(otherPanelsColorButtonsContainer); for (Component component = children.poll(); component != null; component = children.poll()) { if (component instanceof ColorButton) { final ColorButton colorButton = (ColorButton) component; colorButtons.put(colorButton.getColorId(), colorButton); } else if (component instanceof Container) { final Container container = (Container) component; final Component[] components; synchronized (container.getTreeLock()) { components = container.getComponents(); } children.addAll(Arrays.asList(components)); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/FileEditorPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.RuntimeConstants; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeId; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import ru.trolsoft.hexeditor.data.MemoryByteBuffer; import ru.trolsoft.hexeditor.ui.HexTable; import ru.trolsoft.hexeditor.ui.ViewerHexTableModel; import javax.swing.*; import java.awt.*; import java.beans.PropertyChangeListener; import java.io.IOException; import java.io.InputStreamReader; import java.util.Random; /** * @author Nicolas Rinaudo, Maxence Bernard */ class FileEditorPanel extends ThemeEditorPanel implements ThemeId { /** Used to textPreview the editor's theme. */ private RSyntaxTextArea textPreview; private JScrollPane scrollHex; private HexTable hexPreview; private final PropertyChangeListener textPropertyChangeListener = event -> { final String name = event.getPropertyName(); if (name.equals(PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME)) { setTextBackgroundColors(); } else if (name.equals(PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME)) { setTextForegroundColors(); } }; private final PropertyChangeListener hexPropertyChangeListener = event -> { final String name = event.getPropertyName(); if (name.equals(PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME) || name.equals(PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME)) { setHexColors(); } }; /** * Creates a new file table editor. * @param parent dialog containing the panel. * @param themeData themeData being edited. */ FileEditorPanel(PreferencesDialog parent, ThemeData themeData) { super(parent, Translator.get("theme_editor.editor_tab"), themeData); initUI(); } /** * Creates the JPanel that contains all of the color configuration elements. * @param fontChooser font chooser used by the editor panel. * @return the JPanel that contains all of the color configuration elements. */ private JPanel createTextColorsPanel(FontChooser fontChooser) { ProportionalGridPanel gridPanel = new ProportionalGridPanel(3); // Header addLabelRow(gridPanel, false); PreviewLabel label = new PreviewLabel(); // Color buttons addColorButtons(gridPanel, fontChooser, "theme_editor.normal", EDITOR_FOREGROUND_COLOR, EDITOR_BACKGROUND_COLOR, label).addPropertyChangeListener(textPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.selected", EDITOR_SELECTED_FOREGROUND_COLOR, EDITOR_SELECTED_BACKGROUND_COLOR, label).addPropertyChangeListener(textPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.current", -1, EDITOR_CURRENT_BACKGROUND_COLOR, label).addPropertyChangeListener(textPropertyChangeListener); label.addPropertyChangeListener(textPropertyChangeListener); //butt.addUpdatedPreviewComponent(label); // Wraps everything in a flow layout. JPanel colorsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); colorsPanel.add(gridPanel); colorsPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); return colorsPanel; } /** * Creates the JPanel that contains all the color configuration elements. * @param fontChooser font chooser used by the editor panel. * @return the JPanel that contains all the color configuration elements. */ private JPanel createHexColorsPanel(FontChooser fontChooser) { // Initialisation ProportionalGridPanel gridPanel = new ProportionalGridPanel(3); // Header addLabelRow(gridPanel, false); PreviewLabel label = new PreviewLabel(); // Color buttons addColorButtons(gridPanel, fontChooser, "theme_editor.normal_hex", HEX_VIEWER_HEX_FOREGROUND_COLOR, HEX_VIEWER_BACKGROUND_COLOR, label).addPropertyChangeListener(hexPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.normal_ascii", HEX_VIEWER_ASCII_FOREGROUND_COLOR, -1, label).addPropertyChangeListener(hexPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.normal_offset", HEX_VIEWER_OFFSET_FOREGROUND_COLOR, -1, label).addPropertyChangeListener(hexPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.alternate", -1, HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR, label).addPropertyChangeListener(hexPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.selected_hex", -1, HEX_VIEWER_SELECTED_BACKGROUND_COLOR, label).addPropertyChangeListener(hexPropertyChangeListener); addColorButtons(gridPanel, fontChooser, "theme_editor.selected_ascii", -1, HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR, label).addPropertyChangeListener(hexPropertyChangeListener); label.addPropertyChangeListener(hexPropertyChangeListener); //butt.addUpdatedPreviewComponent(label); // Wraps everything in a flow layout. JPanel colorsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); colorsPanel.add(gridPanel); colorsPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); return colorsPanel; } /** * Initializes the panel's UI. */ private void initUI() { JTabbedPane tabbedPane = new JTabbedPane(); // JPanel textEditorPanel = createTextEditorPanel(); tabbedPane.add(Translator.get("theme_editor.text_editor_tab"), createTextViewerPanel()); tabbedPane.add(Translator.get("theme_editor.hex_viewer_tab"), createHexViewerPanel()); setLayout(new BorderLayout()); add(tabbedPane, BorderLayout.NORTH); // // Layout. // setLayout(new BorderLayout()); // add(mainPanel, BorderLayout.NORTH); } private JPanel createTextViewerPanel() { // Font chooser and textPreview initialisation. JPanel mainPanel = new JPanel(new BorderLayout()); FontChooser fontChooser = createFontChooser(EDITOR_FONT); mainPanel.add(createTextPreviewPanel(), BorderLayout.CENTER); addFontChooserListener(fontChooser, textPreview); // Configuration panel initialization. YBoxPanel configurationPanel = new YBoxPanel(); configurationPanel.add(fontChooser); configurationPanel.addSpace(10); configurationPanel.add(createTextColorsPanel(fontChooser)); mainPanel.add(configurationPanel, BorderLayout.WEST); return mainPanel; } private JPanel createHexViewerPanel() { JPanel panel = new JPanel(new BorderLayout()); FontChooser fontChooser = createFontChooser(HEX_VIEWER_FONT); panel.add(createHexPreviewPanel(), BorderLayout.CENTER); addFontChooserListener(fontChooser, hexPreview); // Configuration panel initialization. YBoxPanel configurationPanel = new YBoxPanel(); configurationPanel.add(fontChooser); configurationPanel.addSpace(10); configurationPanel.add(createHexColorsPanel(fontChooser)); panel.add(configurationPanel, BorderLayout.WEST); return panel; } /** * Creates the file editor textPreview panel. * @return the file editor textPreview panel. */ private JPanel createTextPreviewPanel() { // Initializes the textPreview text area. textPreview = new RSyntaxTextArea(15, 15); // Initialises colors. setTextBackgroundColors(); setTextForegroundColors(); // Creates the panel. JPanel panel = new JPanel(new BorderLayout()); JScrollPane scroll = new JScrollPane(textPreview, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); panel.add(scroll, BorderLayout.CENTER); scroll.getViewport().setPreferredSize(textPreview.getPreferredSize()); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); loadText(); textPreview.setCaretPosition(0); return panel; } private JPanel createHexPreviewPanel() { MemoryByteBuffer dataBuffer = new MemoryByteBuffer(250); ViewerHexTableModel model = new ViewerHexTableModel(dataBuffer, 8); try { model.load(); } catch (IOException ignore) {} Random rnd = new Random(); for (int i = 0; i < dataBuffer.getCapacity(); i++) { dataBuffer.setByte(i, rnd.nextInt()); } hexPreview = new HexTable(model); JPanel panel = new JPanel(new BorderLayout()); scrollHex = new JScrollPane(hexPreview, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); panel.add(scrollHex, BorderLayout.CENTER); scrollHex.getViewport().setPreferredSize(new Dimension(300, 200)); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); setHexColors(); return panel; } private void setTextBackgroundColors() { Color background = themeData.getColor(EDITOR_BACKGROUND_COLOR); textPreview.setBackground(background); for (int i = 1; i <= textPreview.getSecondaryLanguageCount(); i++) { textPreview.setSecondaryLanguageBackground(i, background); } textPreview.setSelectionColor(themeData.getColor(EDITOR_SELECTED_BACKGROUND_COLOR)); textPreview.setCurrentLineHighlightColor(themeData.getColor(EDITOR_CURRENT_BACKGROUND_COLOR)); } private void setTextForegroundColors() { textPreview.setForeground(themeData.getColor(EDITOR_FOREGROUND_COLOR)); textPreview.setCaretColor(themeData.getColor(EDITOR_FOREGROUND_COLOR)); textPreview.setSelectedTextColor(themeData.getColor(EDITOR_SELECTED_FOREGROUND_COLOR)); } private void setHexColors() { hexPreview.setBackground(themeData.getColor(HEX_VIEWER_BACKGROUND_COLOR)); hexPreview.setForeground(themeData.getColor(HEX_VIEWER_HEX_FOREGROUND_COLOR)); hexPreview.setAlternateBackground(themeData.getColor(HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR)); hexPreview.setOffsetColumnColor(themeData.getColor(HEX_VIEWER_OFFSET_FOREGROUND_COLOR)); hexPreview.setAsciiColumnColor(themeData.getColor(HEX_VIEWER_ASCII_FOREGROUND_COLOR)); hexPreview.setAsciiSelectionBackgroundColor(themeData.getColor(HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR)); hexPreview.setSelectionBackground(themeData.getColor(HEX_VIEWER_SELECTED_BACKGROUND_COLOR)); hexPreview.setFont(themeData.getFont(HEX_VIEWER_FONT)); hexPreview.setAlternateRowBackground(true); hexPreview.getTableHeader().setFont(new Font("Monospaced", Font.PLAIN, 12)); scrollHex.getViewport().setBackground(hexPreview.getBackground()); } // - Misc. --------------------------------------------------------------------------- // ----------------------------------------------------------------------------------- private void loadText() { try (InputStreamReader in = new InputStreamReader(FileEditorPanel.class.getResourceAsStream(RuntimeConstants.LICENSE))) { char[] buffer = new char[2048]; int count; // Number of characters read from the last read operation. while ((count = in.read(buffer)) >= 0) { textPreview.append(new String(buffer, 0, count)); } } catch (IOException e) { e.printStackTrace(); } } // - Modification management --------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Ignored. */ @Override public void commit() {} } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/FileGroupsPanel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.main.table.FileGroupResolver; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeId; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.BorderLayout; /** * @author Oleg Trifonov */ public class FileGroupsPanel extends ThemeEditorPanel implements ThemeId { private static final int NUMBER_OF_GROUPS = 10; private final JTextField[] fileMasks = new JTextField[NUMBER_OF_GROUPS]; /** * Creates a new FilePanel. * @param parent dialog containing the panel * @param data theme to edit. */ FileGroupsPanel(final PreferencesDialog parent, ThemeData data) { super(parent, Translator.get("theme_editor.file_groups"), data); DocumentListener documentListener = new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { parent.setCommitButtonsEnabled(true); } @Override public void removeUpdate(DocumentEvent e) { parent.setCommitButtonsEnabled(true); } @Override public void changedUpdate(DocumentEvent e) { } }; FilePreviewPanel preview = new FilePreviewPanel(themeData, true); JPanel gridPanel = new ProportionalGridPanel(3); // Header gridPanel.add(new JLabel()); gridPanel.add(createCaptionLabel("theme_editor.normal_color")); gridPanel.add(createCaptionLabel("theme_editor.filemask")); TcPreferencesAPI prefs = TcConfigurations.getPreferences(); for (int i = 0; i < NUMBER_OF_GROUPS; i++) { TcPreference preference = TcPreference.values()[TcPreference.FILE_GROUP_1_MASK.ordinal() + i]; String mask = prefs.getVariable(preference); gridPanel.add(createCaptionLabelWithTitle(Translator.get("theme_editor.group_") + " " + (i+1))); ColorButton colorButton = new ColorButton(parent, themeData, FILE_GROUP_1_FOREGROUND_COLOR + i, PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME, preview); gridPanel.add(colorButton); fileMasks[i] = new JTextField(24); fileMasks[i].setText(mask); gridPanel.add(fileMasks[i]); fileMasks[i].getDocument().addDocumentListener(documentListener); } setLayout(new BorderLayout()); add(gridPanel, BorderLayout.WEST); add(preview, BorderLayout.EAST); } @Override protected void commit() { TcPreferencesAPI prefs = TcConfigurations.getPreferences(); for (int i = 0; i < NUMBER_OF_GROUPS; i++) { TcPreference preference = TcPreference.values()[TcPreference.FILE_GROUP_1_MASK.ordinal() + i]; prefs.setVariable(preference, fileMasks[i].getText().trim()); } try { TcConfigurations.savePreferences(); } catch(Exception ignore) { } FileGroupResolver.getInstance().init(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/FilePanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import static com.mucommander.ui.theme.ThemeData.*; import java.awt.BorderLayout; import java.awt.Component; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JPanel; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.theme.ThemeData; /** * @author Nicolas Rinaudo */ class FilePanel extends ThemeEditorPanel { private CopyFilePanelColorsButton copyColorsButton; /** * Creates a new FilePanel. * @param parent dialog containing the panel * @param isActive whether the color values should be taken from the active or inactive state. * @param data theme to edit. * @param fontChooser File table font chooser. */ FilePanel(PreferencesDialog parent, boolean isActive, ThemeData data, FontChooser fontChooser) { super(parent, Translator.get(isActive ? "theme_editor.active_panel" : "theme_editor.inactive_panel"), data); initUI(isActive, fontChooser); } private void initUI(boolean isActive, FontChooser fontChooser) { JPanel gridPanel = new ProportionalGridPanel(3); FilePreviewPanel preview = new FilePreviewPanel(themeData, isActive); addFontChooserListener(fontChooser, preview); // Header gridPanel.add(new JLabel()); gridPanel.add(createCaptionLabel("theme_editor.normal")); gridPanel.add(createCaptionLabel("theme_editor.selected")); FilePanelColorIds colors = new FilePanelColorIds(); // Background gridPanel.add(createCaptionLabel("theme_editor.background")); ColorButton backgroundButton = new ColorButton(parent, themeData, colors.getIdByActive(isActive, FILE_TABLE_BACKGROUND_COLOR), PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME, preview); gridPanel.add(backgroundButton); ColorButton selectedBackgroundButton = new ColorButton(parent, themeData, colors.getIdByActive(isActive, FILE_TABLE_SELECTED_BACKGROUND_COLOR), PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME, preview); gridPanel.add(selectedBackgroundButton); // Alternate background gridPanel.add(createCaptionLabel("theme_editor.alternate_background")); gridPanel.add(new ColorButton(parent, themeData, colors.getIdByActive(isActive, FILE_TABLE_ALTERNATE_BACKGROUND_COLOR), PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME, preview)); gridPanel.add(new JLabel()); // Folders. gridPanel.add(createCaptionLabel("theme_editor.folder")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, FOLDER_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, FOLDER_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Plain files. gridPanel.add(createCaptionLabel("theme_editor.plain_file")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, FILE_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, FILE_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Archives. gridPanel.add(createCaptionLabel("theme_editor.archive_file")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, ARCHIVE_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, ARCHIVE_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Hidden folders. gridPanel.add(createCaptionLabel("theme_editor.hidden_folder")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, HIDDEN_FOLDER_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Hidden files. gridPanel.add(createCaptionLabel("theme_editor.hidden_file")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, HIDDEN_FILE_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, HIDDEN_FILE_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Symlinks. gridPanel.add(createCaptionLabel("theme_editor.symbolic_link")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, SYMLINK_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, SYMLINK_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Marked files. gridPanel.add(createCaptionLabel("theme_editor.marked_file")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, MARKED_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, MARKED_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Executable files. gridPanel.add(createCaptionLabel("theme_editor.executable_file")); addForegroundColor(gridPanel, colors.getIdByActive(isActive, EXECUTABLE_FOREGROUND_COLOR), backgroundButton, fontChooser, preview); addForegroundColor(gridPanel, colors.getIdByActive(isActive, EXECUTABLE_SELECTED_FOREGROUND_COLOR), selectedBackgroundButton, fontChooser, preview); // Border. gridPanel.add(createCaptionLabel("theme_editor.border")); ColorButton borderButton; gridPanel.add(borderButton = new ColorButton(parent, themeData, colors.getIdByActive(isActive, FILE_TABLE_BORDER_COLOR), PreviewLabel.BORDER_COLOR_PROPERTY_NAME)); borderButton.addUpdatedPreviewComponent(preview); gridPanel.add(borderButton = new ColorButton(parent, themeData, colors.getIdByActive(isActive, FILE_TABLE_SELECTED_OUTLINE_COLOR), PreviewLabel.BORDER_COLOR_PROPERTY_NAME)); borderButton.addUpdatedPreviewComponent(preview); // Copy colors // must be last when color buttons are alraedy added to gridPanel! copyColorsButton = new CopyFilePanelColorsButton(gridPanel, isActive); copyColorsButton.setAlignmentX(Component.CENTER_ALIGNMENT); JPanel rightSide = new JPanel(); rightSide.setLayout(new BoxLayout(rightSide, BoxLayout.Y_AXIS)); rightSide.add(preview); rightSide.add(copyColorsButton); setLayout(new BorderLayout()); add(gridPanel, BorderLayout.WEST); add(rightSide, BorderLayout.EAST); } private void addForegroundColor(JPanel to, int colorId, ColorButton background, FontChooser fontChooser, FilePreviewPanel previewPanel) { PreviewLabel preview = new PreviewLabel(); preview.setTextPainted(true); background.addUpdatedPreviewComponent(preview); addFontChooserListener(fontChooser, preview); ColorButton button = new ColorButton(parent, themeData, colorId, PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME, preview); to.add(button); button.addUpdatedPreviewComponent(previewPanel); } @Override public void commit() {} void setCopyColorButtonsSource(PreferencesPanel otherPanelsColorButtonsContainer) { copyColorsButton.setSource(otherPanelsColorButtonsContainer); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/FilePanelColorIds.java ================================================ package com.mucommander.ui.dialog.pref.theme; import static com.mucommander.ui.theme.ThemeData.*; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; /** * Mapping between colors of the active file panel and colors of the inactive panel. */ class FilePanelColorIds { private final Map colorMap; private final Map reverseColorMap; FilePanelColorIds() { super(); colorMap = new HashMap<>(); add(FILE_TABLE_BACKGROUND_COLOR, FILE_TABLE_INACTIVE_BACKGROUND_COLOR); add(FILE_TABLE_SELECTED_BACKGROUND_COLOR, FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR); add(FILE_TABLE_ALTERNATE_BACKGROUND_COLOR, FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR); add(FOLDER_FOREGROUND_COLOR, FOLDER_INACTIVE_FOREGROUND_COLOR); add(FOLDER_SELECTED_FOREGROUND_COLOR, FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR); add(FILE_FOREGROUND_COLOR, FILE_INACTIVE_FOREGROUND_COLOR); add(FILE_SELECTED_FOREGROUND_COLOR, FILE_INACTIVE_SELECTED_FOREGROUND_COLOR); add(ARCHIVE_FOREGROUND_COLOR, ARCHIVE_INACTIVE_FOREGROUND_COLOR); add(ARCHIVE_SELECTED_FOREGROUND_COLOR, ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR); add(HIDDEN_FOLDER_FOREGROUND_COLOR, HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR); add(HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR, HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR); add(HIDDEN_FILE_FOREGROUND_COLOR, HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR); add(HIDDEN_FILE_SELECTED_FOREGROUND_COLOR, HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR); add(SYMLINK_FOREGROUND_COLOR, SYMLINK_INACTIVE_FOREGROUND_COLOR); add(SYMLINK_SELECTED_FOREGROUND_COLOR, SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR); add(MARKED_FOREGROUND_COLOR, MARKED_INACTIVE_FOREGROUND_COLOR); add(MARKED_SELECTED_FOREGROUND_COLOR, MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR); add(EXECUTABLE_FOREGROUND_COLOR, EXECUTABLE_INACTIVE_FOREGROUND_COLOR); add(EXECUTABLE_SELECTED_FOREGROUND_COLOR, EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR); add(FILE_TABLE_BORDER_COLOR, FILE_TABLE_INACTIVE_BORDER_COLOR); add(FILE_TABLE_SELECTED_OUTLINE_COLOR, FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR); reverseColorMap = colorMap.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); } /** * Gets a color by specifying the id of the active file panel. * * @param isActive * whether the id of the active or the inactive file panel should be returned * @param activeColorId * the id of the color in the active file panel * @return the id of the active or inactive panel depending on isActive */ int getIdByActive(boolean isActive, int activeColorId) { return isActive ? activeColorId : colorMap.get(activeColorId); } /** * Gets a color by specifying the id of the inactive file panel. * * @param isActive * whether the id of the active or the inactive file panel should be returned * @param inactiveColorId * the id of the color in the inactive file panel * @return the id of the active or inactive panel depending on isActive */ int getIdByInactive(boolean isActive, int inactiveColorId) { return isActive ? reverseColorMap.get(inactiveColorId) : inactiveColorId; } private void add(int active, int inactive) { colorMap.put(active, inactive); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/FilePreviewPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.utils.text.Translator; import com.mucommander.ui.border.MutableLineBorder; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.table.CellLabel; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeId; import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * @author Nicolas Rinaudo */ class FilePreviewPanel extends JScrollPane implements PropertyChangeListener, ThemeId { private enum RowType { FOLDER, PLAIN_FILE, ARCHIVE, HIDDEN_FOLDER, HIDDEN_FILE, SYMLINK, MARKED_FILE, EXECUTABLE_FILE, GROUP_1_FILE, GROUP_2_FILE, GROUP_3_FILE, GROUP_4_FILE, GROUP_5_FILE, GROUP_6_FILE, GROUP_7_FILE, GROUP_8_FILE, GROUP_9_FILE, GROUP_10_FILE, } private final ThemeData data; private final boolean isActive; private PreviewTable table; private final ImageIcon symlinkIcon; /** * Creates a new preview panel on the specified theme data. * @param data data to preview. * @param isActive whether we're previewing the active or inactive state. */ FilePreviewPanel(ThemeData data, boolean isActive) { super(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); this.data = data; this.isActive = isActive; symlinkIcon = IconManager.getCompositeIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.FILE_ICON_NAME), IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.SYMLINK_ICON_NAME)); initUI(); } /** * Initializes the previwer's UI. */ private void initUI() { table = new PreviewTable(); setViewportView(table); getViewport().setBackground(getColor(isActive ? Theme.FILE_TABLE_BACKGROUND_COLOR : Theme.FILE_TABLE_INACTIVE_BACKGROUND_COLOR)); setBorder(new MutableLineBorder(getColor(isActive ? Theme.FILE_TABLE_BORDER_COLOR : Theme.FILE_TABLE_INACTIVE_BORDER_COLOR))); addPropertyChangeListener(this); } public void propertyChange(PropertyChangeEvent event) { switch(event.getPropertyName()) { case PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME: getViewport().setBackground(getColor(isActive ? Theme.FILE_TABLE_BACKGROUND_COLOR : Theme.FILE_TABLE_INACTIVE_BACKGROUND_COLOR)); break; case PreviewLabel.BORDER_COLOR_PROPERTY_NAME: // Some (rather evil) look and feels will change borders outside of muCommander's control, // this check is necessary to ensure no exception is thrown. if (getBorder() instanceof MutableLineBorder) { ((MutableLineBorder)getBorder()).setLineColor(getColor(isActive ? Theme.FILE_TABLE_BORDER_COLOR : Theme.FILE_TABLE_INACTIVE_BORDER_COLOR)); } break; case PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME: break; default: return; } repaint(); } /** * Resets the table's row height with the new font. */ @Override public void setFont(Font font) { if(table != null) table.setRowHeight(font); } /** * Used to preview the current table theme. * @author Nicolas Rinaudo */ private class PreviewTable extends JTable { private final PreviewCellRenderer cellRenderer; private Dimension preferredSize; /** * Creates a new preview table. */ PreviewTable() { super(new String[][] {{"", Translator.get("theme_editor.folder")}, {"", Translator.get("theme_editor.plain_file")}, {"", Translator.get("theme_editor.archive_file")}, {"", Translator.get("theme_editor.hidden_folder")}, {"", Translator.get("theme_editor.hidden_file")}, {"", Translator.get("theme_editor.symbolic_link")}, {"", Translator.get("theme_editor.marked_file")}, {"", Translator.get("theme_editor.group_file_")+1}, {"", Translator.get("theme_editor.group_file_")+2}, {"", Translator.get("theme_editor.group_file_")+3}, {"", Translator.get("theme_editor.group_file_")+4}, {"", Translator.get("theme_editor.group_file_")+5}, {"", Translator.get("theme_editor.group_file_")+6}, {"", Translator.get("theme_editor.group_file_")+7}, {"", Translator.get("theme_editor.group_file_")+8}, {"", Translator.get("theme_editor.group_file_")+9}, {"", Translator.get("theme_editor.group_file_")+10}, }, new String[] {"", Translator.get("preview")}); // Initializes table painting. cellRenderer = new PreviewCellRenderer(); setShowGrid(false); // Initializes the table selection. getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); changeSelection(0, 0, false, false); // Initializes row dimensions. setRowHeight(data.getFont(FILE_TABLE_FONT)); setIntercellSpacing(new Dimension(0,0)); // Initializes the table header. getTableHeader().setResizingAllowed(false); getTableHeader().setReorderingAllowed(false); ((DefaultTableCellRenderer)getTableHeader().getDefaultRenderer()).setHorizontalAlignment(SwingConstants.LEFT); } /** * Resets column widths. */ @Override public void doLayout() { int width = getIconWidth(); getColumnModel().getColumn(0).setWidth(width); getColumnModel().getColumn(1).setWidth(Math.max(getWidth() - width, getLabelWidth())); } /** * Returns the width of the icon label. */ private int getIconWidth() {return (int)FileIcons.getIconDimension().getWidth() + 2 * CellLabel.CELL_BORDER_WIDTH;} /** * Returns the width of the text label. */ private int getLabelWidth() { FontMetrics fm = getFontMetrics(data.getFont(FILE_TABLE_FONT)); int rowCount = getModel().getRowCount(); int width = 0; for (int i = 0; i < rowCount; i++) { width = Math.max(width, fm.stringWidth(((String) (getModel().getValueAt(i, 1)))) + 2 * CellLabel.CELL_BORDER_WIDTH); } return width; } /** * Returns the table's preferred size. */ @Override public Dimension getPreferredSize() {return new Dimension(getIconWidth() + 2 * getLabelWidth(), getModel().getRowCount()*getRowHeight());} /** * Returns the table's preferred size. */ @Override public Dimension getPreferredScrollableViewportSize() { if (preferredSize == null) { preferredSize = getPreferredSize(); } return preferredSize; } /** * Initializes the row height depending on the font. */ private void setRowHeight(Font font) { setRowHeight(2 * CellLabel.CELL_BORDER_HEIGHT + Math.max(getFontMetrics(font).getHeight(), (int)FileIcons.getIconDimension().getHeight())); } /** * Uses our preview cell renderer rather than the default one. */ @Override public TableCellRenderer getCellRenderer(int row, int column) { return cellRenderer; } /** * Cell are not editable. */ @Override public boolean isCellEditable(int row, int column) {return false;} } /** * Used to render preview cells. * @author Nicolas Rinaudo */ private class PreviewCellRenderer implements TableCellRenderer { private final CellLabel label; private final CellLabel icon; /** * Creates a new preview cell renderer. */ PreviewCellRenderer() { label = new CellLabel(); icon = new CellLabel(); } /** * Returns the foreground color of the specified cell. */ private Color getForegroundColor(RowType row, boolean isSelected) { switch(row) { // Folders. case FOLDER: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? FOLDER_SELECTED_FOREGROUND_COLOR : FOLDER_FOREGROUND_COLOR); } else { return getColor(isSelected ? FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR : FOLDER_INACTIVE_FOREGROUND_COLOR); } // Plain files. case PLAIN_FILE: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? FILE_SELECTED_FOREGROUND_COLOR : FILE_FOREGROUND_COLOR); } else { return getColor(isSelected ? FILE_INACTIVE_SELECTED_FOREGROUND_COLOR : FILE_INACTIVE_FOREGROUND_COLOR); } // Archives. case ARCHIVE: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? ARCHIVE_SELECTED_FOREGROUND_COLOR : ARCHIVE_FOREGROUND_COLOR); } else { return getColor(isSelected ? ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR : ARCHIVE_INACTIVE_FOREGROUND_COLOR); } // Hidden folders. case HIDDEN_FOLDER: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR : HIDDEN_FOLDER_FOREGROUND_COLOR); } else { return getColor(isSelected ? HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR : HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR); } // Hidden files. case HIDDEN_FILE: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? HIDDEN_FILE_SELECTED_FOREGROUND_COLOR : HIDDEN_FILE_FOREGROUND_COLOR); } else { return getColor(isSelected ? HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR : HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR); } // Symlinks. case SYMLINK: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? SYMLINK_SELECTED_FOREGROUND_COLOR : SYMLINK_FOREGROUND_COLOR); } else { return getColor(isSelected ? SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR : SYMLINK_INACTIVE_FOREGROUND_COLOR); } // Marked files. case MARKED_FILE: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? MARKED_SELECTED_FOREGROUND_COLOR : MARKED_FOREGROUND_COLOR); } else { return getColor(isSelected ? MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR : MARKED_INACTIVE_FOREGROUND_COLOR); } // Executable files. case EXECUTABLE_FILE: if (FilePreviewPanel.this.isActive) { return getColor(isSelected ? EXECUTABLE_SELECTED_FOREGROUND_COLOR : EXECUTABLE_FOREGROUND_COLOR); } else { return getColor(isSelected ? EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR : EXECUTABLE_INACTIVE_FOREGROUND_COLOR); } case GROUP_1_FILE: case GROUP_2_FILE: case GROUP_3_FILE: case GROUP_4_FILE: case GROUP_5_FILE: case GROUP_6_FILE: case GROUP_7_FILE: case GROUP_8_FILE: case GROUP_9_FILE: case GROUP_10_FILE: int group = row.ordinal() - RowType.GROUP_1_FILE.ordinal(); return getColor(FILE_GROUP_1_FOREGROUND_COLOR + group); } // Impossible. return null; } /** * Returns the object used to render the specified cell. */ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { RowType rowType = RowType.values()[row]; CellLabel currentLabel; // Icon label foreground. if (column == 0) { currentLabel = icon; if (rowType == RowType.FOLDER) { currentLabel.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.FOLDER_ICON_NAME)); } else if (rowType == RowType.ARCHIVE) { currentLabel.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.ARCHIVE_ICON_NAME)); } else if (rowType == RowType.SYMLINK) { currentLabel.setIcon(symlinkIcon); } else { currentLabel.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.FILE_ICON_NAME)); } } // Text label foreground. else { currentLabel = label; currentLabel.setFont(data.getFont(FILE_TABLE_FONT)); currentLabel.setText((String)value); currentLabel.setForeground(getForegroundColor(rowType, isSelected)); } // Foreground. if (isSelected) { currentLabel.setOutline(getColor(isActive ? FILE_TABLE_SELECTED_OUTLINE_COLOR : FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR)); } else { currentLabel.setOutline(null); } // Background. if (FilePreviewPanel.this.isActive) { if (isSelected) { currentLabel.setBackground(getColor(FILE_TABLE_SELECTED_BACKGROUND_COLOR)); } else { currentLabel.setBackground(getColor((row % 2 == 0) ? FILE_TABLE_BACKGROUND_COLOR : FILE_TABLE_ALTERNATE_BACKGROUND_COLOR)); } } else { if (isSelected) { currentLabel.setBackground(getColor(FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR)); } else { currentLabel.setBackground(getColor((row % 2 == 0) ? FILE_TABLE_INACTIVE_BACKGROUND_COLOR : FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR)); } } return currentLabel; } } private Color getColor(int id) { return data.getColor(id); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/FolderPanePanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.theme.ThemeData; import javax.swing.*; import java.awt.*; /** * @author Nicolas Rinaudo, Maxence Bernard */ class FolderPanePanel extends ThemeEditorPanel { private FileGroupsPanel fileGroupsPanel; /** * Creates a new file table editor. * @param parent dialog containing the panel. * @param themeData themeData being edited. */ FolderPanePanel(PreferencesDialog parent, ThemeData themeData) { super(parent, Translator.get("theme_editor.folder_tab"), themeData); initUI(); } /** * Initializes the panel's UI. */ private void initUI() { FontChooser fontChooser = createFontChooser(ThemeData.FILE_TABLE_FONT); JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP); // Adds the general panel. tabbedPane.add(Translator.get("theme_editor.general"), createScrollPane(createGeneralPanel(fontChooser))); // Adds the active panel. FilePanel activeFilePanel = new FilePanel(parent, true, themeData, fontChooser); tabbedPane.add(activeFilePanel.getTitle(), createScrollPane(activeFilePanel)); // Adds the inactive panel. FilePanel inactiveFilePanel = new FilePanel(parent, false, themeData, fontChooser); tabbedPane.add(inactiveFilePanel.getTitle(), createScrollPane(inactiveFilePanel)); // Give the file panels the colors of the respective other one to let them copy from each other. activeFilePanel.setCopyColorButtonsSource(inactiveFilePanel); inactiveFilePanel.setCopyColorButtonsSource(activeFilePanel); // Adds the file groups panel. fileGroupsPanel = new FileGroupsPanel(parent, themeData); tabbedPane.add(fileGroupsPanel.getTitle(), createScrollPane(fileGroupsPanel)); // Creates the layout. setLayout(new BorderLayout()); add(tabbedPane, BorderLayout.NORTH); } /** * Creates the 'general' theme. */ private JPanel createGeneralPanel(FontChooser chooser) { ProportionalGridPanel panel = new ProportionalGridPanel(4); // Initializes the quicksearch panel. addLabelRow(panel); panel.add(addColorButtons(panel, chooser, "theme_editor.quick_search.unmatched_file", ThemeData.FILE_TABLE_UNMATCHED_FOREGROUND_COLOR, ThemeData.FILE_TABLE_UNMATCHED_BACKGROUND_COLOR)); JPanel quickSearchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); quickSearchPanel.add(panel); quickSearchPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.quick_search"))); // Initializes the panel. YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(chooser); mainPanel.addSpace(10); mainPanel.add(quickSearchPanel); // Wraps everything in a border layout. JPanel wrapper = new JPanel(new BorderLayout()); wrapper.add(mainPanel, BorderLayout.NORTH); return wrapper; } /** * Modification management. * Ignored. */ @Override public void commit() { fileGroupsPanel.commit(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/LocationBarPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.progress.ProgressTextField; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeId; import javax.swing.*; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * @author Nicolas Rinaudo, Maxence Bernard */ class LocationBarPanel extends ThemeEditorPanel implements PropertyChangeListener, ThemeId { private ProgressTextField normalPreview; private ProgressTextField progressPreview; /** * Creates a new file table editor. * @param parent dialog containing the panel. * @param themeData themeData being edited. */ LocationBarPanel(PreferencesDialog parent, ThemeData themeData) { super(parent, Translator.get("theme_editor.locationbar_tab"), themeData); initUI(); } private JPanel createConfigurationPanel() { FontChooser fontChooser; YBoxPanel mainPanel; JPanel flowPanel; ProportionalGridPanel colorsPanel; PreviewLabel label; fontChooser = createFontChooser(LOCATION_BAR_FONT); addFontChooserListener(fontChooser, normalPreview); addFontChooserListener(fontChooser, progressPreview); addLabelRow(colorsPanel = new ProportionalGridPanel(3), false); label = new PreviewLabel(); addColorButtons(colorsPanel, fontChooser, "theme_editor.normal", LOCATION_BAR_FOREGROUND_COLOR, LOCATION_BAR_BACKGROUND_COLOR, label).addPropertyChangeListener(this); addColorButtons(colorsPanel, fontChooser, "theme_editor.selected", LOCATION_BAR_SELECTED_FOREGROUND_COLOR, LOCATION_BAR_SELECTED_BACKGROUND_COLOR).addPropertyChangeListener(this); label.setTextPainted(true); addFontChooserListener(fontChooser, label); colorsPanel.add(createCaptionLabel("theme_editor.progress")); PreviewLabel previewLabel = new PreviewLabel(); previewLabel.setTextPainted(true); colorsPanel.add(new JLabel()); ColorButton btn = new ColorButton(parent, themeData, LOCATION_BAR_PROGRESS_COLOR, PreviewLabel.OVERLAY_COLOR_PROPERTY_NAME, previewLabel); colorsPanel.add(btn); btn.addUpdatedPreviewComponent(label); label.addPropertyChangeListener(this); flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); flowPanel.add(colorsPanel); flowPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); mainPanel = new YBoxPanel(); mainPanel.add(fontChooser); mainPanel.addSpace(10); mainPanel.add(flowPanel); /* normalPreview.setPreferredSize(new Dimension((normalPreview.getPreferredSize().width * 3) / 2, normalPreview.getPreferredSize().height)); progressPreview.setPreferredSize(new Dimension((progressPreview.getPreferredSize().width * 3) / 2, progressPreview.getPreferredSize().height)); */ return mainPanel; } private JPanel createPreviewPanel() { YBoxPanel panel; JPanel borderPanel; panel = new YBoxPanel(); // panel.add(new JLabel(Translator.get("theme_editor.normal"))); panel.add(createCaptionLabel("theme_editor.normal")); normalPreview = new ProgressTextField(0, themeData.getColor(LOCATION_BAR_PROGRESS_COLOR)); panel.add(normalPreview); normalPreview.setText(System.getProperty("user.home")); panel.addSpace(10); panel.add(createCaptionLabel("theme_editor.progress")); progressPreview = new ProgressTextField(50, themeData.getColor(LOCATION_BAR_PROGRESS_COLOR)); panel.add(progressPreview); progressPreview.setText(System.getProperty("user.home")); progressPreview.setEnabled(false); borderPanel = new JPanel(new BorderLayout()); borderPanel.add(panel, BorderLayout.NORTH); borderPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); setBackgroundColors(); setForegroundColors(); setProgressColors(); return borderPanel; } /** * Initializes the panel's UI. */ private void initUI() { JPanel panel; panel = new JPanel(new BorderLayout()); panel.add(createPreviewPanel(), BorderLayout.EAST); panel.add(createConfigurationPanel(), BorderLayout.CENTER); setLayout(new BorderLayout()); add(panel, BorderLayout.NORTH); } public void propertyChange(PropertyChangeEvent event) { // Background color changed. if (event.getPropertyName().equals(PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME)) { setBackgroundColors(); // Foreground color changed. } else if (event.getPropertyName().equals(PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME)) { setForegroundColors(); // Overlay color changed. } else if(event.getPropertyName().equals(PreviewLabel.OVERLAY_COLOR_PROPERTY_NAME)) { setProgressColors(); } } private void setBackgroundColors() { normalPreview.setBackground(themeData.getColor(LOCATION_BAR_BACKGROUND_COLOR)); normalPreview.setSelectionColor(themeData.getColor(LOCATION_BAR_SELECTED_BACKGROUND_COLOR)); progressPreview.setBackground(themeData.getColor(LOCATION_BAR_BACKGROUND_COLOR)); progressPreview.setSelectionColor(themeData.getColor(LOCATION_BAR_SELECTED_BACKGROUND_COLOR)); } private void setForegroundColors() { normalPreview.setForeground(themeData.getColor(LOCATION_BAR_FOREGROUND_COLOR)); normalPreview.setSelectedTextColor(themeData.getColor(LOCATION_BAR_SELECTED_FOREGROUND_COLOR)); progressPreview.setForeground(themeData.getColor(LOCATION_BAR_FOREGROUND_COLOR)); progressPreview.setSelectedTextColor(themeData.getColor(LOCATION_BAR_SELECTED_FOREGROUND_COLOR)); progressPreview.setDisabledTextColor(themeData.getColor(LOCATION_BAR_FOREGROUND_COLOR)); } private void setProgressColors() { progressPreview.setProgressColor(themeData.getColor(LOCATION_BAR_PROGRESS_COLOR)); } // - Modification management --------------------------------------------------------- // ----------------------------------------------------------------------------------- @Override public void commit() {} } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/QuickListPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.KeyListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.quicklist.QuickList; import com.mucommander.ui.quicklist.item.QuickListDataList; import com.mucommander.ui.quicklist.item.QuickListDataListWithIcons; import com.mucommander.ui.quicklist.item.QuickListHeaderItem; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeId; /** * @author Arik Hadas */ public class QuickListPanel extends ThemeEditorPanel implements PropertyChangeListener, ThemeId { // - Instance fields ----------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Used to preview the quick list. */ private JPanel quickListPreviewPanel; /** The header of the sample quick list */ private QuickListHeaderItem header = new QuickListHeaderItem(Translator.get("sample_text")); /** The items of the sample quick list */ private final static String[] sampleData; /** The icon of the sample items */ private final Icon sampleIcon = IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.FOLDER_ICON_NAME); static { String sampleText = Translator.get("sample_text"); sampleData = new String[10]; for (int i=0; i list = new QuickListDataListWithIcons(sampleData) { { for (KeyListener listener : getKeyListeners()) removeKeyListener(listener); } @Override public Icon getImageIconOfItem(String item, final Dimension preferredSize) { return sampleIcon; } @Override protected void addMouseListenerToList() { } }; /** * Creates the quick list preview panel. * @return the quick list preview panel. */ private JPanel createPreviewPanel() { JPanel panel; // Preview panel. JScrollPane scroll; // Wraps the preview quick list. // add JScrollPane that contains the TablePopupDataList to the popup. scroll = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scroll.setBorder(null); scroll.getVerticalScrollBar().setFocusable( false ); scroll.getHorizontalScrollBar().setFocusable( false ); // Creates the panel. panel = new JPanel(); quickListPreviewPanel = new YBoxPanel(); quickListPreviewPanel.add(header); quickListPreviewPanel.add(scroll); quickListPreviewPanel.setBorder(new QuickList.PopupsBorder()); panel.add(quickListPreviewPanel); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); return panel; } // - Initialization ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Creates a new quick list editor. * @param parent dialog containing the panel. * @param themeData themeData being edited. */ QuickListPanel(PreferencesDialog parent, ThemeData themeData) { super(parent, Translator.get("quick_lists_menu"), themeData); initUI(); } // - UI initialization --------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Creates the JPanel that contains all of the item's color configuration elements. * @param fontChooser font chooser used by the editor panel. * @return the JPanel that contains all of the item's color configuration elements. */ private JPanel createItemColorsPanel(FontChooser fontChooser) { ProportionalGridPanel gridPanel = new ProportionalGridPanel(3); // Contains all the color buttons. addLabelRow(gridPanel, false); PreviewLabel label = new PreviewLabel(); // Color buttons. addColorButtons(gridPanel, fontChooser, "theme_editor.normal", QUICK_LIST_ITEM_FOREGROUND_COLOR, QUICK_LIST_ITEM_BACKGROUND_COLOR, label).addPropertyChangeListener(this); addColorButtons(gridPanel, fontChooser, "theme_editor.selected", QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR, QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR, label).addPropertyChangeListener(this); label.addPropertyChangeListener(this); // Wraps everything in a flow layout. JPanel colorsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); colorsPanel.add(gridPanel); colorsPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); return colorsPanel; } /** * Creates the JPanel that contains all the header's color configuration elements. * @param fontChooser font chooser used by the editor panel. * @return the JPanel that contains all the header's color configuration elements. */ private JPanel createHeaderColorsPanel(FontChooser fontChooser) { ProportionalGridPanel gridPanel = new ProportionalGridPanel(3); // // Contains all the color buttons. // Header. addLabelRow(gridPanel, false); PreviewLabel label = new PreviewLabel(); // Color buttons. addColorButtons(gridPanel, fontChooser, "", QUICK_LIST_HEADER_FOREGROUND_COLOR, QUICK_LIST_HEADER_BACKGROUND_COLOR, label).addPropertyChangeListener(this); addColorButtons(gridPanel, fontChooser, "", -1, QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR, label).addPropertyChangeListener(this); label.addPropertyChangeListener(this); // Wraps everything in a flow layout. JPanel colorsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); colorsPanel.add(gridPanel); colorsPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); return colorsPanel; } /** * Initializes the panel's UI. */ private void initUI() { header.addComponentListener(new ComponentListener() { public void componentHidden(ComponentEvent e) {} public void componentMoved(ComponentEvent e) {} public void componentResized(ComponentEvent e) { quickListPreviewPanel.repaint(); } public void componentShown(ComponentEvent e) {} }); // Font chooser and preview initialization. FontChooser fontChooser1 = createFontChooser(QUICK_LIST_HEADER_FONT); FontChooser fontChooser2 = createFontChooser(QUICK_LIST_ITEM_FONT); addFontChooserListener(fontChooser1, header); addFontChooserListener(fontChooser2, list); // Header configuration panel initialization. YBoxPanel headerConfigurationPanel = new YBoxPanel(); headerConfigurationPanel.add(fontChooser1); headerConfigurationPanel.addSpace(10); headerConfigurationPanel.add(createHeaderColorsPanel(fontChooser1)); // Item configuration panel initialization. YBoxPanel itemConfigurationPanel = new YBoxPanel(); itemConfigurationPanel.add(fontChooser2); itemConfigurationPanel.addSpace(10); itemConfigurationPanel.add(createItemColorsPanel(fontChooser1)); // create the tabbed pane. JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.add(Translator.get("theme_editor.header"), headerConfigurationPanel); tabbedPane.add(Translator.get("theme_editor.item"), itemConfigurationPanel); // Main layout. JPanel mainPanel = new JPanel(new BorderLayout()); mainPanel.add(tabbedPane, BorderLayout.CENTER); mainPanel.add(createPreviewPanel(), BorderLayout.EAST); // Layout. setLayout(new BorderLayout()); add(mainPanel, BorderLayout.NORTH); } /** * Listens on changes on the foreground and background colors. */ public void propertyChange(PropertyChangeEvent event) { // Background color changed. if (event.getPropertyName().equals(PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME)) { header.setBackgroundColors(themeData.getColor(QUICK_LIST_HEADER_BACKGROUND_COLOR), themeData.getColor(QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR)); list.setBackgroundColors(themeData.getColor(QUICK_LIST_ITEM_BACKGROUND_COLOR), themeData.getColor(QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR)); } // Foreground color changed. else if (event.getPropertyName().equals(PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME)) { header.setForegroundColor(themeData.getColor(QUICK_LIST_HEADER_FOREGROUND_COLOR)); list.setForegroundColors(themeData.getColor(QUICK_LIST_ITEM_FOREGROUND_COLOR), themeData.getColor(QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR)); } } /** * Ignored. */ @Override public void commit() {} } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/ShellPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.RuntimeConstants; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.combobox.EditableComboBox; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.theme.ThemeData; import javax.swing.*; import java.awt.BorderLayout; import java.awt.FlowLayout; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * @author Nicolas Rinaudo, Maxence Bernard */ class ShellPanel extends ThemeEditorPanel implements PropertyChangeListener { private JTextArea shellPreview; private JTabbedPane tabbedPane; private EditableComboBox historyPreview; private JLabel lblRun, lblOutput; /** * Creates a new file table editor. * @param parent dialog containing the panel. * @param themeData themeData being edited. */ ShellPanel(PreferencesDialog parent, ThemeData themeData) { super(parent, Translator.get("theme_editor.shell_tab"), themeData); initUI(); } private JComponent createConfigurationPanel(int fontId, int foregroundId, int backgroundId, int selectedForegroundId, int selectedBackgroundId, JComponent fontListener) { YBoxPanel mainPanel = new YBoxPanel(); FontChooser fontChooser = createFontChooser(fontId); mainPanel.add(fontChooser); mainPanel.addSpace(10); addFontChooserListener(fontChooser, fontListener); ProportionalGridPanel colorPanel = new ProportionalGridPanel(3); addLabelRow(colorPanel, false); addColorButtons(colorPanel, fontChooser, "theme_editor.normal", foregroundId, backgroundId).addPropertyChangeListener(this); addColorButtons(colorPanel, fontChooser, "theme_editor.selected", selectedForegroundId, selectedBackgroundId).addPropertyChangeListener(this); JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); flowPanel.add(colorPanel); flowPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); mainPanel.add(flowPanel); return createScrollPane(mainPanel); } private JPanel createPreviewPanel() { JPanel panel = new JPanel(new BorderLayout()); panel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); YBoxPanel headerPanel = new YBoxPanel(); lblRun = new JLabel(Translator.get("run_dialog.run_command_description") + ":"); headerPanel.add(lblRun); headerPanel.add(historyPreview = new EditableComboBox<>(new JTextField("trolcommander -v"))); historyPreview.addItem("trolcommander -v"); historyPreview.addItem("java -version"); headerPanel.addSpace(10); lblOutput = new JLabel(Translator.get("run_dialog.command_output")+":"); headerPanel.add(lblOutput); panel.add(headerPanel, BorderLayout.NORTH); shellPreview = new JTextArea(20, 15); JScrollPane scroll = new JScrollPane(shellPreview, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); panel.add(scroll, BorderLayout.CENTER); scroll.getViewport().setPreferredSize(shellPreview.getPreferredSize()); shellPreview.append(RuntimeConstants.APP_STRING); shellPreview.append("\nCopyright (C) "); shellPreview.append(RuntimeConstants.COPYRIGHT); shellPreview.append(" Oleg Trifonov\nThis is free software, distributed under the terms of the GNU General Public License."); shellPreview.setCaretPosition(0); setForegroundColors(); setBackgroundColors(); return panel; } /** * Initializes the panel's UI. */ private void initUI() { setLayout(new BorderLayout()); tabbedPane = new JTabbedPane(); JPanel previewPanel = createPreviewPanel(); tabbedPane.add(Translator.get("theme_editor.shell_tab"), createConfigurationPanel(ThemeData.SHELL_FONT, ThemeData.SHELL_FOREGROUND_COLOR, ThemeData.SHELL_BACKGROUND_COLOR, ThemeData.SHELL_SELECTED_FOREGROUND_COLOR, ThemeData.SHELL_SELECTED_BACKGROUND_COLOR, shellPreview)); tabbedPane.add(Translator.get("theme_editor.shell_history_tab"), createConfigurationPanel(ThemeData.SHELL_HISTORY_FONT, ThemeData.SHELL_HISTORY_FOREGROUND_COLOR, ThemeData.SHELL_HISTORY_BACKGROUND_COLOR, ThemeData.SHELL_HISTORY_SELECTED_FOREGROUND_COLOR, ThemeData.SHELL_HISTORY_SELECTED_BACKGROUND_COLOR, historyPreview)); tabbedPane.add(Translator.get("theme_editor.terminal_tab"), createConfigurationPanel(ThemeData.TERMINAL_FONT, ThemeData.TERMINAL_FOREGROUND_COLOR, ThemeData.TERMINAL_BACKGROUND_COLOR, ThemeData.TERMINAL_SELECTED_FOREGROUND_COLOR, ThemeData.TERMINAL_SELECTED_BACKGROUND_COLOR, shellPreview)); tabbedPane.addChangeListener(e -> { setBackgroundColors(); setForegroundColors(); boolean shellMode = tabbedPane.getSelectedIndex() < 2; historyPreview.setVisible(shellMode); lblRun.setVisible(shellMode); lblOutput.setVisible(shellMode); }); JPanel mainPanel = new JPanel(new BorderLayout()); mainPanel.add(tabbedPane, BorderLayout.WEST); mainPanel.add(previewPanel, BorderLayout.CENTER); add(mainPanel, BorderLayout.NORTH); // ThemeData.addDefaultValuesListener(new ThemeListener() { // // @Override // public void colorChanged(ColorChangedEvent event) { //System.out.println(event); // setForegroundColors(); // setBackgroundColors(); // } // // @Override // public void fontChanged(FontChangedEvent event) { // } // }); } public void propertyChange(final PropertyChangeEvent event) { if (event.getPropertyName().equals(PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME)) { setBackgroundColors(); } else if(event.getPropertyName().equals(PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME)) { setForegroundColors(); } } private void setBackgroundColors() { boolean shellMode = tabbedPane.getSelectedIndex() < 2; if (shellMode) { shellPreview.setBackground(themeData.getColor(ThemeData.SHELL_BACKGROUND_COLOR)); shellPreview.setSelectionColor(themeData.getColor(ThemeData.SHELL_SELECTED_BACKGROUND_COLOR)); historyPreview.setBackground(themeData.getColor(ThemeData.SHELL_HISTORY_BACKGROUND_COLOR)); historyPreview.setSelectionBackground(themeData.getColor(ThemeData.SHELL_HISTORY_SELECTED_BACKGROUND_COLOR)); } else { shellPreview.setBackground(themeData.getColor(ThemeData.TERMINAL_BACKGROUND_COLOR)); shellPreview.setSelectionColor(themeData.getColor(ThemeData.TERMINAL_SELECTED_BACKGROUND_COLOR)); } } private void setForegroundColors() { boolean shellMode = tabbedPane.getSelectedIndex() < 2; if (shellMode) { shellPreview.setForeground(themeData.getColor(ThemeData.SHELL_FOREGROUND_COLOR)); shellPreview.setSelectedTextColor(themeData.getColor(ThemeData.SHELL_SELECTED_FOREGROUND_COLOR)); shellPreview.setCaretColor(themeData.getColor(ThemeData.SHELL_FOREGROUND_COLOR)); historyPreview.setForeground(themeData.getColor(ThemeData.SHELL_HISTORY_FOREGROUND_COLOR)); historyPreview.setSelectionForeground(themeData.getColor(ThemeData.SHELL_HISTORY_SELECTED_FOREGROUND_COLOR)); } else { shellPreview.setForeground(themeData.getColor(ThemeData.TERMINAL_FOREGROUND_COLOR)); shellPreview.setSelectedTextColor(themeData.getColor(ThemeData.TERMINAL_SELECTED_FOREGROUND_COLOR)); shellPreview.setCaretColor(themeData.getColor(ThemeData.TERMINAL_FOREGROUND_COLOR)); } } @Override public void commit() {} } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/StatusBarPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.utils.text.SizeFormat; import com.mucommander.utils.text.Translator; import com.mucommander.ui.border.MutableLineBorder; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeId; import javax.swing.*; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * @author Nicolas Rinaudo, Maxence Bernard */ class StatusBarPanel extends ThemeEditorPanel implements PropertyChangeListener, ThemeId { private static final int OK = 0; private static final int WARNING = 1; private static final int CRITICAL = 2; private final static int WARNING_LEVEL_COLOR_IDS[] = { STATUS_BAR_OK_COLOR, STATUS_BAR_WARNING_COLOR, STATUS_BAR_CRITICAL_COLOR }; private final static String WARNING_LEVEL_LABELS[] = { "theme_editor.free_space.ok", "theme_editor.free_space.warning", "theme_editor.free_space.critical" }; private final static int VOLUME_INFO_SIZE_FORMAT = SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_SHORT | SizeFormat.INCLUDE_SPACE | SizeFormat.ROUND_TO_KB; private final static long TOTAL_SIZE = 85899345920L; private final static long NORMAL_SIZE = TOTAL_SIZE / 2; private final static long WARNING_SIZE = TOTAL_SIZE / 10; private final static long CRITICAL_SIZE = TOTAL_SIZE / 100; private final static int WARNING_DRAW_PERCENTAGE[] = {50, 10, 1}; private final static String WARNING_LEVEL_TEXT[] = { Translator.get("status_bar.volume_free", SizeFormat.format(NORMAL_SIZE, VOLUME_INFO_SIZE_FORMAT) + " / " + SizeFormat.format(TOTAL_SIZE, VOLUME_INFO_SIZE_FORMAT)), Translator.get("status_bar.volume_free", SizeFormat.format(WARNING_SIZE, VOLUME_INFO_SIZE_FORMAT) + " / " + SizeFormat.format(TOTAL_SIZE, VOLUME_INFO_SIZE_FORMAT)), Translator.get("status_bar.volume_free", SizeFormat.format(CRITICAL_SIZE, VOLUME_INFO_SIZE_FORMAT) + " / " + SizeFormat.format(TOTAL_SIZE, VOLUME_INFO_SIZE_FORMAT)) }; private JLabel normalPreview; private Preview okPreview; private Preview warningPreview; private Preview criticalPreview; /** * Creates a new file table editor. * @param parent dialog containing the panel. * @param themeData themeData being edited. */ StatusBarPanel(PreferencesDialog parent, ThemeData themeData) { super(parent, Translator.get("theme_editor.statusbar_tab"), themeData); initUI(); } private JPanel createGeneralPanel(FontChooser chooser, ColorButton foreground) { // Initializes the color panel. JPanel colorPanel = new ProportionalGridPanel(2); colorPanel.add(createCaptionLabel("theme_editor.text")); colorPanel.add(foreground); // Wraps the color panel in a flow layout. JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); flowPanel.add(colorPanel); flowPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.colors"))); // Creates the general panel. YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(chooser); mainPanel.addSpace(10); mainPanel.add(flowPanel); return mainPanel; } private JPanel createFreeSpacePanel(FontChooser chooser, ColorButton foreground, ColorButton background, ColorButton border) { JPanel colorPanel; JPanel flowPanel; PreviewLabel previewLabel; colorPanel = new ProportionalGridPanel(2); colorPanel.add(new JLabel()); colorPanel.add(createCaptionLabel("theme_editor.color")); colorPanel.add(createCaptionLabel("theme_editor.background")); colorPanel.add(background); colorPanel.add(createCaptionLabel("theme_editor.border")); colorPanel.add(border); for(int i=0; i<3; i++) { previewLabel = new PreviewLabel(); previewLabel.setOverlayUnderText(true); previewLabel.setTextPainted(true); foreground.addUpdatedPreviewComponent(previewLabel); background.addUpdatedPreviewComponent(previewLabel); border.addUpdatedPreviewComponent(previewLabel); addFontChooserListener(chooser, previewLabel); colorPanel.add(createCaptionLabel(WARNING_LEVEL_LABELS[i])); colorPanel.add(new ColorButton(parent, themeData, WARNING_LEVEL_COLOR_IDS[i], PreviewLabel.OVERLAY_COLOR_PROPERTY_NAME, previewLabel)); previewLabel.addPropertyChangeListener(this); } flowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); flowPanel.add(colorPanel); return flowPanel; } private void addPreviewLabel(YBoxPanel panel, JLabel preview, String label, FontChooser chooser) { JPanel wrapper; panel.add(createCaptionLabel(label)); wrapper = new JPanel(new BorderLayout()); wrapper.add(preview, BorderLayout.NORTH); panel.add(wrapper); addFontChooserListener(chooser, preview); } private JPanel createPreviewPanel(FontChooser fontChooser) { YBoxPanel previewPanel; Insets insets; previewPanel = new YBoxPanel(); addPreviewLabel(previewPanel, normalPreview = new JLabel(Translator.get("status_bar.selected_files", "3", "14")), "theme_editor.normal", fontChooser); normalPreview.setForeground(themeData.getColor(ThemeData.STATUS_BAR_FOREGROUND_COLOR)); addPreviewLabel(previewPanel, okPreview = new Preview(OK), "theme_editor.free_space.ok", fontChooser); addPreviewLabel(previewPanel, warningPreview = new Preview(WARNING), "theme_editor.free_space.warning", fontChooser); addPreviewLabel(previewPanel, criticalPreview = new Preview(CRITICAL), "theme_editor.free_space.critical", fontChooser); previewPanel.setBorder(BorderFactory.createTitledBorder(Translator.get("preview"))); insets = previewPanel.getInsets(); previewPanel.setInsets(new Insets(insets.top, insets.left + 8, insets.bottom, insets.right + 6)); return previewPanel; } /** * Initializes the panel's UI. */ private void initUI() { JPanel mainPanel; ColorButton foreground; ColorButton background; ColorButton border; PreviewLabel previewLabel; PreviewLabel borderPreviewLabel; FontChooser fontChooser; JTabbedPane tabbedPane; fontChooser = createFontChooser(STATUS_BAR_FONT); // Initializes the foreground color button. foreground = new ColorButton(parent, themeData, STATUS_BAR_FOREGROUND_COLOR, PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME, previewLabel = new PreviewLabel()); previewLabel.setTextPainted(true); addFontChooserListener(fontChooser, previewLabel); previewLabel.addPropertyChangeListener(this); // Initializes the background and border color buttons. background = new ColorButton(parent, themeData, STATUS_BAR_BACKGROUND_COLOR, PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME, previewLabel = new PreviewLabel()); border = new ColorButton(parent, themeData, STATUS_BAR_BORDER_COLOR, PreviewLabel.BORDER_COLOR_PROPERTY_NAME, borderPreviewLabel = new PreviewLabel()); // Initializes the background color preview. previewLabel.setTextPainted(true); foreground.addUpdatedPreviewComponent(previewLabel); border.addUpdatedPreviewComponent(previewLabel); addFontChooserListener(fontChooser, previewLabel); previewLabel.addPropertyChangeListener(this); // Initializes the border color preview. borderPreviewLabel.setTextPainted(true); foreground.addUpdatedPreviewComponent(borderPreviewLabel); background.addUpdatedPreviewComponent(borderPreviewLabel); addFontChooserListener(fontChooser, borderPreviewLabel); borderPreviewLabel.addPropertyChangeListener(this); tabbedPane = new JTabbedPane(); tabbedPane.add(Translator.get("theme_editor.general"), createGeneralPanel(fontChooser, foreground)); tabbedPane.add(Translator.get("theme_editor.free_space"), createFreeSpacePanel(fontChooser, foreground, background, border)); // Main layout. mainPanel = new JPanel(new BorderLayout()); mainPanel.add(tabbedPane, BorderLayout.CENTER); mainPanel.add(createPreviewPanel(fontChooser), BorderLayout.EAST); // Aligns everything north. setLayout(new BorderLayout()); add(mainPanel, BorderLayout.NORTH); } // - Modification management --------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Ignored. */ @Override public void commit() {} // - Property listening -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Refreshes the UI depending on the property event. */ public void propertyChange(PropertyChangeEvent event) { switch (event.getPropertyName()) { // Repaints previews when the overlay or background color have been changed case PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME: case PreviewLabel.OVERLAY_COLOR_PROPERTY_NAME: okPreview.repaint(); warningPreview.repaint(); criticalPreview.repaint(); break; // Resets the preview labels' foreground color case PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME: okPreview.repaint(); warningPreview.repaint(); criticalPreview.repaint(); break; // Resets the preview labels' borders case PreviewLabel.BORDER_COLOR_PROPERTY_NAME: okPreview.refreshBorder(); warningPreview.refreshBorder(); criticalPreview.refreshBorder(); break; } } // - Preview labels ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- private class Preview extends JLabel { private MutableLineBorder border; private int type; Preview(int type) { super(WARNING_LEVEL_TEXT[type]); setOpaque(false); setBorder(border = new MutableLineBorder(Color.BLACK, 1)); setHorizontalAlignment(CENTER); setForeground(themeData.getColor(STATUS_BAR_FOREGROUND_COLOR)); this.type = type; } void refreshBorder() { border.setLineColor(themeData.getColor(STATUS_BAR_BORDER_COLOR)); repaint(); } @Override public void paint(Graphics g) { int width = ((getWidth() - 2) * WARNING_DRAW_PERCENTAGE[type]) / 100; if (type == OK) { g.setColor(themeData.getColor(STATUS_BAR_OK_COLOR)); } else if (type == WARNING) { g.setColor(themeData.getColor(STATUS_BAR_WARNING_COLOR)); } else { g.setColor(themeData.getColor(STATUS_BAR_CRITICAL_COLOR)); } g.fillRect(1, 1, width + 1, getHeight() - 2); g.setColor(themeData.getColor(STATUS_BAR_BACKGROUND_COLOR)); g.fillRect(width + 1, 1, getWidth() - width - 1, getHeight() - 2); super.paint(g); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/ThemeEditorDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.conf.TcSnapshot; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.component.PrefComponent; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeManager; import java.awt.*; /** * Main dialog for the theme editor. * @author Nicolas Rinaudo */ public class ThemeEditorDialog extends PreferencesDialog { private ThemeData data; private Theme theme; private Theme modifiedTheme; /** * Creates a new theme editor dialog. * @param parent parent of the dialog. * @param theme theme to edit. */ public ThemeEditorDialog(Dialog parent, Theme theme) { super(parent, createTitle(theme)); initUI(theme); } /** * Creates a new theme editor dialog. * @param parent parent of the dialog. * @param theme theme to edit. */ public ThemeEditorDialog(Frame parent, Theme theme) { super(parent, createTitle(theme)); initUI(theme); } private static String createTitle(Theme theme) { return Translator.get("theme_editor.title") + ": " + theme.getName(); } private void initUI(Theme theme) { this.theme = theme; data = theme.cloneData(); addPreferencesPanel(new FolderPanePanel(this, data), false); addPreferencesPanel(new LocationBarPanel(this, data)); addPreferencesPanel(new StatusBarPanel(this, data)); addPreferencesPanel(new ShellPanel(this, data)); addPreferencesPanel(new FileEditorPanel(this, data)); addPreferencesPanel(new QuickListPanel(this, data)); // Sets the dialog's size. Dimension screenSize = TcSnapshot.getScreenSize(); Dimension minimumSize = new Dimension(580, 300); Dimension maximumSize = new Dimension(screenSize); if (screenSize.getWidth() >= 1024 && screenSize.getHeight() > 700) { minimumSize.setSize(800, 480); } setMinimumSize(minimumSize); setMaximumSize(maximumSize); } /** * Edits the theme specified at creation time and returns the actually modified theme. This is a new custom theme if * a pedefined theme was specified or else the theme itself. * * @return the actually modified theme it was modified by the user, null otherwise. */ public Theme editTheme() { showDialog(); return modifiedTheme; } @Override public boolean checkCommit() { super.checkCommit(); // If the theme has been modified and is a predefined theme, asks the user to confirm // whether it's ok to duplicate it. if (!theme.isIdentical(data) && !theme.canModify()) return new QuestionDialog(this, Translator.get("warning"), Translator.get("theme_editor.theme_warning_predefined"), this, new String[]{Translator.get("yes"), Translator.get("no")}, new int[]{0, 1}, 0).getActionValue() == 0; return true; } @Override public void commit() { super.commit(); if (theme.isIdentical(data)) { return; } try { // If the theme cannot be modified, create a new custom theme with the same name to save modifications. if (!theme.canModify()) { modifiedTheme = ThemeManager.duplicateTheme(theme); } else { modifiedTheme = theme; } modifiedTheme.importData(data); ThemeManager.writeTheme(modifiedTheme); theme = modifiedTheme; } catch (Exception exception) { try { InformationDialog.showErrorDialog(this, Translator.get("write_error"), Translator.get("cannot_write_file", ThemeManager.getFile(theme.getType(), theme.getName()).getAbsolutePath())); exception.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } @Override public void componentChanged(PrefComponent component) { setCommitButtonsEnabled(!theme.isIdentical(data)); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/ThemeEditorPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.pref.theme; import com.mucommander.utils.text.Translator; import com.mucommander.ui.chooser.FontChooser; import com.mucommander.ui.chooser.PreviewLabel; import com.mucommander.ui.dialog.pref.PreferencesDialog; import com.mucommander.ui.dialog.pref.PreferencesPanel; import com.mucommander.ui.layout.ProportionalGridPanel; import com.mucommander.ui.theme.ThemeData; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.util.ArrayList; import java.util.List; /** * Base class for theme editor panels. *

    * A ThemeEditorPanel is a {@link com.mucommander.ui.dialog.pref.PreferencesPanel} with some * theme specific features: *

      *
    • Access to the {@link #themeData ThemeData} being edited.
    • *
    • Helper methods for theme-specific layout creation.
    • *
    * * @author Maxence Bernard, Nicolas Rinaudo */ abstract class ThemeEditorPanel extends PreferencesPanel { /** Edited theme data. */ protected ThemeData themeData; /** Holds references to listeners to prevent them from being garbage collected. */ private final List listenerReferences = new ArrayList<>(); /** Font used to display caption labels. */ private Font captionLabelFont; /** Color used to display caption labels. */ private final Color captionTextColor = new Color(48, 48, 48); /** * Creates a new ThemeEditorPanel. * @param parent dialog in which the panel is stored. * @param title title of the panel. * @param themeData data that is being edited. */ ThemeEditorPanel(PreferencesDialog parent, String title, ThemeData themeData) { super(parent, title); this.themeData = themeData; captionLabelFont = new JLabel().getFont(); captionLabelFont = captionLabelFont.deriveFont(Font.BOLD, captionLabelFont.getSize()-1.5f); } // - Caption label methods ----------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Creates a caption label containing the specified localised entry. * @param dictionaryKey name of the dictionary entry to use in the label. * @return a caption label containing the specified localised entry. */ JLabel createCaptionLabel(String dictionaryKey) { JLabel captionLabel = new JLabel(Translator.get(dictionaryKey)); captionLabel.setFont(captionLabelFont); captionLabel.setForeground(captionTextColor); return captionLabel; } /** * Creates a caption label containing the specified entry. * @param title label title * @return a caption label containing the specified entry. */ JLabel createCaptionLabelWithTitle(String title) { JLabel captionLabel = new JLabel(title); captionLabel.setFont(captionLabelFont); captionLabel.setForeground(captionTextColor); return captionLabel; } /** * Adds a row with standard color type labels. *

    * This is a convenience method and is strictly equivalent to calling * {@link #addLabelRow(ProportionalGridPanel,boolean) addLabelRow}(pane, true). * * @param panel panel in which to add the label row. */ void addLabelRow(ProportionalGridPanel panel) {addLabelRow(panel, true);} /** * Adds a row with standard color type labels. *

    * The labels that will be created are: *

         *    <EMPTY> | Text | Background | (Preview)
         * 
    * * @param panel panel in which to add the label row. * @param includePreview whether to add the preview label. */ void addLabelRow(ProportionalGridPanel panel, boolean includePreview) { // Skips first column. panel.add(new JLabel()); // Creates the standard labels. panel.add(createCaptionLabel("theme_editor.text")); panel.add(createCaptionLabel("theme_editor.background")); // Adds the preview label if requested. if (includePreview) { panel.add(createCaptionLabel("preview")); } } // - Font chooser code --------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Creates a font chooser that will keep the specified font up-to-date in the current theme data. * @param fontId identifier of the font this chooser will be editing. */ FontChooser createFontChooser(int fontId) { // Initializes the font chooser. FontChooser fontChooser = new FontChooser(themeData.getFont(fontId)); fontChooser.setBorder(BorderFactory.createTitledBorder(Translator.get("theme_editor.font"))); ChangeListener listener = new ThemeFontChooserListener(themeData, fontId, parent); fontChooser.addChangeListener(listener); // Hold a reference to this listener to prevent garbage collection listenerReferences.add(listener); return fontChooser; } /** * Registers a listener on the specified font chooser. *

    * The specified listener will receive calls to its setFont method whenever * the font chooser has been updated. * * @param fontChooser chooser to monitor. * @param previewComponent component whose font should be tied to that of the chooser */ void addFontChooserListener(FontChooser fontChooser, JComponent previewComponent) { // Update button font when a new font has been chosen in the FontChooser if (fontChooser != null) { ChangeListener listener; fontChooser.addChangeListener(listener = new PreviewFontChooserListener(previewComponent)); previewComponent.setFont(fontChooser.getCurrentFont()); // Hold a reference to this listener to prevent garbage collection listenerReferences.add(listener); } } // - Scroll pane methods ------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Wraps the specified panel within a scroll pane. *

    * The resulting scroll pane will have a vertical bar as needed, no horizontal scroll bar policy. * * @param panel panel to wrap in a JScrollPane. */ JComponent createScrollPane(JPanel panel) { JScrollPane scrollPane = new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollPane.setBorder(null); return scrollPane; } // - Color buttons methods ----------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Adds color buttons to the specified panel. *

    * This is a convenience method and is strictly equivalent to calling * addColorButtons(gridPanel, fontChooser, label, foregroundId, backgroundId, null). * * @param gridPanel a 3 columns proportinal grid panel in which to add the buttons. * @param fontChooser used to decide which font to use in each color button's preview. * @param label label for the row. * @param foregroundId identifier of the color to display in the foreground button. * @param backgroundId identifier of the color to display in the background button. */ PreviewLabel addColorButtons(ProportionalGridPanel gridPanel, FontChooser fontChooser, String label, int foregroundId, int backgroundId) { return addColorButtons(gridPanel, fontChooser, label, foregroundId, backgroundId, null); } /** * Adds color buttons to the specified panel. *

    * This method will create a row containing the following items: *

         * LABEL | COLOR (foreground) | COLOR (background)
         * 
    * * @param gridPanel a 3 columns proportional grid panel in which to add the buttons. * @param fontChooser used to decide which font to use in each color button's preview. * @param label label for the row. * @param foregroundId identifier of the color to display in the foreground button. * @param backgroundId identifier of the color to display in the background button. * @param comp component to register as a listener on the color buttons. */ PreviewLabel addColorButtons(ProportionalGridPanel gridPanel, FontChooser fontChooser, String label, int foregroundId, int backgroundId, JComponent comp) { // Adds the row's caption label. gridPanel.add(createCaptionLabel(label)); // Initializes the color buttons' preview label. PreviewLabel previewLabel = new PreviewLabel(); previewLabel.setTextPainted(true); addFontChooserListener(fontChooser, previewLabel); // Creates the foreground color button. if (foregroundId >= 0 ) { ColorButton colorButton = new ColorButton(parent, themeData, foregroundId, PreviewLabel.FOREGROUND_COLOR_PROPERTY_NAME, previewLabel); gridPanel.add(colorButton); if (comp != null) { colorButton.addUpdatedPreviewComponent(comp); } } else { gridPanel.add(Box.createHorizontalGlue()); } // Creates the background color button. if (backgroundId >= 0) { ColorButton colorButton = new ColorButton(parent, themeData, backgroundId, PreviewLabel.BACKGROUND_COLOR_PROPERTY_NAME, previewLabel); gridPanel.add(colorButton); if (comp != null) { colorButton.addUpdatedPreviewComponent(comp); } } else { gridPanel.add(Box.createHorizontalGlue()); } return previewLabel; } // - Ad-hoc FontChooser listeners ---------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Used to listen on FontChoosers and update theme data when the font is changed. * @author Nicolas Rinaudo */ private static class ThemeFontChooserListener implements ChangeListener { /** Theme data in which to update the font when it changes. */ private final ThemeData data; /** Identifier of the font we're listening on. */ private final int fontId; /** Parent dialog of this panel **/ private final PreferencesDialog dialog; /** * Creates a new ThemeFontChooserListener. * @param data theme data to modify when change events are received. * @param fontId identifier of the font that is being listened on. */ ThemeFontChooserListener(ThemeData data, int fontId, PreferencesDialog dialog) { this.data = data; this.fontId = fontId; this.dialog = dialog; } /** * Updates the theme data with the new font value. */ public void stateChanged(ChangeEvent event) { data.setFont(fontId, ((FontChooser)event.getSource()).getCurrentFont()); // Inform the panel's parent dialog that a component in it was changed. dialog.componentChanged(null); } } /** * Used to listen on FontChoosers and update preview components when the font is changed. * @author Nicolas Rinaudo */ private static class PreviewFontChooserListener implements ChangeListener { /** Component to update when the font has changed. */ private final JComponent preview; /** * Creates a new instance of PreviewFontChooserListener. * @param preview component to update when the font has changed. */ PreviewFontChooserListener(JComponent preview) { this.preview = preview; } /** * Updates the preview component. */ public void stateChanged(ChangeEvent event) { preview.setFont(((FontChooser)event.getSource()).getCurrentFont()); } } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/pref/theme/package.html ================================================ UI component used to let users modify a theme. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/FTPPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.ftp.FTPFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.DialogOwner; import com.mucommander.ui.encoding.EncodingListener; import com.mucommander.ui.encoding.EncodingSelectBox; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.net.MalformedURLException; import java.text.ParseException; /** * This ServerPanel helps initiate FTP connections. * * @author Maxence Bernard */ public class FTPPanel extends ServerPanel implements ActionListener, EncodingListener { private final static int STANDARD_PORT = FileURL.getRegisteredHandler(FileProtocols.FTP).getStandardPort(); private static final Credentials ANONYMOUS_CREDENTIALS = FileURL.getRegisteredHandler(FileProtocols.FTP).getGuestCredentials(); private final JTextField serverField; private final JTextField usernameField; private final JPasswordField passwordField; private final JTextField initialDirField; private final JSpinner portSpinner; private final JSpinner nbRetriesSpinner; private final JSpinner retryDelaySpinner; private final EncodingSelectBox encodingSelectBox; private final JCheckBox passiveCheckBox; private final JCheckBox anonymousCheckBox; private static String lastServer = ""; private static String lastUsername = ""; private static String lastInitialDir = "/"; private static int lastPort = STANDARD_PORT; private static String lastEncoding = FTPFile.DEFAULT_ENCODING; // Not static so that it is not remembered (for security reasons) private String lastPassword = ""; /** Passive mode is enabled by default because of firewall restrictions */ private static boolean passiveMode = true; private static boolean anonymousUser; FTPPanel(final ServerConnectDialog dialog, MainFrame mainFrame) { super(dialog, mainFrame); // Server field, initialized to last server entered serverField = new JTextField(lastServer); serverField.selectAll(); addTextFieldListeners(serverField, true); addRow(Translator.get("server_connect_dialog.server"), serverField, 15); // Username field, initialized to last username entered or 'anonymous' if anonymous user was previously selected usernameField = new JTextField(anonymousUser?ANONYMOUS_CREDENTIALS.getLogin():lastUsername); usernameField.selectAll(); usernameField.setEditable(!anonymousUser); addTextFieldListeners(usernameField, false); addRow(Translator.get("server_connect_dialog.username"), usernameField, 5); // Password field, initialized to "" passwordField = new JPasswordField(); addTextFieldListeners(passwordField, false); addRow(Translator.get("password"), passwordField, 15); // Initial directory field, initialized to "/" initialDirField = new JTextField(lastInitialDir); initialDirField.selectAll(); addTextFieldListeners(initialDirField, true); addRow(Translator.get("server_connect_dialog.initial_dir"), initialDirField, 5); // Port field, initialized to last port (default is 21) portSpinner = createPortSpinner(lastPort); addRow(Translator.get("server_connect_dialog.port"), portSpinner, 15); // Encoding combo box encodingSelectBox = new EncodingSelectBox(new DialogOwner(mainFrame.getJFrame()), lastEncoding); encodingSelectBox.addEncodingListener(this); addRow(Translator.get("encoding"), encodingSelectBox, 15); // Connection retries when server busy nbRetriesSpinner = createIntSpinner(FTPFile.DEFAULT_NB_CONNECTION_RETRIES, 0, Integer.MAX_VALUE, 1); addRow(Translator.get("ftp_connect.nb_connection_retries"), nbRetriesSpinner, 5); // Delay between two retries retryDelaySpinner = createIntSpinner(FTPFile.DEFAULT_CONNECTION_RETRY_DELAY, 0, Integer.MAX_VALUE, 1); addRow(Translator.get("ftp_connect.retry_delay"), retryDelaySpinner, 15); // Anonymous user checkbox anonymousCheckBox = new JCheckBox(Translator.get("ftp_connect.anonymous_user"), anonymousUser); anonymousCheckBox.addActionListener(this); addRow("", anonymousCheckBox, 5); // Passive mode checkbox passiveCheckBox = new JCheckBox(Translator.get("ftp_connect.passive_mode"), passiveMode); passiveCheckBox.addActionListener(this); addRow("", passiveCheckBox, 0); } private void updateValues() { lastServer = serverField.getText(); if(!anonymousUser) { lastUsername = usernameField.getText(); lastPassword = new String(passwordField.getPassword()); } lastInitialDir = initialDirField.getText(); lastPort = (Integer) portSpinner.getValue(); } //////////////////////////////// // ServerPanel implementation // //////////////////////////////// @Override FileURL getServerURL() throws MalformedURLException { updateValues(); if(!lastInitialDir.startsWith("/")) lastInitialDir = "/"+lastInitialDir; FileURL url = FileURL.getFileURL(FileProtocols.FTP+"://"+lastServer+lastInitialDir); if(anonymousUser) url.setCredentials(new Credentials(ANONYMOUS_CREDENTIALS.getLogin(), new String(passwordField.getPassword()))); else url.setCredentials(new Credentials(lastUsername, lastPassword)); // Set port url.setPort(lastPort); // Set passiveMode property to true (default) or false url.setProperty(FTPFile.PASSIVE_MODE_PROPERTY_NAME, ""+passiveMode); // Set FTP encoding property url.setProperty(FTPFile.ENCODING_PROPERTY_NAME, encodingSelectBox.getSelectedEncoding()); // Set connection retry properties url.setProperty(FTPFile.NB_CONNECTION_RETRIES_PROPERTY_NAME, ""+nbRetriesSpinner.getValue()); url.setProperty(FTPFile.CONNECTION_RETRY_DELAY_PROPERTY_NAME, ""+retryDelaySpinner.getValue()); return url; } @Override boolean usesCredentials() { return true; } @Override public void dialogValidated() { // Commits the current spinner value in case it was being edited and 'enter' was pressed // (the spinner value would otherwise not be committed) try { portSpinner.commitEdit(); } catch(ParseException ignored) { } updateValues(); } //////////////////////////// // ActionListener methods // //////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == passiveCheckBox) { passiveMode = passiveCheckBox.isSelected(); } else if (source == anonymousCheckBox) { updateValues(); anonymousUser = anonymousCheckBox.isSelected(); if (anonymousUser) { usernameField.setEnabled(false); usernameField.setText(ANONYMOUS_CREDENTIALS.getLogin()); passwordField.setEnabled(false); passwordField.setText(ANONYMOUS_CREDENTIALS.getPassword()); } else { usernameField.setEnabled(true); usernameField.setText(lastUsername); passwordField.setEnabled(true); passwordField.setText(lastPassword); } } } public void encodingChanged(Object source, String oldEncoding, String newEncoding) { lastEncoding = newEncoding; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/HDFSPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.hadoop.HDFSFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.net.MalformedURLException; import java.text.ParseException; /** * This ServerPanel helps initiate HDFS connections. * * @author Maxence Bernard */ public class HDFSPanel extends ServerPanel { private final JTextField serverField; private final JTextField usernameField; // private JTextField groupField; private final JTextField initialDirField; private final JSpinner portSpinner; private static String lastServer = ""; private static String lastUsername; // private static String lastGroup = HDFSFile.getDefaultGroup(); private static String lastInitialDir = "/"; private static int lastPort = FileURL.getRegisteredHandler(FileProtocols.HDFS).getStandardPort(); static { try { lastUsername = HDFSFile.getDefaultUsername(); } catch (Throwable t) { t.printStackTrace(); } } HDFSPanel(ServerConnectDialog dialog, final MainFrame mainFrame) { super(dialog, mainFrame); // Server field, initialized to last server entered serverField = new JTextField(lastServer); serverField.selectAll(); addTextFieldListeners(serverField, true); addRow(Translator.get("server_connect_dialog.server"), serverField, 15); // Username field, initialized to last username usernameField = new JTextField(lastUsername); usernameField.selectAll(); addTextFieldListeners(usernameField, false); addRow(Translator.get("server_connect_dialog.username"), usernameField, 15); // // Password field, initialized to "" // groupField = new JTextField(lastGroup); // groupField.selectAll(); // addTextFieldListeners(groupField, false); // addRow(Translator.get("server_connect_dialog.group"), groupField, 15); // Initial directory field, initialized to "/" initialDirField = new JTextField(lastInitialDir); initialDirField.selectAll(); addTextFieldListeners(initialDirField, true); addRow(Translator.get("server_connect_dialog.initial_dir"), initialDirField, 5); // Port field, initialized to last port portSpinner = createPortSpinner(lastPort); addRow(Translator.get("server_connect_dialog.port"), portSpinner, 15); } private void updateValues() { lastServer = serverField.getText(); lastUsername = usernameField.getText(); // lastGroup = groupField.getText(); lastInitialDir = initialDirField.getText(); lastPort = (Integer) portSpinner.getValue(); } //////////////////////////////// // ServerPanel implementation // //////////////////////////////// @Override FileURL getServerURL() throws MalformedURLException { updateValues(); if(!lastInitialDir.startsWith("/")) lastInitialDir = "/"+lastInitialDir; FileURL url = FileURL.getFileURL(FileProtocols.HDFS+"://"+lastServer+lastInitialDir); // Set user and group url.setCredentials(new Credentials(lastUsername, "")); // url.setProperty(HDFSFile.GROUP_PROPERTY_NAME, lastGroup); // Set port url.setPort(lastPort); return url; } @Override boolean usesCredentials() { return true; } @Override public void dialogValidated() { // Commits the current spinner value in case it was being edited and 'enter' was pressed // (the spinner value would otherwise not be committed) try { portSpinner.commitEdit(); } catch(ParseException ignored) { } updateValues(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/HTTPPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.net.MalformedURLException; /** * This ServerPanel helps initiate HTTP connections. * * @author Maxence Bernard */ public class HTTPPanel extends ServerPanel { private final JTextField urlField; private final JTextField usernameField; private final JPasswordField passwordField; private static String lastURL = "http://"; private static String lastUsername = ""; // Not static so that it is not saved (for security reasons) private String lastPassword = ""; HTTPPanel(ServerConnectDialog dialog, MainFrame mainFrame) { super(dialog, mainFrame); // Webserver (URL) field urlField = new JTextField(lastURL); urlField.selectAll(); addTextFieldListeners(urlField, true); addRow(Translator.get("server_connect_dialog.http_url"), urlField, 20); // HTTP Basic authentication fields addRow(new JLabel(Translator.get("http_connect.basic_authentication")), 10); // Username field usernameField = new JTextField(lastUsername); usernameField.selectAll(); addTextFieldListeners(usernameField, false); addRow(Translator.get("server_connect_dialog.username"), usernameField, 5); // Password field passwordField = new JPasswordField(lastPassword); addTextFieldListeners(passwordField, false); addRow(Translator.get("password"), passwordField, 0); } private void updateValues() { lastURL = urlField.getText(); lastUsername = usernameField.getText(); lastPassword = new String(passwordField.getPassword()); } @Override FileURL getServerURL() throws MalformedURLException { updateValues(); if (!(lastURL.toLowerCase().startsWith(FileProtocols.HTTP+"://") || lastURL.toLowerCase().startsWith(FileProtocols.HTTPS+"://"))) { lastURL = FileProtocols.HTTP + "://" + lastURL; } FileURL fileURL = FileURL.getFileURL(lastURL); fileURL.setCredentials(new Credentials(lastUsername, lastPassword)); return fileURL; } @Override boolean usesCredentials() { return true; } @Override public void dialogValidated() { updateValues(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/NFSPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.nfs.NFSFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.net.MalformedURLException; import java.text.ParseException; /** * This ServerPanel helps initiate NFS connections. * * @author Maxence Bernard */ public class NFSPanel extends ServerPanel { private final static int STANDARD_PORT = FileURL.getRegisteredHandler(FileProtocols.NFS).getStandardPort(); private final JTextField serverField; private final JTextField shareField; private final JSpinner portSpinner; private final JComboBox nfsVersionComboBox; private final JComboBox nfsProtocolComboBox; private static String lastServer = ""; private static String lastShare = ""; private static int lastPort = STANDARD_PORT; private static String lastNfsVersion = NFSFile.DEFAULT_NFS_VERSION; private static String lastNfsProtocol = NFSFile.DEFAULT_NFS_PROTOCOL; NFSPanel(ServerConnectDialog dialog, MainFrame mainFrame) { super(dialog, mainFrame); // Server field, initialized to last value serverField = new JTextField(lastServer); serverField.selectAll(); addTextFieldListeners(serverField, true); addRow(Translator.get("server_connect_dialog.server"), serverField, 5); // NFS share, initialized to "" shareField = new JTextField(lastShare); shareField.selectAll(); addTextFieldListeners(shareField, true); addRow(Translator.get("server_connect_dialog.share"), shareField, 15); // Port field, initialized to last value (default is 2049) portSpinner = createPortSpinner(lastPort); addRow(Translator.get("server_connect_dialog.port"), portSpinner, 15); // NFS version, initialized to last value (default is NFSFile's default) nfsVersionComboBox = new JComboBox<>(); nfsVersionComboBox.addItem(NFSFile.NFS_VERSION_2); nfsVersionComboBox.addItem(NFSFile.NFS_VERSION_3); nfsVersionComboBox.setSelectedItem(lastNfsVersion); addRow(Translator.get("server_connect_dialog.nfs_version"), nfsVersionComboBox, 5); // NFS protocol, initialized to last value (default is NFSFile's default) nfsProtocolComboBox = new JComboBox<>(); nfsProtocolComboBox.addItem(NFSFile.NFS_PROTOCOL_AUTO); nfsProtocolComboBox.addItem(NFSFile.NFS_PROTOCOL_TCP); nfsProtocolComboBox.addItem(NFSFile.NFS_PROTOCOL_UDP); nfsProtocolComboBox.setSelectedItem(lastNfsProtocol); addRow(Translator.get("server_connect_dialog.protocol"), nfsProtocolComboBox, 15); } private void updateValues() { lastServer = serverField.getText(); lastShare = shareField.getText(); lastPort = (Integer) portSpinner.getValue(); lastNfsVersion = (String)nfsVersionComboBox.getSelectedItem(); lastNfsProtocol = (String)nfsProtocolComboBox.getSelectedItem(); } //////////////////////////////// // ServerPanel implementation // //////////////////////////////// @Override FileURL getServerURL() throws MalformedURLException { updateValues(); FileURL url = FileURL.getFileURL(FileProtocols.NFS+"://"+lastServer+(lastShare.startsWith("/")?"":"/")+lastShare); // Set port url.setPort(lastPort); // Set NFS version url.setProperty(NFSFile.NFS_VERSION_PROPERTY_NAME, lastNfsVersion); // Set NFS protocol url.setProperty(NFSFile.NFS_PROTOCOL_PROPERTY_NAME, lastNfsProtocol); return url; } @Override boolean usesCredentials() { return false; } @Override public void dialogValidated() { // Commits the current spinner value in case it was being edited and 'enter' was pressed // (the spinner value would otherwise not be committed) try { portSpinner.commitEdit(); } catch(ParseException ignored) { } updateValues(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/S3Panel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.net.MalformedURLException; import java.text.ParseException; /** * This ServerPanel helps initiate S3 connections. * * @author Maxence Bernard */ public class S3Panel extends ServerPanel { private final JTextField serverField; private final JTextField usernameField; private final JPasswordField passwordField; private final JTextField initialDirField; private final JSpinner portSpinner; private static String lastServer = "s3.amazonaws.com"; private static String lastUsername = ""; // Not static so that it is not saved (for security reasons) private String lastPassword = ""; private static String lastInitialDir = "/"; private static int lastPort = FileURL.getRegisteredHandler(FileProtocols.S3).getStandardPort(); S3Panel(ServerConnectDialog dialog, final MainFrame mainFrame) { super(dialog, mainFrame); // Server field, initialized to last server entered (s3.amazonaws.com by default) serverField = new JTextField(lastServer); serverField.selectAll(); addTextFieldListeners(serverField, true); addRow(Translator.get("server_connect_dialog.server"), serverField, 15); // Username field, initialized to last username usernameField = new JTextField(lastUsername); usernameField.selectAll(); addTextFieldListeners(usernameField, false); // Not localized on purpose addRow("Access ID Key", usernameField, 5); // Password field, initialized to "" passwordField = new JPasswordField(); addTextFieldListeners(passwordField, false); // Not localized on purpose addRow("Secret Access Key", passwordField, 15); // Initial directory field, initialized to "/" initialDirField = new JTextField(lastInitialDir); initialDirField.selectAll(); addTextFieldListeners(initialDirField, true); addRow(Translator.get("server_connect_dialog.initial_dir"), initialDirField, 5); // Port field, initialized to last port portSpinner = createPortSpinner(lastPort); addRow(Translator.get("server_connect_dialog.port"), portSpinner, 15); } private void updateValues() { lastServer = serverField.getText(); lastUsername = usernameField.getText(); lastPassword = new String(passwordField.getPassword()); lastInitialDir = initialDirField.getText(); lastPort = (Integer) portSpinner.getValue(); } //////////////////////////////// // ServerPanel implementation // //////////////////////////////// @Override FileURL getServerURL() throws MalformedURLException { updateValues(); if(!lastInitialDir.startsWith("/")) lastInitialDir = "/"+lastInitialDir; FileURL url = FileURL.getFileURL(FileProtocols.S3+"://"+lastServer+lastInitialDir); url.setCredentials(new Credentials(lastUsername, lastPassword)); url.setPort(lastPort); return url; } @Override boolean usesCredentials() { return true; } @Override public void dialogValidated() { // Commits the current spinner value in case it was being edited and 'enter' was pressed // (the spinner value would otherwise not be committed) try { portSpinner.commitEdit(); } catch(ParseException ignored) { } updateValues(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/SFTPPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.net.MalformedURLException; import java.nio.file.FileSystems; import java.text.ParseException; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JPanel; import javax.swing.JPasswordField; import javax.swing.JSpinner; import javax.swing.JTextField; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.sftp.SFTPFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; /** * This ServerPanel helps initiate SFTP connections. * * @author Maxence Bernard, Vassil Dichev */ public class SFTPPanel extends ServerPanel { private final static int STANDARD_PORT = FileURL.getRegisteredHandler(FileProtocols.SFTP).getStandardPort(); private final JTextField serverField; private final JTextField privateKeyPathField; private final JTextField usernameField; private final JPasswordField passwordField; private final JTextField initialDirField; private final JSpinner portSpinner; private static String lastServer = ""; private static String lastKeyPath = ""; private static String lastUsername = ""; // Not static so that it is not saved (for security reasons) private String lastPassword = ""; private static String lastInitialDir = "/"; private static int lastPort = STANDARD_PORT; SFTPPanel(ServerConnectDialog dialog, final MainFrame mainFrame) { super(dialog, mainFrame); // Server field, initialized to last server entered serverField = new JTextField(lastServer); serverField.selectAll(); addTextFieldListeners(serverField, true); addRow(Translator.get("server_connect_dialog.server"), serverField, 15); // Username field, initialized to last username usernameField = new JTextField(lastUsername); usernameField.selectAll(); addTextFieldListeners(usernameField, false); addRow(Translator.get("server_connect_dialog.username"), usernameField, 5); // Password field, initialized to "" passwordField = new JPasswordField(); addTextFieldListeners(passwordField, false); addRow(Translator.get("password")+"/"+Translator.get("server_connect_dialog.passphrase"), passwordField, 15); // Key file field, initialized to last file JPanel privateKeyChooser = new JPanel(new BorderLayout()); if (OsFamily.getCurrent().isUnixBased() && (lastKeyPath == null || lastKeyPath.trim().isEmpty())) { lastKeyPath = System.getProperty("user.home") + "/.ssh/id_rsa"; } privateKeyPathField = new JTextField(lastKeyPath); privateKeyPathField.selectAll(); addTextFieldListeners(privateKeyPathField, false); privateKeyChooser.add(privateKeyPathField, BorderLayout.CENTER); JButton chooseFileButton = new JButton("..."); // Mac OS X: small component size if (OsFamily.MAC_OS_X.isCurrent()) { chooseFileButton.putClientProperty("JComponent.sizeVariant", "small"); } chooseFileButton.addActionListener(new ActionListener() { final JFileChooser fc = new JFileChooser(System.getProperty("user.home") + FileSystems.getDefault().getSeparator() + ".ssh"); public void actionPerformed(ActionEvent e) { int returnVal = fc.showOpenDialog(mainFrame.getJFrame()); if (returnVal == JFileChooser.APPROVE_OPTION) { privateKeyPathField.setText(fc.getSelectedFile().getAbsolutePath()); } } } ); privateKeyChooser.add(chooseFileButton, BorderLayout.EAST); addRow(Translator.get("server_connect_dialog.private_key"), privateKeyChooser, 15); // Initial directory field, initialized to "/" initialDirField = new JTextField(lastInitialDir); initialDirField.selectAll(); addTextFieldListeners(initialDirField, true); addRow(Translator.get("server_connect_dialog.initial_dir"), initialDirField, 5); // Port field, initialized to last port (default is 22) portSpinner = createPortSpinner(lastPort); addRow(Translator.get("server_connect_dialog.port"), portSpinner, 15); } private void updateValues() { lastKeyPath = privateKeyPathField.getText().trim(); lastServer = serverField.getText().trim(); lastUsername = usernameField.getText(); lastPassword = new String(passwordField.getPassword()); lastInitialDir = initialDirField.getText().trim(); lastPort = (Integer) portSpinner.getValue(); } //////////////////////////////// // ServerPanel implementation // //////////////////////////////// @Override FileURL getServerURL() throws MalformedURLException { updateValues(); if (!lastInitialDir.startsWith("/")) { lastInitialDir = "/" + lastInitialDir; } FileURL url = FileURL.getFileURL(FileProtocols.SFTP+"://"+lastServer+lastInitialDir); // Set credentials url.setCredentials(new Credentials(lastUsername, lastPassword)); if (!lastKeyPath.isEmpty()) { url.setProperty(SFTPFile.PRIVATE_KEY_PATH_PROPERTY_NAME, lastKeyPath); } // Set port url.setPort(lastPort); return url; } @Override boolean usesCredentials() { return true; } @Override public void dialogValidated() { // Commits the current spinner value in case it was being edited and 'enter' was pressed // (the spinner value would otherwise not be committed) try { portSpinner.commitEdit(); } catch(ParseException ignore) { } updateValues(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/SMBPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.net.MalformedURLException; /** * This ServerPanel helps initiate SMB connections. * * @author Maxence Bernard */ public class SMBPanel extends ServerPanel { private final JTextField domainField; private final JTextField serverField; private final JTextField shareField; private final JTextField usernameField; private final JPasswordField passwordField; private static String lastDomain = ""; private static String lastServer = ""; private static String lastShare = ""; private static String lastUsername = ""; // Not static so that it is not saved (for security reasons) private String lastPassword = ""; SMBPanel(ServerConnectDialog dialog, MainFrame mainFrame) { super(dialog, mainFrame); // Server field serverField = new JTextField(lastServer); serverField.selectAll(); addTextFieldListeners(serverField, true); addRow(Translator.get("server_connect_dialog.server"), serverField, 5); // Share field shareField = new JTextField(lastShare); shareField.selectAll(); addTextFieldListeners(shareField, true); addRow(Translator.get("server_connect_dialog.share"), shareField, 15); // Domain field domainField = new JTextField(lastDomain); domainField.selectAll(); addTextFieldListeners(domainField, true); addRow(Translator.get("server_connect_dialog.domain"), domainField, 15); // Username field usernameField = new JTextField(lastUsername); usernameField.selectAll(); addTextFieldListeners(usernameField, false); addRow(Translator.get("server_connect_dialog.username"), usernameField, 5); // Password field passwordField = new JPasswordField(); addTextFieldListeners(passwordField, false); addRow(Translator.get("password"), passwordField, 0); } private void updateValues() { lastServer = serverField.getText(); lastShare = shareField.getText(); lastDomain = domainField.getText(); lastUsername = usernameField.getText(); lastPassword = new String(passwordField.getPassword()); } @Override FileURL getServerURL() throws MalformedURLException { updateValues(); FileURL url = FileURL.getFileURL(FileProtocols.SMB+"://"+lastServer+(lastShare.startsWith("/")?"":"/")+lastShare); // Insert the domain (if any) before the username, separated by a semicolon String userInfo = lastUsername; if (!lastDomain.isEmpty()) { userInfo = lastDomain+";"+userInfo; } url.setCredentials(new Credentials(userInfo, lastPassword)); return url; } @Override boolean usesCredentials() { return true; } @Override public void dialogValidated() { updateValues(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/ServerConnectDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.auth.CredentialsMapping; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ConnectToServerAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.helper.FocusRequester; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; /** * Dialog that assists the user in connecting to a filesystem. It contains tabs and associated panels for each of the * supported protocols. * * @author Maxence Bernard */ public class ServerConnectDialog extends FocusDialog implements ActionListener, ChangeListener { private final FolderPanel folderPanel; private final JButton btnCancel; private ServerPanel pnlCurrentServer; private final JTabbedPane tabbedPane; private final List serverPanels = new ArrayList<>(); private final JLabel lblUrl; private final JCheckBox cbSaveCredentials; // Dialog's width has to be at least 320 private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(520, 0); private static Class lastPanelClass = FTPPanel.class; /** * Creates a new ServerConnectDialog that changes the current folder on the specified {@link FolderPanel}. * * @param folderPanel the panel on which to change the current folder */ public ServerConnectDialog(FolderPanel folderPanel) { this(folderPanel, lastPanelClass); } /** * Creates a new ServerConnectDialog that changes the current folder on the specified {@link FolderPanel}. * The specified panel is selected when the dialog appears. * * @param folderPanel the panel on which to change the current folder * @param selectPanelClass class of the ServerPanel to select */ public ServerConnectDialog(FolderPanel folderPanel, Class selectPanelClass) { super(folderPanel.getMainFrame().getJFrame(), ActionProperties.getActionLabel(ConnectToServerAction.Descriptor.ACTION_ID), folderPanel.getMainFrame().getJFrame()); this.folderPanel = folderPanel; lastPanelClass = selectPanelClass; MainFrame mainFrame = folderPanel.getMainFrame(); Container contentPane = getContentPane(); this.tabbedPane = new JTabbedPane(JTabbedPane.TOP); addTab(FileProtocols.FTP, new FTPPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.HDFS, new HDFSPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.HTTP, new HTTPPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.NFS, new NFSPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.S3, new S3Panel(this, mainFrame), selectPanelClass); addTab(FileProtocols.SFTP, new SFTPPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.SMB, new SMBPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.WEBDAV, new WebDAVPanel(this, mainFrame), selectPanelClass); addTab(FileProtocols.VSPHERE, new VSpherePanel(this, mainFrame), selectPanelClass); pnlCurrentServer = getCurrentServerPanel(); // Listen to tab change events tabbedPane.addChangeListener(this); contentPane.add(tabbedPane, BorderLayout.CENTER); YBoxPanel yPanel = new YBoxPanel(); XBoxPanel xPanel = new XBoxPanel(); xPanel.add(new JLabel(i18n("server_connect_dialog.server_url")+":")); xPanel.addSpace(5); lblUrl = new JLabel(""); updateURLLabel(); xPanel.add(lblUrl); yPanel.add(xPanel); yPanel.addSpace(10); this.cbSaveCredentials = new JCheckBox(i18n("auth_dialog.store_credentials"), false); // Enables 'save credentials' checkbox only if server panel/protocol uses credentials cbSaveCredentials.setEnabled(pnlCurrentServer.usesCredentials()); yPanel.add(cbSaveCredentials); JButton okButton = new JButton(i18n("server_connect_dialog.connect")); btnCancel = new JButton(i18n("cancel")); yPanel.add(DialogToolkit.createOKCancelPanel(okButton, btnCancel, getRootPane(), this)); contentPane.add(yPanel, BorderLayout.SOUTH); // initial focus setInitialFocusComponent(pnlCurrentServer); setMinimumSize(MINIMUM_DIALOG_DIMENSION); } public void addTab(String protocol, ServerPanel serverPanel, Class selectPanelClass) { if (!FileFactory.isRegisteredProtocol(protocol)) { return; } JPanel northPanel = new JPanel(new BorderLayout()); northPanel.add(serverPanel, BorderLayout.NORTH); tabbedPane.addTab(protocol.toUpperCase(), northPanel); if (selectPanelClass.equals(serverPanel.getClass())) { tabbedPane.setSelectedComponent(northPanel); } serverPanels.add(serverPanel); } void updateURLLabel() { try { FileURL url = pnlCurrentServer.getServerURL(); lblUrl.setText(url == null ? " " : url.toString(false)); } catch(MalformedURLException ex) { lblUrl.setText(" "); } } private ServerPanel getCurrentServerPanel() { return serverPanels.get(tabbedPane.getSelectedIndex()); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnCancel) { dispose(); return; } try { pnlCurrentServer.dialogValidated(); FileURL serverURL = pnlCurrentServer.getServerURL(); // Can throw a MalformedURLException // create a CredentialsMapping instance and pass to Folder so that it uses it to connect to the folder and // adds to CredentialsManager once the folder has been successfully changed Credentials credentials = serverURL.getCredentials(); CredentialsMapping credentialsMapping; if (credentials != null) { credentialsMapping = new CredentialsMapping(credentials, serverURL, cbSaveCredentials.isSelected()); } else { credentialsMapping = null; } dispose(); // Change the current folder folderPanel.tryChangeCurrentFolder(serverURL, credentialsMapping); } catch(IOException ex) { InformationDialog.showErrorDialog(this, i18n("table.folder_access_error_title"), i18n("folder_does_not_exist")); } } public void stateChanged(ChangeEvent e) { pnlCurrentServer = getCurrentServerPanel(); lastPanelClass = pnlCurrentServer.getClass(); // Enables 'save credentials' checkbox only if server panel/protocol uses credentials cbSaveCredentials.setEnabled(pnlCurrentServer.usesCredentials()); updateURLLabel(); FocusRequester.requestFocus(pnlCurrentServer); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/ServerPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.layout.XAlignedComponentPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.*; import java.net.MalformedURLException; /** * This abstract class represents a panel that helps the user initiate a connection to a servers using a certain file * protocol. This class is agnostic with respect to the file protocol used -- subclasses implement a specific file * protocol. * * @author Maxence Bernard */ public abstract class ServerPanel extends XAlignedComponentPanel { protected ServerConnectDialog dialog; protected MainFrame mainFrame; protected ServerPanel(ServerConnectDialog dialog, MainFrame mainFrame) { // Add a 10-pixel gap label and text component super(10); this.dialog = dialog; this.mainFrame = mainFrame; } @Override public Insets getInsets() { return new Insets(8, 6, 8, 6); } protected JSpinner createPortSpinner(int portValue) { return createIntSpinner(portValue, 1, 65535, 1); } protected JSpinner createIntSpinner(int value, int minValue, int maxValue, int step) { JSpinner spinner = new JSpinner(new SpinnerNumberModel(value, minValue, maxValue, step)); // Left-aligns the text within the text field, and use a simple decimal format with no thousand separator JSpinner.NumberEditor editor = new JSpinner.NumberEditor(spinner, "#####"); editor.getTextField().setHorizontalAlignment(JTextField.LEADING); spinner.setEditor(editor); // Any changes made to the spinner will update the URL label spinner.addChangeListener(e -> dialog.updateURLLabel()); return spinner; } protected void addTextFieldListeners(JTextField textField, boolean updateLabel) { textField.addActionListener(dialog); if (updateLabel) { textField.getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { dialog.updateURLLabel(); } public void insertUpdate(DocumentEvent e) { dialog.updateURLLabel(); } public void removeUpdate(DocumentEvent e) { dialog.updateURLLabel(); } }); } } /** * Returns the current server URL represented by this panel, null if it is not available. * This method may be called at any time by {@link ServerConnectDialog}. * * @return the current server URL represented by this panel, null if it is not available * @throws MalformedURLException if an exception was thrown while creating the FileURL instance */ abstract FileURL getServerURL() throws MalformedURLException; /** * Returns true if this panel allows the user to specify credentials for the file protocol. * * @return true if this panel allows the user to specify credentials for the file protocol */ abstract boolean usesCredentials(); /** * This method is called by {@link ServerConnectDialog} when the dialog has been validated by the user * ('OK' button or 'Enter' key pressed). This is where component values should be saved for when a * new instance of the panel is created. */ abstract void dialogValidated(); } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/server/ShowServerConnectionsDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.server; import com.mucommander.commons.file.Credentials; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.connection.ConnectionHandler; import com.mucommander.commons.file.connection.ConnectionPool; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowServerConnectionsAction; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.List; /** * This dialog shows a list of server connections and allows the user to close/disconnect them. * * @author Maxence Bernard */ public class ShowServerConnectionsDialog extends FocusDialog implements ActionListener { private final MainFrame mainFrame; private final JList connectionList; private final List connections; private final JButton btnDisconnect; private final JButton btnGoto; private final JButton btnClose; // Dialog's size has to be at least 400x300 private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(400,300); // Dialog's size has to be at most 600x400 private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(600,400); public ShowServerConnectionsDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(ShowServerConnectionsAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); this.mainFrame = mainFrame; Container contentPane = getContentPane(); // Add the list of server connections connections = ConnectionPool.getConnectionHandlersSnapshot(); connectionList = new JList<>(new AbstractListModel<>() { public int getSize() { return connections.size(); } public String getElementAt(int i) { ConnectionHandler connHandler = connections.get(i); // Show login (but not password) in the URL // Note: realm returned by ConnectionHandler does not contain credentials FileURL clonedRealm = (FileURL) connHandler.getRealm().clone(); Credentials loginCredentials = new Credentials(connHandler.getCredentials().getLogin(), ""); clonedRealm.setCredentials(loginCredentials); return clonedRealm.toString(true) + " (" + i18n(connHandler.isLocked() ? "server_connections_dialog.connection_busy" : "server_connections_dialog.connection_idle") + ")"; } }); // Only one list index can be selected at a time connectionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Select the first connection in the list boolean hasConnections = !connections.isEmpty(); if (hasConnections) { connectionList.setSelectedIndex(0); } contentPane.add( new JScrollPane(connectionList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED), BorderLayout.CENTER); // Add buttons XBoxPanel buttonsPanel = new XBoxPanel(); JPanel buttonGroupPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // Disconnect button btnDisconnect = new JButton(i18n("server_connections_dialog.disconnect")); btnDisconnect.setMnemonic(mnemonicHelper.getMnemonic(btnDisconnect)); btnDisconnect.setEnabled(hasConnections); if (hasConnections) { btnDisconnect.addActionListener(this); } buttonGroupPanel.add(btnDisconnect); // Go to button btnGoto = new JButton(i18n("go_to")); btnGoto.setMnemonic(mnemonicHelper.getMnemonic(btnGoto)); btnGoto.setEnabled(hasConnections); if (hasConnections) { btnGoto.addActionListener(this); } buttonGroupPanel.add(btnGoto); buttonsPanel.add(buttonGroupPanel); // Button that closes the window btnClose = new JButton(i18n("close")); btnClose.setMnemonic(mnemonicHelper.getMnemonic(btnClose)); btnClose.addActionListener(this); buttonsPanel.add(Box.createHorizontalGlue()); buttonsPanel.add(btnClose); contentPane.add(buttonsPanel, BorderLayout.SOUTH); // Connections list will receive initial focus setInitialFocusComponent(connectionList); // Selects 'Done' button when enter is pressed getRootPane().setDefaultButton(btnClose); // Packs dialog setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); setDefaultCloseOperation(DISPOSE_ON_CLOSE); showDialog(); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // Disconnects the selected connection if (source == btnDisconnect) { int selectedIndex = connectionList.getSelectedIndex(); if (selectedIndex >= 0 && selectedIndex < connections.size()) { final ConnectionHandler connHandler = connections.get(selectedIndex); // Close connection in a separate thread as I/O can lock. // Todo: Add a confirmation dialog if the connection is active as it will stop whatever the connection is currently doing new Thread(connHandler::closeConnection).start(); // Remove connection from the list connections.remove(selectedIndex); connectionList.setSelectedIndex(Math.min(selectedIndex, connections.size())); connectionList.repaint(); // Disable contextual buttons if there are no more connections if (connections.isEmpty()) { btnDisconnect.setEnabled(false); btnGoto.setEnabled(false); } } } else if (source == btnGoto) { // Goes to the selected connection // Dispose the dialog first dispose(); int selectedIndex = connectionList.getSelectedIndex(); if (selectedIndex>=0 && selectedIndex Components used to let users enter remote file systems information before connecting. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/shell/RunDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.shell; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.PrintStream; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import com.mucommander.commons.file.FileURL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.process.AbstractProcess; import com.mucommander.process.ProcessListener; import com.mucommander.shell.Shell; import com.mucommander.shell.ShellHistoryManager; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.RunCommandAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; /** * Dialog used to execute a user-defined command. *

    * Creates and displays a new dialog allowing the user to input a command which will be executed once the action is confirmed. * The command output of the user command is displayed in a text area *

    * Note that even though this component is affected by themes, it's impossible to edit the current theme while it's being displayed. * For this reason, the RunDialog doesn't listen to theme modifications. * * @author Maxence Bernard, Nicolas Rinaudo */ public class RunDialog extends FocusDialog implements ActionListener, ProcessListener, KeyListener { private static final Logger LOGGER = LoggerFactory.getLogger(RunDialog.class); /** Main frame this dialog depends on. */ private final MainFrame mainFrame; /** Editable combo box used for shell input and history. */ private ShellComboBox inputCombo; /** Run/stop button. */ private JButton btnRunStop; /** Cancel button. */ private JButton btnCancel; /** Clear shell history button. */ private JButton btnClear; /** Text area used to display the shell output. */ private JTextArea outputTextArea; /** Used to let the user known that the command is still running. */ private SpinningDial dial; /** Stream used to send characters to the process' stdin process. */ private PrintStream processInput; /** Process currently running, null if none. */ private AbstractProcess currentProcess; /** Minimum dimensions for the dialog. */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(600, 400); /** * Creates the dialog's shell output area. * @return a scroll pane containing the dialog's shell output area. */ private JScrollPane createOutputArea() { // Creates and initializes the output area. outputTextArea = new JTextArea(); outputTextArea.setLineWrap(true); outputTextArea.setCaretPosition(0); outputTextArea.setRows(10); outputTextArea.setEditable(false); outputTextArea.addKeyListener(this); // Applies the current theme to the shell output area. outputTextArea.setForeground(ThemeManager.getCurrentColor(Theme.SHELL_FOREGROUND_COLOR)); outputTextArea.setCaretColor(ThemeManager.getCurrentColor(Theme.SHELL_FOREGROUND_COLOR)); outputTextArea.setBackground(ThemeManager.getCurrentColor(Theme.SHELL_BACKGROUND_COLOR)); outputTextArea.setSelectedTextColor(ThemeManager.getCurrentColor(Theme.SHELL_SELECTED_FOREGROUND_COLOR)); outputTextArea.setSelectionColor(ThemeManager.getCurrentColor(Theme.SHELL_SELECTED_BACKGROUND_COLOR)); outputTextArea.setFont(ThemeManager.getCurrentFont(Theme.SHELL_FONT)); // Creates a scroll pane on the shell output area. return new JScrollPane(outputTextArea, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); } /** * Creates the shell input part of the dialog. * @return the shell input part of the dialog. */ private YBoxPanel createInputArea() { YBoxPanel mainPanel = new YBoxPanel(); // Adds a textual description: // - if we're working in a local directory, 'init in current folder'. // - if we're working on a non-standard FS, 'init in home folder'. FileURL fileUrl = mainFrame.getActivePanel().getCurrentFolder().getURL(); boolean isFile = fileUrl.getScheme().equals(FileProtocols.FILE); String label = i18n(isFile ? "run_dialog.run_command_description" : "run_dialog.run_in_home_description") + ":"; mainPanel.add(new JLabel(label)); // Adds the shell input combo box. mainPanel.add(inputCombo = new ShellComboBox(this)); inputCombo.setEnabled(true); // Adds a textual description of the shell output area. mainPanel.addSpace(10); JPanel labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); labelPanel.add(new JLabel(i18n("run_dialog.command_output")+":")); labelPanel.add(new JLabel(dial = new SpinningDial())); mainPanel.add(labelPanel); return mainPanel; } /** * Creates a panel containing the dialog's buttons. * @return a panel containing the dialog's buttons. */ private XBoxPanel createButtonsArea() { XBoxPanel buttonsPanel = new XBoxPanel(); // 'Clear history' button. buttonsPanel.add(btnClear = new JButton(i18n("run_dialog.clear_history"))); btnClear.addActionListener(this); // Separator. buttonsPanel.add(Box.createHorizontalGlue()); // 'Run / stop' and 'Cancel' buttons. buttonsPanel.add(DialogToolkit.createOKCancelPanel( btnRunStop = new JButton(i18n("run_dialog.run")), btnCancel = new JButton(i18n("cancel")), getRootPane(), this)); return buttonsPanel; } /** * Creates and displays a new RunDialog. * @param mainFrame the main frame this dialog is attached to. */ public RunDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), ActionProperties.getActionLabel(RunCommandAction.Descriptor.ACTION_ID), mainFrame.getJFrame()); this.mainFrame = mainFrame; // Initializes the dialog's UI. Container contentPane = getContentPane(); contentPane.add(createInputArea(), BorderLayout.NORTH); contentPane.add(createOutputArea(), BorderLayout.CENTER); contentPane.add(createButtonsArea(), BorderLayout.SOUTH); // Sets default items. setInitialFocusComponent(inputCombo); getRootPane().setDefaultButton(btnRunStop); // Makes sure that any running process will be killed when the dialog is closed. addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { if (currentProcess != null) { processInput.close(); currentProcess.destroy(); } } }); // Sets the dialog's minimum size. setMinimumSize(MINIMUM_DIALOG_DIMENSION); } /** * Notifies the RunDialog that the current process has died. * @param retValue process' return code (not used). */ public void processDied(int retValue) { LOGGER.debug("process exit, return value = {}", retValue); currentProcess = null; if (processInput != null) { processInput.close(); processInput = null; } switchToRunState(); } /** * Ignored. */ public void processOutput(byte[] buffer, int offset, int length) {} /** * Notifies the RunDialog that the process has output some text. * @param output contains the process' output. */ public void processOutput(String output) { addToTextArea(output); } /** * Notifies the RunDialog that a key has been pressed. *

    * This method will ignore all events while a process is not running. If a process is running: *

      *
    • VK_ESCAPE events are skipped and left to the Cancel button to handle.
    • *
    • Printable characters are passed to the process and consumed.
    • *
    • All other events are consumed.
    • *
    * *

    * At the time of writing, tab characters do not seem to be caught. * * @param event describes the key event. */ public void keyPressed(KeyEvent event) { // Only handle keyPressed events when a process is running. if (currentProcess == null) { return; } // Ignores VK_ESCAPE events, as their behavior is a bit strange: they register // as a printable character, and reacting to their being typed apparently consumes // the event - preventing the dialog from being closed. if (event.getKeyCode() != KeyEvent.VK_ESCAPE) { char character = event.getKeyChar(); // Only printable key typed are passed to the shell. if (character != KeyEvent.CHAR_UNDEFINED) { processInput.print(character); addToTextArea(String.valueOf(character)); } event.consume(); } } /** * Not used. */ public void keyTyped(KeyEvent event) {} /** * Not used. */ public void keyReleased(KeyEvent event) {} private void clearHistory() { ShellHistoryManager.clear(); // Sets the new focus depending on whether a process is currently running or not. if (currentProcess == null) { inputCombo.requestFocus(); outputTextArea.setText(""); } else { outputTextArea.requestFocus(); outputTextArea.getCaret().setVisible(true); } } private void stopRunning() { processInput.close(); currentProcess.destroy(); this.currentProcess = null; switchToRunState(); } /** * Notifies the RunDialog that an action has been performed. * @param e describes the action that occurred. */ public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == btnClear) { // 'Clear shell history' has been pressed, clear shell history. clearHistory(); } else if (source == btnRunStop) { // 'Run / stop' button has been pressed. // If we're not running a process, start a new one. if (currentProcess == null) { runCommand(inputCombo.getCommand()); } else { // If we're running a process, kill it. stopRunning(); } } else if (source == btnCancel) { // Cancel button disposes the dialog and kills the process if (currentProcess != null) { currentProcess.destroy(); } dispose(); } } /** * Switches the UI back to 'Run command' state. */ private void switchToRunState() { // Stops the spinning dial. dial.setAnimated(false); // Change 'Stop' button to 'Run' this.btnRunStop.setText(i18n("run_dialog.run")); // Make command field active again this.inputCombo.setEnabled(true); inputCombo.requestFocus(); // Disables the caret in the process output area. outputTextArea.getCaret().setVisible(false); // Repaint this dialog repaint(); } /** * Runs the specified command. * @param command command to init. */ void runCommand(String command) { try { // Starts the spinning dial. dial.setAnimated(true); // Change 'Run' button to 'Stop' this.btnRunStop.setText(i18n("run_dialog.stop")); // Resets the process output area. outputTextArea.setText(""); outputTextArea.setCaretPosition(0); outputTextArea.getCaret().setVisible(true); outputTextArea.requestFocus(); // No new command can be entered while a process is running. inputCombo.setEnabled(false); currentProcess = Shell.execute(command, mainFrame.getActivePanel().getCurrentFolder(), this); processInput = new PrintStream(currentProcess.getOutputStream(), true); // Repaints the dialog. repaint(); } catch(Exception e) { // Notifies the user that an error occurred and resets to normal state. addToTextArea(i18n("generic_error")); switchToRunState(); } } /** * Appends the specified string to the shell output area. * @param s string to append to the shell output area. */ private void addToTextArea(String s) { outputTextArea.append(s); outputTextArea.setCaretPosition(outputTextArea.getText().length()); outputTextArea.getCaret().setVisible(true); outputTextArea.repaint(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/shell/ShellComboBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.shell; import com.mucommander.shell.ShellHistoryListener; import com.mucommander.shell.ShellHistoryManager; import com.mucommander.ui.autocomplete.CompleterFactory; import com.mucommander.ui.combobox.AutocompleteEditableCombobox; import com.mucommander.ui.combobox.EditableComboBox; import com.mucommander.ui.combobox.EditableComboBoxListener; import com.mucommander.ui.combobox.SaneComboBox; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.util.Iterator; /** * Widget used for shell command input. *

    * In addition to providing basic shell command input features, this widget interfaces with * the {@link com.mucommander.shell.ShellHistoryManager} to offer a history of shell commands * for the user to browse through. *

    * Note that even though this component is affected by themes, it's impossible to edit the current theme while it's being displayed. * For this reason, the RunDialog doesn't listen to theme modifications. * * @author Maxence Bernard, Nicolas Rinaudo */ public class ShellComboBox extends AutocompleteEditableCombobox implements EditableComboBoxListener, ShellHistoryListener, PopupMenuListener { /** Input field used to type in commands. */ private final JTextField input; /** Where to init commands. */ private final RunDialog parent; /** * Creates a new shell combo box. * @param parent where to execute commands. */ public ShellComboBox(RunDialog parent) { super(CompleterFactory.getComboboxOptionsCompleter()); this.parent = parent; // Sets the combo box's editor. this.input = getTextField(); addPopupMenuListener(this); // Sets colors and font according to the current theme. setForeground(ThemeManager.getCurrentColor(Theme.SHELL_HISTORY_FOREGROUND_COLOR)); setBackground(ThemeManager.getCurrentColor(Theme.SHELL_HISTORY_BACKGROUND_COLOR)); setSelectionForeground(ThemeManager.getCurrentColor(Theme.SHELL_HISTORY_SELECTED_FOREGROUND_COLOR)); setSelectionBackground(ThemeManager.getCurrentColor(Theme.SHELL_HISTORY_SELECTED_BACKGROUND_COLOR)); setFont(ThemeManager.getCurrentFont(Theme.SHELL_HISTORY_FONT)); // Fills the combo box with the current history. populateHistory(); ShellHistoryManager.addListener(this); // Select first item in the combo box (if any) if (getItemCount() > 0) { setSelectedIndex(0); } // Automatically update the text field's contents when an item is selected in this combo box setComboSelectionUpdatesTextField(true); // Listener to actions fired by this EditableComboBox addEditableComboBoxListener(this); } /** * Fills the combo box with the current shell history. */ private void populateHistory() { // Empties the content of the combo box removeAllItems(); // Iterates through all shell history elements. Iterator iterator = ShellHistoryManager.getHistoryIterator(); String command = null; while (iterator.hasNext()) { insertItemAt((command = iterator.next()), 0); } // If the list is not empty, initializes the input field on the last command. if (command != null) { input.setText(command); input.setSelectionStart(0); input.setSelectionEnd(command.length()); } } // - Misc. ---------------------------------------------------------------------- // ------------------------------------------------------------------------------ /** * Overrides this method to ignore events received when this component is disabled. */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (enabled) { input.setSelectionStart(0); input.setSelectionEnd(input.getText().length()); } } // - EditableComboBoxListener implementation ------------------------------------ // ------------------------------------------------------------------------------ public void comboBoxSelectionChanged(SaneComboBox source) {} public void textFieldValidated(EditableComboBox source) {parent.runCommand(input.getText());} public void textFieldCancelled(EditableComboBox source) {parent.dispose();} // - Shell listener code -------------------------------------------------------- // ------------------------------------------------------------------------------ public void historyChanged(String command) { command = command.trim(); insertItemAt(command, 0); } public void historyCleared() { removeAllItems(); input.setText(""); } // - Popup menu listening ------------------------------------------------------- // ------------------------------------------------------------------------------ /** * Ignored. */ public void popupMenuCanceled(PopupMenuEvent e) {} /** * Makes sure the selection is always the first element in the list. */ public void popupMenuWillBecomeVisible(PopupMenuEvent e) { setComboSelectionUpdatesTextField(false); if (getItemCount() > 0) { setSelectedIndex(0); } setComboSelectionUpdatesTextField(true); } /** * Ignored. */ public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {} // - Command handling ----------------------------------------------------------- // ------------------------------------------------------------------------------ /** * Returns the current shell command. * @return the current shell command. */ public String getCommand() {return input.getText();} } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/shell/package.html ================================================ Components used to run shell commands and browse the shell history. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/shutdown/QuitDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.shutdown; import javax.swing.JCheckBox; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.QuitAction; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; /** * Quit confirmation dialog invoked when the user asked the application to quit, which gives the user a chance * to cancel the operation in case the quit shortcut was hit by mistake. * *

    A checkbox allows the user to disable this confirmation dialog for the next the application is quit. It can * later be re-enabled in the application preferences. * * @author Maxence Bernard */ public class QuitDialog extends QuestionDialog { /** * This flag used to prevent duplication of QuitDialog on macOS */ private static boolean displayed = false; /** True when quit confirmation button has been pressed by the user */ private final boolean quitConfirmed; private final static int QUIT_ACTION = 0; private final static int CANCEL_ACTION = 1; /** * Creates a new instance of QuitDialog, displays the dialog and waits for a user's choice. This dialog * doesn't quit the application when 'Quit' is confirmed, it is up to the method that invoked this dialog * to perform that getTask, only if {@link #quitConfirmed()} returns true. * *

    If 'Quit' is selected and the 'Show next time' checkbox is unchecked, the preference will be saved and * {@link #confirmQuit()} will return true. * * @param mainFrame the parent MainFrame */ public QuitDialog(MainFrame mainFrame) { super(mainFrame.getJFrame(), Translator.get("quit_dialog.title"), Translator.get("quit_dialog.desc", ""+WindowManager.getMainFrames().size()), mainFrame.getJFrame(), new String[] {ActionProperties.getActionLabel(QuitAction.Descriptor.ACTION_ID), Translator.get("cancel")}, new int[] {QUIT_ACTION, CANCEL_ACTION}, 0); JCheckBox showNextTimeCheckBox = new JCheckBox(Translator.get("quit_dialog.show_next_time"), true); addComponent(showNextTimeCheckBox); this.quitConfirmed = getActionValue() == QUIT_ACTION; if (quitConfirmed) { // Remember user preference TcConfigurations.getPreferences().setVariable(TcPreference.CONFIRM_ON_QUIT, showNextTimeCheckBox.isSelected()); } } /** * Returns true if the user confirmed and pressed the Quit button. * * @return true if the user confirmed and pressed the Quit button */ public boolean quitConfirmed() { return quitConfirmed; } /** * Returns true if quit confirmation hasn't been disabled in the preferences, and if there is at least * one window to close. * * @return true if quit confirmation hasn't been disabled in the preferences */ public static boolean confirmationRequired() { return !WindowManager.getMainFrames().isEmpty() // May happen after an uncaught exception in the startup sequence && TcConfigurations.getPreferences().getVariable(TcPreference.CONFIRM_ON_QUIT, TcPreferences.DEFAULT_CONFIRM_ON_QUIT); } /** * Shows up a QuitDialog asking the user for confirmation to quit, and returns true if user confirmed * the operation. The dialog will not be shown if quit confirmation has been disabled in the preferences. * In this case, true will simply be returned. * * @return true if user confirmed the quit operation */ public static synchronized boolean confirmQuit() { if (displayed) { return false; } displayed = true; // Show confirmation dialog only if it hasn't been disabled in the preferences if (confirmationRequired()) { QuitDialog quitDialog = new QuitDialog(WindowManager.getCurrentMainFrame()); // Return true if user confirmed quit displayed = false; return quitDialog.quitConfirmed(); } displayed = false; return true; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/shutdown/package.html ================================================ Dialogs displayed before shutdown. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/startup/CheckVersionDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.startup; import com.mucommander.commons.file.FileFactory; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.SelfUpdateJob; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.GoToWebsiteAction; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.layout.InformationPane; import com.mucommander.ui.main.MainFrame; import com.mucommander.updates.VersionChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.List; /** * This class takes care of retrieving the information about the latest muCommander version from a remote server and * displaying the result to the end user. * * @author Maxence Bernard */ public class CheckVersionDialog extends QuestionDialog { private static Logger logger; /** Parent MainFrame instance */ private final MainFrame mainFrame; /** true if the user manually clicked on the 'Check for updates' menu item, * false if the update check was automatically triggered on startup */ private final boolean userInitiated; /** Dialog's width has to be at least 240 */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320,0); private final static int OK_ACTION = 0; private final static int GO_TO_WEBSITE_ACTION = 1; private final static int INSTALL_AND_RESTART_ACTION = 2; /** * Checks for updates and notifies the user of the outcome. The check itself is performed in a separate thread * to prevent the app from waiting for the request's result. * * @param userInitiated true if the user manually clicked on the 'Check for updates' menu item, * false if the update check was automatically triggered on startup. If the check was automatically triggered, * the user won't be notified if there is no new version (current version is the latest). */ public CheckVersionDialog(MainFrame mainFrame, VersionChecker versionChecker, boolean userInitiated) { super(mainFrame.getJFrame(), "", mainFrame.getJFrame()); this.mainFrame = mainFrame; this.userInitiated = userInitiated; // Do all the hard work in a separate thread //new Thread(this, "com.mucommander.ui.dialog.startup.CheckVersionDialog's Thread").start(); init(versionChecker); } /** * Checks for updates and notifies the user of the outcome. */ public void init(VersionChecker version) { Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); String message; String title; URL downloadURL = null; boolean downloadOption = false; String jarURL = null; try { getLogger().debug("Checking for new version..."); if (version == null) { version = VersionChecker.getInstance(); } //version = VersionChecker.getInstance(); // A newer version is available if (version != null && version.isNewVersionAvailable()) { getLogger().info("A new version is available!"); title = i18n("version_dialog.new_version_title"); // Checks if the current platform can open a new browser window downloadURL = new URI(version.getDownloadURL()).toURL(); downloadOption = DesktopManager.isOperationSupported(DesktopManager.BROWSE, new Object[] {downloadURL}); // If the platform is not capable of opening a new browser window, // display the download URL. message = downloadOption ? i18n("version_dialog.new_version") : i18n("version_dialog.new_version_url", downloadURL.toString()); //message += version.getLatestVersion() + "/ " + RuntimeConstants.VERSION + "(" + RuntimeConstants.BUILD_NUMBER + ")"; jarURL = version.getJarURL(); } else { // We're already running latest version getLogger().debug("No new version."); // If the version check was not initiated by the user (i.e. was automatic), // we do not need to inform the user that he already has the latest version if (!userInitiated) { dispose(); return; } title = i18n("version_dialog.no_new_version_title"); message = i18n("version_dialog.no_new_version"); } } catch(Exception e) { // If the version check was not initiated by the user (i.e. was automatic), // we do not need to inform the user that the check failed if (!userInitiated) { dispose(); return; } title = i18n("version_dialog.not_available_title"); message = i18n("version_dialog.not_available"); } // Set title setTitle(title); List actionsV = new ArrayList<>(); List labelsV = new ArrayList<>(); // 'OK' choice actionsV.add(OK_ACTION); labelsV.add(i18n("ok")); // 'Go to website' choice (if available) if (downloadOption) { actionsV.add(GO_TO_WEBSITE_ACTION); labelsV.add(ActionProperties.getActionLabel(GoToWebsiteAction.Descriptor.ACTION_ID)); } // 'Install and restart' choice (if available) if (jarURL != null) { actionsV.add(INSTALL_AND_RESTART_ACTION); labelsV.add(i18n("version_dialog.install_and_restart")); } // Turn the vectors into arrays int nbChoices = actionsV.size(); int[] actions = new int[nbChoices]; String[] labels = new String[nbChoices]; for (int i = 0; i < nbChoices; i++) { actions[i] = actionsV.get(i); labels[i] = labelsV.get(i); } InformationPane pane = new InformationPane(message, null, Font.PLAIN, InformationPane.INFORMATION_ICON); init(pane, labels, actions, 0); JCheckBox cbShowNextTime = new JCheckBox(i18n("prefs_dialog.check_for_updates_on_startup"), TcConfigurations.getPreferences().getVariable(TcPreference.CHECK_FOR_UPDATE, TcPreferences.DEFAULT_CHECK_FOR_UPDATE)); addComponent(cbShowNextTime); setMinimumSize(MINIMUM_DIALOG_DIMENSION); // Show dialog and get user action int action = getActionValue(); executeAction(downloadURL, jarURL, action); // Remember user preference TcConfigurations.getPreferences().setVariable(TcPreference.CHECK_FOR_UPDATE, cbShowNextTime.isSelected()); } private void executeAction(URL downloadURL, String jarURL, int action) { if (action == GO_TO_WEBSITE_ACTION) { gotoWebSite(downloadURL); } else if (action == INSTALL_AND_RESTART_ACTION) { installAndRelaunch(jarURL); } } private void installAndRelaunch(String jarURL) { ProgressDialog progressDialog = new ProgressDialog(mainFrame, i18n("Installing new version")); SelfUpdateJob job = new SelfUpdateJob(progressDialog, mainFrame, FileFactory.getFile(jarURL)); progressDialog.start(job); } private void gotoWebSite(URL downloadURL) { try { DesktopManager.executeOperation(DesktopManager.BROWSE, new Object[] {downloadURL}); } catch(Exception e) { InformationDialog.showErrorDialog(this); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(CheckVersionDialog.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/startup/InitialSetupDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.startup; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Iterator; import javax.swing.*; import com.mucommander.RuntimeConstants; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.ui.combobox.TcComboBox; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import static com.mucommander.conf.TcPreference.*; import static com.mucommander.conf.TcPreference.LOOK_AND_FEEL; /** * Dialog box allowing users to select misc. setup options for muCommander. * @author Nicolas Rinaudo */ public class InitialSetupDialog extends FocusDialog implements ActionListener { /** All available look and feels. */ private UIManager.LookAndFeelInfo[] lfInfo; /** Used to select a startup theme. */ private TcComboBox cbTheme; /** Used to select a look and feel. */ private TcComboBox cbLookAndFeel; private TcComboBox cbEditorTheme; /** Used to validate the user's choice. */ private JButton btnOk; /** * Creates the dialog's theme panel. * @return the dialog's theme panel. */ private JPanel createThemesPanel() { JPanel themePanel = new YBoxPanel(); themePanel.setAlignmentX(LEFT_ALIGNMENT); themePanel.setBorder(BorderFactory.createTitledBorder(i18n("prefs_dialog.themes"))); // Adds the panel description. JPanel tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(i18n("setup.theme") + ':')); // Adds the theme combo box. cbTheme = createThemeComboBox(); tempPanel.add(cbTheme); themePanel.add(tempPanel); // Adds the panel description. tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(i18n("prefs_dialog.syntax_themes") + ':')); cbEditorTheme = new TcComboBox<>(ThemeManager.predefinedSyntaxThemeNames()); cbEditorTheme.addActionListener(this); tempPanel.add(cbEditorTheme); themePanel.add(tempPanel); return themePanel; } private TcComboBox createThemeComboBox() { TcComboBox themeComboBox = new TcComboBox<>(); Iterator themes = ThemeManager.availableThemes(); int index = 0; // Index of the currently analyzed theme. int selectedIndex = 0; // Index of the current theme in the combo box. // Adds all themes to the combo box. while (themes.hasNext()) { Theme theme = themes.next(); themeComboBox.addItem(theme); if (ThemeManager.isCurrentTheme(theme)) { selectedIndex = index; } index++; } // Selects the current theme. themeComboBox.setSelectedIndex(selectedIndex); themeComboBox.addActionListener(this); return themeComboBox; } /** * Creates the dialog's look and feel panel. * @return the dialog's look and feel panel. */ private JPanel createLookAndFeelPanel() { // Initializes the theme panel. JPanel lfPanel = new YBoxPanel(); lfPanel.setAlignmentX(LEFT_ALIGNMENT); lfPanel.setBorder(BorderFactory.createTitledBorder(i18n("prefs_dialog.look_and_feel"))); // Adds the panel description. JPanel tempPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); tempPanel.add(new JLabel(i18n("setup.look_and_feel") + ':')); lfPanel.add(tempPanel); // Initialises the l&f combo box. cbLookAndFeel = new TcComboBox<>(); lfInfo = UIManager.getInstalledLookAndFeels(); String currentLf = UIManager.getLookAndFeel().getName(); int selectedIndex = -1; // Goes through all available look&feels and selects the current one. for (int i = 0; i < lfInfo.length; i++) { String buffer = lfInfo[i].getName(); // Tries to select current L&F if (currentLf.equals(buffer)) { selectedIndex = i; }// Under Mac OS X, Mac L&F is either reported as 'MacOS' or 'MacOS Adaptative' // so we need this test else if (selectedIndex == -1 && (currentLf.startsWith(buffer) || buffer.startsWith(currentLf))) { selectedIndex = i; } cbLookAndFeel.addItem(buffer); } // If no match, selects first one if (selectedIndex == -1) { selectedIndex = 0; } cbLookAndFeel.setSelectedIndex(selectedIndex); cbLookAndFeel.addActionListener(this); lfPanel.add(cbLookAndFeel); return lfPanel; } /** * Creates the dialog's main panel. * @return the dialog's main panel. */ private JPanel createMainPanel() { YBoxPanel mainPanel = new YBoxPanel(); mainPanel.add(new JLabel(i18n("setup.intro"))); mainPanel.addSpace(10); mainPanel.add(createThemesPanel()); mainPanel.addSpace(10); mainPanel.add(createLookAndFeelPanel()); mainPanel.addSpace(10); JPanel okPanel = new JPanel(); okPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); okPanel.add(btnOk = new JButton(i18n("ok"))); btnOk.addActionListener(this); mainPanel.add(okPanel); return mainPanel; } /** * Creates a new InitialSetupDialog. * @param owner dialog's owner. */ public InitialSetupDialog(Frame owner) { super(owner, i18n("setup.title"), owner); preInitDefault(); getContentPane().add(createMainPanel(), BorderLayout.CENTER); pack(); setResizable(false); setInitialFocusComponent(cbTheme); setKeyboardDisposalEnabled(false); getRootPane().setDefaultButton(btnOk); } @Override public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == cbTheme) { ThemeManager.setCurrentTheme((Theme) cbTheme.getSelectedItem()); } else if (src == cbLookAndFeel) { setVariable(LOOK_AND_FEEL, lfInfo[cbLookAndFeel.getSelectedIndex()].getClassName()); } else if (src == cbEditorTheme) { setVariable(SYNTAX_THEME_NAME, cbEditorTheme.getSelectedIndex()); } else if (src == btnOk) { ThemeManager.setCurrentTheme((Theme) cbTheme.getSelectedItem()); setVariable(LOOK_AND_FEEL, lfInfo[cbLookAndFeel.getSelectedIndex()].getClassName()); postInitDefault(); dispose(); } } private void preInitDefault() { initFileGroups(); boolean isGnome = OsFamily.LINUX.isCurrent() && new com.mucommander.desktop.gnome.GuessedGnomeDesktopAdapter().isAvailable(); if (isGnome && RuntimeConstants.DISPLAY_4K) { setVariable(TOOLBAR_ICON_SCALE, 2.0f); setVariable(COMMAND_BAR_ICON_SCALE, 2.0f); setVariable(TABLE_ICON_SCALE, 3.0f); } } private static void initFileGroups() { setVariable(FILE_GROUP_1_MASK, "*.png,*.jpg,*.gif,*.bmp,*.pcx,*.ico,*.jpeg,*.psd"); setVariable(FILE_GROUP_2_MASK, "*.java,*.pas,*.cpp,*.hpp,*.h,*.c,*.m,*.kt,*.s,*.asm,*.inc,*.go,*.rs,*.art"); setVariable(FILE_GROUP_3_MASK, "*.htm,*.html,*.css"); setVariable(FILE_GROUP_4_MASK, "*.py,*.php,*.js,*.gradle"); setVariable(FILE_GROUP_5_MASK, "*.xml,*.svg"); setVariable(FILE_GROUP_6_MASK, "*.doc,*.rtf,*.txt,*.pdf,*.djvu"); setVariable(FILE_GROUP_7_MASK, "*.mpg,*.mp4,*.avi"); setVariable(FILE_GROUP_8_MASK, ""); setVariable(FILE_GROUP_9_MASK, ""); setVariable(FILE_GROUP_10_MASK, ""); } private void postInitDefault() { } private static void setVariable(TcPreference preference, String value) { TcConfigurations.getPreferences().setVariable(preference, value); } private static void setVariable(TcPreference preference, int value) { TcConfigurations.getPreferences().setVariable(preference, value); } private static void setVariable(TcPreference preference, float value) { TcConfigurations.getPreferences().setVariable(preference, value); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/startup/package.html ================================================ Dialogs displayed when muCommander is starting up. ================================================ FILE: src/main/java/com/mucommander/ui/dialog/symlink/CreateSymLinkDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.symlink; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.SymLinkUtils; import com.mucommander.job.ui.UserInputHelper; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.text.FilePathField; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JTextField; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.nio.file.AccessDeniedException; import java.nio.file.FileAlreadyExistsException; /** * @author Oleg Trifonov * Created on 09/06/14. */ public class CreateSymLinkDialog extends FocusDialog implements ActionListener { /** * Dialog size constraints */ private static final Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320, 0); /** * Dialog width should not exceed 360, height is not an issue (always the same) */ private static final Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(1024, 320); private static final int RETRY_ACTION = 1; private static final int CANCEL_ACTION = 0; private final Frame mainFrame; private final FilePathField edtTarget; private final JTextField edtName; private final JButton btnOk; private final JButton btnCancel; /** * * @param mainFrame * @param linkPath path of root directory for created symbolic link or symbolic link itself * @param targetFile existing filename (filename symlink will point to) */ public CreateSymLinkDialog(Frame mainFrame, AbstractFile linkPath, AbstractFile targetFile) { super(mainFrame, i18n("symboliclinkeditor.create"), null); this.mainFrame = mainFrame; Container contentPane = getContentPane(); YBoxPanel yPanel = new YBoxPanel(10); yPanel.add(new JLabel(i18n("symboliclinkeditor.target_file_create") + ':')); edtTarget = new FilePathField(); yPanel.add(edtTarget); yPanel.addSpace(10); yPanel.add(new JLabel(i18n("symboliclinkeditor.link_name") + ':')); edtName = new FilePathField(); yPanel.add(edtName); contentPane.add(yPanel, BorderLayout.NORTH); btnOk = new JButton(i18n("ok")); btnCancel = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this), BorderLayout.SOUTH); // Path field will receive initial focus setInitialFocusComponent(edtTarget); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); edtName.setText(linkPath.getAbsolutePath() + targetFile.getName()); edtTarget.setText(targetFile.getAbsolutePath()); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == btnOk) { new Thread(this::execute).start(); } else if (e.getSource() == btnCancel) { dispose(); } } private void execute() { QuestionDialog dialog = null; final String targetPath = edtTarget.getText(); final String linkPath = edtName.getText(); String errorMessage; while (true) { try { SymLinkUtils.createSymlink(linkPath, targetPath); // success break; } catch (FileAlreadyExistsException e) { errorMessage = i18n("cannot_write_symlink_already_exists", linkPath); } catch (AccessDeniedException e) { errorMessage = i18n("cannot_write_symlink_access_denied", linkPath); } catch (IOException e) { errorMessage = i18n("cannot_write_symlink", linkPath); } if (dialog == null) { dialog = new QuestionDialog(mainFrame, i18n("error"), errorMessage, mainFrame, new String[] {i18n("retry"), i18n("cancel")}, new int[] {RETRY_ACTION, CANCEL_ACTION}, 0); } UserInputHelper jobUserInput = new UserInputHelper(null, dialog); int action = (Integer)jobUserInput.getUserInput(); if (action < 0 || action == CANCEL_ACTION) { break; } } cancel(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/symlink/EditSymlinkDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.symlink; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.SymLinkUtils; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.text.FilePathField; import javax.swing.JButton; import javax.swing.JLabel; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; /** * Created on 23/06/14. */ public class EditSymlinkDialog extends FocusDialog implements ActionListener { private final Frame mainFrame; private final AbstractFile linkPath; private final FilePathField edtTarget; private final JButton btnOk; /** * Dialog size constraints */ private static final Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(320, 0); /** * Dialog width should not exceed 360, height is not an issue (always the same) */ private static final Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(1024, 320); public EditSymlinkDialog(Frame mainFrame, AbstractFile linkPath) { super(mainFrame, i18n("symboliclinkeditor.edit"), null); this.mainFrame = mainFrame; this.linkPath = linkPath; Container contentPane = getContentPane(); YBoxPanel yPanel = new YBoxPanel(10); edtTarget = new FilePathField(); edtTarget.setDefaultLocation(linkPath.getParent()); String hint = String.format(i18n("symboliclinkeditor.target_file_edit"), linkPath.getBaseName())+ ':'; yPanel.add(new JLabel(hint)); yPanel.add(edtTarget); edtTarget.setText(SymLinkUtils.getTargetPath(linkPath)); edtTarget.addActionListener(this); yPanel.addSpace(10); contentPane.add(yPanel, BorderLayout.NORTH); btnOk = new JButton(i18n("ok")); JButton btnCancel = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this), BorderLayout.SOUTH); btnOk.addActionListener(this); // Path field will receive initial focus setInitialFocusComponent(edtTarget); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == btnOk || e.getSource() == edtTarget) { SymLinkUtils.editSymlink(linkPath, edtTarget.getText()); } cancel(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dialog/tab/TabTitleDialog.kt ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dialog.tab import com.mucommander.ui.action.ActionProperties import com.mucommander.ui.action.impl.SetTabTitleAction import com.mucommander.ui.dialog.DialogToolkit import com.mucommander.ui.dialog.FocusDialog import com.mucommander.ui.layout.XBoxPanel import com.mucommander.ui.main.FolderPanel import com.mucommander.ui.main.MainFrame import com.mucommander.ui.text.SizeConstrainedDocument import java.awt.BorderLayout import java.awt.Dimension import java.awt.event.ActionEvent import java.awt.event.ActionListener import javax.swing.* /** * This dialog allow the user to enter a title for the currently selected tab. * Empty title means that the tab title will be based on the current location * presented in the tab. * * @author Arik Hadas */ class TabTitleDialog( mainFrame: MainFrame, /** The FolderPanel to which this tab belongs */ private val folderPanel: FolderPanel ) : FocusDialog( mainFrame.jFrame, ActionProperties.getActionLabel(SetTabTitleAction.Descriptor.ACTION_ID), folderPanel.panel ), ActionListener { private val okButton = JButton(i18n("ok")) /** The text field in which the title is entered */ private val titleTextField = JTextField().apply { setDocument(SizeConstrainedDocument(31)) text = folderPanel.tabs.getCurrentTab().getTitle() selectAll() } init { val cancelButton = JButton(i18n("cancel")) contentPane.apply { setLayout(BorderLayout()) // Add customization panel add(createInnerPanel(), BorderLayout.CENTER) // Aligns the button panel to the right. add(DialogToolkit.createOKCancelPanel(okButton, cancelButton, rootPane, this@TabTitleDialog), BorderLayout.SOUTH) } minimumSize = MINIMUM_SIZE } private fun createInnerPanel(): JPanel = XBoxPanel().apply { add(JLabel(i18n("title") + ":")) addSpace(10) add(titleTextField) //, BorderLayout.CENTER); setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)) } private fun changeTabTitle() { val title = titleTextField.getText().trim() folderPanel.tabs.setTitle(title.ifEmpty { null }) } override fun actionPerformed(e: ActionEvent) { dispose() if (e.getSource() === okButton) { changeTabTitle() } } companion object { /** Ensure the dialog width is at least 300 */ private val MINIMUM_SIZE = Dimension(250, 0) } } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/ClipboardNotifier.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dnd; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.FlavorEvent; import java.awt.datatransfer.FlavorListener; import javax.swing.Action; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * ClipboardNotifier allows an action to be dynamically enabled when the clipboard contains files, and disabled otherwise. * *

    ClipboardNotifier requires Java 1.5 and does not work under Mac OS X (tested under Tiger with Java 1.5.0_06). * * @author Maxence Bernard */ public class ClipboardNotifier implements FlavorListener { private static final Logger LOGGER = LoggerFactory.getLogger(ClipboardNotifier.class); /** The action to dynamically enable/disable */ private Action action; /** * Starts monitoring the clipboard for files and dynamically enable/disable the specified action accordingly. * The action is initially enabled if the clipboard contains files. * * @param action the action to dynamically enable/disable when files are present/not present */ public ClipboardNotifier(Action action) { this.action = action; // Toggle initial state toggleActionState(); // Monitor clipboard changes ClipboardSupport.getClipboard().addFlavorListener(this); } /** * Toggle the action depending on the clipboard contents. */ private void toggleActionState() { try { action.setEnabled(ClipboardSupport.getClipboard().isDataFlavorAvailable(DataFlavor.javaFileListFlavor)); } catch(Exception e) { // Works around "java.lang.IllegalStateException: cannot open system clipboard" thrown when the clipboard // is currently unavailable (ticket #164). LOGGER.debug("Caught an exception while querying the clipboard for files", e); } } /////////////////////////////////// // FlavorListener implementation // /////////////////////////////////// public void flavorsChanged(FlavorEvent event) { toggleActionState(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/ClipboardOperations.java ================================================ package com.mucommander.ui.dnd; /** * * @author Kezides */ //List of possible operations to be made with the clipboard. public enum ClipboardOperations { CUT, COPY, ARCHIVE } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/ClipboardSupport.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dnd; import com.mucommander.commons.file.util.FileSet; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.Transferable; /** * This class provides methods to more easily interact with the system clipboard. * * @author Maxence Bernard */ public class ClipboardSupport { private static ClipboardOperations operation; public static ClipboardOperations getOperation(){ return operation; } public static void setOperation(ClipboardOperations operation){ ClipboardSupport.operation = operation; } /** * Returns the system clipboard's contents as a Transferable, null * if it currently has no contents. */ private static Transferable getClipboardContents() { try { return getClipboard().getContents(null); } catch(IllegalStateException e) { return null; } } /** * Sets the contents of the system clipboard. * * @param transferable the data to transfer to the clipboard */ public static void setClipboardContents(Transferable transferable) { try { getClipboard().setContents(transferable, null); } catch(IllegalStateException ignored) {} } /** * Returns the files contained by the system clipboard as a {@link com.mucommander.commons.file.util.FileSet}, null * if it currently has no contents or if the item(s) contained are not files. */ public static FileSet getClipboardFiles() { Transferable transferable = getClipboardContents(); // May return null if no file could be retrieved from the transferable instance return transferable == null ? null : TransferableFileSet.getTransferFiles(transferable); } /** * Transfers the files contained in the specified {@link com.mucommander.commons.file.util.FileSet} to the system clipboard. * The data will be transferred as a {@link TransferableFileSet}. * * @param fileSet the files to transfer to the system clipboard. */ public static void setClipboardFiles(FileSet fileSet) { TransferableFileSet tfs = new TransferableFileSet(fileSet); // Disable FileSetDataFlavor support which would otherwise throw an exception because the data is not serializable tfs.setFileSetDataFlavorSupported(false); setClipboardContents(tfs); } /** * Returns an instance of the system clipboard. */ public static Clipboard getClipboard() { return Toolkit.getDefaultToolkit().getSystemClipboard(); } } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/DnDContext.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dnd; import com.mucommander.ui.main.FolderPanel; /** * This class gives information about the context in which a drag-and-drop operation is being performed. * The getters are static since only one drag-and-drop operation can be performed at the same time. The information * returned by the getters is meaningful only when a drag-and-drop is being carried out. * * @see FileDragSourceListener * @author Maxence Bernard */ class DnDContext { /** Has the drag operation been initiated by muCommander ? */ private static boolean dragInitiatedByMucommander; /** FolderPanel instance which initiated the drag */ private static FolderPanel dragInitiator; /** Current drag gesture modifiers */ private static int dragGestureModifiersEx; /** * Returns true if the current drag has been initiated by muCommander, i.e. *not* by another application. * The returned value has a meaning only if a drag operation is currently being performed. */ static boolean isDragInitiatedByMucommander() { return dragInitiatedByMucommander; } /** * This method is called by {@link FileDragSourceListener}. */ static void setDragInitiatedByMucommander(boolean b) { dragInitiatedByMucommander = b; } /** * Returns the {@link FolderPanel} instance that initiated the drag operation. * This method returns null if the current drag has not been initiated by muCommander. */ static FolderPanel getDragInitiator() { return dragInitiator; } /** * This method is called by {@link FileDragSourceListener}. */ static void setDragInitiator(FolderPanel fp) { dragInitiator = fp; } /** * Returns the extended modifiers that are currently pressed while dragging. * This method returns 0 if the current drag has not been initiated by muCommander. */ static int getDragGestureModifiersEx() { return dragGestureModifiersEx; } /** * This method is called by {@link FileDragSourceListener}. */ static void setDragGestureModifiersEx(int modifiersEx) { dragGestureModifiersEx = modifiersEx; // AppLogger.finest("gestureModifiersEx="+modifiersEx); // AppLogger.finest("getModifiersExText="+ InputEvent.getModifiersExText(modifiersEx)); // AppLogger.finest("is shift down="+((modifiersEx&InputEvent.SHIFT_DOWN_MASK)!=0)); // AppLogger.finest("is ctrl down="+((modifiersEx&InputEvent.CTRL_DOWN_MASK)!=0)); // AppLogger.finest("is alt down="+((modifiersEx&InputEvent.ALT_DOWN_MASK)!=0)); // AppLogger.finest("is meta down="+((modifiersEx&InputEvent.META_DOWN_MASK)!=0)); } } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/FileDragSourceListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dnd; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import java.awt.*; import java.awt.dnd.*; import java.awt.event.InputEvent; /** * This class adds 'drag' support to components that are registered using the {@link #enableDrag(java.awt.Component)} * method. * *

    A {@link com.mucommander.ui.main.FolderPanel} instance has to be specified at creation time, this instance will be * used to retrieve the list of selected/marked file(s) that are dragged, whenever a drag operation is initiated on * of the registered components. * * @author Maxence Bernard */ public class FileDragSourceListener implements DragGestureListener, DragSourceListener { /** the FolderPanel instance used to retrieve dragged files */ private final FolderPanel folderPanel; /** * Creates a new FileDragSourceListener using the specified FolderPanel that will be used to retrieve the dragged files * based on the current file selection. * * @param folderPanel the FolderPanel used to retrieve the list of selected/marked file(s) that are dragged */ public FileDragSourceListener(FolderPanel folderPanel) { this.folderPanel = folderPanel; } /** * Enables drag operations on the specified component. This class will be notified whenever drag operations * are performed on the component. * * @param c the component for which to add 'drag' support */ public void enableDrag(Component c) { DragSource dragSource = DragSource.getDefaultDragSource(); dragSource.createDefaultDragGestureRecognizer(c, DnDConstants.ACTION_COPY|DnDConstants.ACTION_MOVE|DnDConstants.ACTION_LINK, this); } // /** // * Creates a custom DragGestureEvent instance re-using the information contained in the given DragGestureEvent, but // * overridding the actions with the specified actions bitwise mask. // * When used with DragSource.startDrag, this allows to start a drag operation with a different source // * action set from the one specified in the DragGestureRecognizer, based on the current state and // * contents of the FolderPanel. // */ // private DragGestureEvent createCustomDragGestureEvent(DragGestureEvent originalDGE, int actions) { // Vector eventList = new Vector(); // Iterator eventIterator = originalDGE.iterator(); // // while(eventIterator.hasNext()) // eventList.add(eventIterator.next()); // // DragGestureRecognizer dragGestureRecognizer = originalDGE.getSourceAsDragGestureRecognizer(); // dragGestureRecognizer.setSourceActions(actions); // // return new DragGestureEvent(dragGestureRecognizer, // actions, // originalDGE.getDragOrigin(), // eventList); // } ///////////////////////////////// // DragGestureListener methods // ///////////////////////////////// public void dragGestureRecognized(DragGestureEvent event) { if (folderPanel.getMainFrame().getNoEventsMode()) return; FileTable fileTable = folderPanel.getFileTable(); BaseFileTableModel tableModel = fileTable.getFileTableModel(); // Return (do not initiate drag) if mouse button2 or button3 was used if ((event.getTriggerEvent().getModifiersEx() & (InputEvent.BUTTON2_DOWN_MASK|InputEvent.BUTTON3_DOWN_MASK)) != 0) return; // Do not use that to retrieve the current selected file as it is inaccurate: the selection could have changed since the mouse was clicked. // AbstractFile selectedFile = fileTable.getSelectedFile(false); // // Return if selected file is null (could happen if '..' is selected) // if(selectedFile==null) // return; // Find out which row was clicked int clickedRow = fileTable.rowAtPoint(event.getDragOrigin()); int clickedCol = fileTable.columnAtPoint(event.getDragOrigin()); int index = tableModel.getFileIndexAt(clickedRow, clickedCol); // Return (do not initiate drag) if the selected file is the parent folder '..' if (index < 0 || fileTable.isParentFolder(index)) { return; } // Retrieve the file corresponding to the clicked row AbstractFile selectedFile = tableModel.getFileAt(index); // Find out which files are to be dragged, based on the selected file and currenlty marked files. // If there are some files marked, drag marked files only if the selected file is one of the marked files. // In any other case, only drag the selected file. FileSet markedFiles; FileSet draggedFiles; if (tableModel.getNbMarkedFiles() > 0 && (markedFiles = fileTable.getSelectedFiles()).contains(selectedFile)) { draggedFiles = markedFiles; } else { draggedFiles = new FileSet(folderPanel.getCurrentFolder(), selectedFile); } // Set initial DnDContext information DnDContext.setDragInitiatedByMucommander(true); DnDContext.setDragInitiator(folderPanel); DnDContext.setDragGestureModifiersEx(event.getTriggerEvent().getModifiersEx()); // Start dragging DragSource.getDefaultDragSource().startDrag(event, null, new TransferableFileSet(draggedFiles), this); // DragSource.getDefaultDragSource().startDrag(createCustomDragGestureEvent(event, DnDConstants.ACTION_MOVE), null, new TransferableFileSet(draggedFiles), this); } /////////////////////////////////////// // DragSourceListener implementation // /////////////////////////////////////// public void dragEnter(DragSourceDragEvent event) { // Update drag gesture modifiers DnDContext.setDragGestureModifiersEx(event.getGestureModifiersEx()); } public void dragOver(DragSourceDragEvent event) { } public void dropActionChanged(DragSourceDragEvent event) { // Update drag gesture modifiers DnDContext.setDragGestureModifiersEx(event.getGestureModifiersEx()); } public void dragExit(DragSourceEvent event) { } public void dragDropEnd(DragSourceDropEvent event) { // Reset DnDContext information DnDContext.setDragInitiatedByMucommander(false); DnDContext.setDragInitiator(null); DnDContext.setDragGestureModifiersEx(0); } } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/FileDropTargetListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dnd; import java.awt.Cursor; import java.awt.datatransfer.DataFlavor; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragSource; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.event.InputEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.job.CopyJob; import com.mucommander.job.MoveJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; /** * Provides file(s) 'drop' support to components that add a DropTarget using this DropTargetListener. * A {@link com.mucommander.ui.main.FolderPanel} instance has to be specified at creation time, this instance will be * used to change the current folder, or copy/move files to the current folder. * *

    There are 2 different modes this class can operate in. The mode to be used has to be specified when this class is * instantiated. * *

    In 'folder change mode', when a file or string representing a file path is dropped, the associated FolderPanel's * current folder is changed: *

      *
    • If the file is a directory, the current folder is changed to that directory *
    • For any other file kind (archive, regular file...), current folder is changed to the file's parent folder * and the file is selected *
    * If more than one file (or file path) is dropped, only the first one is taken into account. * *

    In the normal mode, files (or file paths) that are dropped can also be moved or copied to the associated FolderPanel's * current folder, on top of the change current folder action. The actual drop action performed (move, copy or change current folder) * depends on the keyboard modifiers typed by the user when dragging the files. * When the mouse cursor enters the drop-enabled component's area, it is changed to symbolize the action to be performed. * The default drop action (when no modifier is down) is copy. * *

    Drop events originating from the same FolderPanel are on purpose not accepted as spring-loaded folders are not * (yet) supported which would make the drop operation ambiguous and confusing. * * @author Maxence Bernard */ public class FileDropTargetListener implements DropTargetListener { private static final Logger LOGGER = LoggerFactory.getLogger(FileDropTargetListener.class); /** the FolderPanel instance used to change the current folder when a file is dropped */ private final FolderPanel folderPanel; /** Mode that specifies what to do when files are dropped */ private final boolean changeFolderOnlyMode; /** Drop action (copy or move) currenlty specified by the user */ private int currentDropAction; /** Has DropTargetDragEvent event been accepted ? */ private boolean dragAccepted; /** * Extended modifiers which must be down while dragging for the drop action to be a MOVE and not a COPY (default): * InputEvent.META_DOWN_MASK under Mac OS X, InputEvent.ALT_DOWN_MASK under any other * platform. */ private final static int MOVE_ACTION_MODIFIERS_EX = OsFamily.MAC_OS_X.isCurrent()? InputEvent.META_DOWN_MASK :InputEvent.ALT_DOWN_MASK; /** * Creates a new FileDropTargetListener using the provided FolderPanel that will be used to either change the * current folder or copy/move when files are dropped, depending on the specified operating mode and drop action. * * @param folderPanel the FolderPanel instance used to change the current folder or copy/move when files are dropped * @param changeFolderOnlyMode if true, the FolderPanel's current folder can only be changed when file(s) * are dropped, files cannot be copied or moved. */ public FileDropTargetListener(FolderPanel folderPanel, boolean changeFolderOnlyMode) { this.folderPanel = folderPanel; this.changeFolderOnlyMode = changeFolderOnlyMode; } /** * Returns a mouse Cursor that symbolizes the given drop action and 'accepted' status. * The given action must one of the following: *

      *
    • DnDConstants.ACTION_COPY *
    • DnDConstants.ACTION_MOVE *
    • DnDConstants.ACTION_LINK *
    * If the action has any other value, the default Cursor is returned. */ private Cursor getDragActionCursor(int dropAction, boolean dragAccepted) { switch(dropAction) { case DnDConstants.ACTION_COPY: return dragAccepted?DragSource.DefaultCopyDrop:DragSource.DefaultCopyNoDrop; case DnDConstants.ACTION_MOVE: return dragAccepted?DragSource.DefaultMoveDrop:DragSource.DefaultMoveNoDrop; case DnDConstants.ACTION_LINK: return dragAccepted?DragSource.DefaultLinkDrop:DragSource.DefaultLinkNoDrop; default: return Cursor.getDefaultCursor(); } } /** * Accepts or rejects the specified DropTargetDragEvent and changes the mouse cursor to match the * current drop action. * The drag event will be accepted it supports at least one of the supported DataFlavors and one of the two * following conditions are true: *
      *
    • the event originates from one of muCommander's {@link FolderPanel} for which the current folder is not the * same as the FolderPanel associated with this FileDropTargetListener *
    • the event does not originate from muCommander *
    * *

    This method overrides the default drop action for drag-and-drop operations within muCommander to make it * DnDConstants.ACTION_COPY instead of DnDConstants.ACTION_MOVE. * For a move action to be performed when the mouse is released, the modifiers defined by * {@link #MOVE_ACTION_MODIFIERS_EX} must be down. * * @return true if the event was accepted, false otherwise */ private boolean acceptOrRejectDragEvent(DropTargetDragEvent event) { this.currentDropAction = event.getDropAction(); this.dragAccepted = event.isDataFlavorSupported(TransferableFileSet.getFileSetDataFlavor()) || event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) || event.isDataFlavorSupported(DataFlavor.getTextPlainUnicodeFlavor()); if(dragAccepted && DnDContext.isDragInitiatedByMucommander()) { FolderPanel dragInitiator = DnDContext.getDragInitiator(); if(dragInitiator==folderPanel || dragInitiator.getCurrentFolder().equalsCanonical(folderPanel.getCurrentFolder())) { // Refuse drag if the drag was initiated by the same FolderPanel, or if its current folder is the same // as this one this.dragAccepted = false; } else { // Change the default drop action to DnDConstants.ACTION_COPY instead of DnDConstants.ACTION_MOVE, // if the move extended modifiers are not currently down. int dragModifiers = DnDContext.getDragGestureModifiersEx(); if(currentDropAction==DnDConstants.ACTION_MOVE && (dragModifiers&MOVE_ACTION_MODIFIERS_EX)==0 && (event.getSourceActions()&DnDConstants.ACTION_COPY)!=0) { LOGGER.debug("changing default action, was: DnDConstants.ACTION_MOVE, now: DnDConstants.ACTION_COPY"); currentDropAction = DnDConstants.ACTION_COPY; } } } LOGGER.trace("dragAccepted="+dragAccepted+" dropAction="+currentDropAction); if (dragAccepted) { // Accept the drag event with our drop action event.acceptDrag(currentDropAction); } else { // Reject the drag event event.rejectDrag(); } LOGGER.trace("cursor="+getDragActionCursor(currentDropAction, dragAccepted)); // Change the mouse cursor on this FolderPanel and child components folderPanel.getPanel().setCursor(getDragActionCursor(currentDropAction, dragAccepted)); return dragAccepted; } @Override public void dragEnter(DropTargetDragEvent event) { acceptOrRejectDragEvent(event); } public void dragOver(DropTargetDragEvent event) { // Although it doesn't look necessary, cursor needs to be set each time this method is called otherwise // it returns to the default one (at least under Mac OS X w/ Java 1.5) acceptOrRejectDragEvent(event); } public void dropActionChanged(DropTargetDragEvent event) { acceptOrRejectDragEvent(event); } public void dragExit(DropTargetEvent event) { // Restore default cursor folderPanel.getPanel().setCursor(Cursor.getDefaultCursor()); } public void drop(DropTargetDropEvent event) { // Restore default cursor, no matter what folderPanel.getPanel().setCursor(Cursor.getDefaultCursor()); // The drop() method is called even if a DropTargetDropEvent was rejected before, // so this test is really necessary if (!dragAccepted) { event.rejectDrop(); return; } // Accept drop event event.acceptDrop(currentDropAction); // Retrieve the files contained by the transferable as a FileSet (takes care of handling the different DataFlavors) FileSet droppedFiles = TransferableFileSet.getTransferFiles(event.getTransferable()); // Stop and report failure if no file could not be retrieved if (droppedFiles == null || droppedFiles.size() == 0) { // Report drop failure event.dropComplete(false); return; } // If in 'change folder mode' or if the drop action is 'ACTION_LINK' in normal mode: // change the FolderPanel's current folder to the dropped file/folder : // - If the file is a directory, the current folder is changed to that directory // - For any other file kind (archive, regular file...), current folder is changed to the file's parent folder and the file is selected // If more than one file is dropped, only the first one is used if (changeFolderOnlyMode || currentDropAction == DnDConstants.ACTION_LINK) { AbstractFile file = droppedFiles.elementAt(0); // If file is a directory, change current folder to that directory if (file.isDirectory()) folderPanel.tryChangeCurrentFolder(file); // For any other file kind (archive, regular file...), change directory to the file's parent folder // and select the file else folderPanel.tryChangeCurrentFolder(file.getParent(), file, false); // Request focus on the FolderPanel folderPanel.getPanel().requestFocus(); } // Normal mode: copy or move dropped files to the FolderPanel's current folder else { MainFrame mainFrame = folderPanel.getMainFrame(); AbstractFile destFolder = folderPanel.getCurrentFolder(); if (currentDropAction == DnDConstants.ACTION_MOVE) { // Start moving files ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("move_dialog.moving")); MoveJob moveJob = new MoveJob(progressDialog, mainFrame, droppedFiles, destFolder, null, FileCollisionDialog.ASK_ACTION, false); progressDialog.start(moveJob); } else { // Start copying files ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); CopyJob job = new CopyJob(progressDialog, mainFrame, droppedFiles, destFolder, null, CopyJob.Mode.COPY, FileCollisionDialog.ASK_ACTION); progressDialog.start(job); } } // Report that the drop event has been successfully handled event.dropComplete(true); } } ================================================ FILE: src/main/java/com/mucommander/ui/dnd/TransferableFileSet.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.dnd; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.util.FileSet; import lombok.Setter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.BufferedReader; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Vector; /** * This class represents a Transferable file set and is used for Drag and Drop transfers initiated by muCommander * (dragged from a muCommander UI component). * *

    The actual file set data can be fetched using one of those 3 DataFlavors : *

      *
    • FileSetDataFlavor (as returned by {@link #getFileSetDataFlavor()}): data returned as a {@link com.mucommander.commons.file.util.FileSet}. * This flavor is used for local file transfers (within the application) only. In particular, this DataFlavor cannot * be used to transfer data to the clipboard because the data (FileSet) cannot be serialized. * In this case, the {@link #setFileSetDataFlavorSupported(boolean)} method should be used to disable FileSet DataFlavor. *
    • *
    • DataFlavor.javaFileListFlavor : data returned as a java.util.Vector of java.io.File files. * This flavor is used for file transfers to and from external applications. *
    • *
    • text/uri-list (RFC 2483): an alternative flavor supported by Gnome and KDE where the data is returned as * a String containing file URIs separated by '\r\n'. This flavor is as of today the only one supported by GNOME and * KDE. See bug #45 and http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4899516 . *
    • *
    • DataFlavor.stringFlavor: data returned as a String containing files paths separated by \n characters. * This other alternative flavor is used for file transfers to and from external applications that do not support * either of DataFlavor.javaFileListFlavor and text/uri-list but text only (plain text editors for example). *
    • *
    * * @author Maxence Bernard, Xavi Miró */ public class TransferableFileSet implements Transferable { private static final Logger LOGGER = LoggerFactory.getLogger(TransferableFileSet.class); /** Transferred FileSet */ private final FileSet fileSet; /** Is FileSet DataFlavor supported ? */ private boolean fileSetFlavorSupported = true; /** Is DataFlavor.javaFileListFlavor supported ? */ private boolean javaFileListFlavorSupported = true; /** Is DataFlavor.stringFlavor supported ? */ private boolean stringFlavorSupported = true; /** Is text/uri-list (RFC 2483) flavor supported ? * -- SETTER -- * Sets whether the text/uri-list (RFC 2483) should be supported by this Transferable * (supported by default). */ @Setter private boolean textUriFlavorSupported = true; /** Does DataFlavor.stringFlavor transfer the files' full paths or filenames only ? */ private boolean stringFlavourTransfersFilename = false; /** Does DataFlavor.stringFlavor transfer the files' filenames with extension or without ? */ private boolean stringFlavourTransfersFileBaseName = false; /** DataFlavor used for GNOME/KDE transfers */ private static DataFlavor TEXT_URI_FLAVOR; /** Custom FileSet DataFlavor used for local transfers */ private static DataFlavor FILE_SET_DATA_FLAVOR; static { // create a single custom DataFlavor instance that designates the FileSet class to transfer data try { FILE_SET_DATA_FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType+";class="+FileSet.class.getName()); TEXT_URI_FLAVOR = new DataFlavor("text/uri-list;class="+String.class.getName()); } catch(ClassNotFoundException e) { // That should never happen LOGGER.debug("FileSet DataFlavor could not be instantiated", e); } } /** * Creates a new Transferable file set with support for all DataFlavors enabled. * * @param fileSet the files to be transferred */ public TransferableFileSet(FileSet fileSet) { this.fileSet = fileSet; } /** * Sets whether the FileSet DataFlavor (as returned by {@link #getFileSetDataFlavor()} * should be supported by this Transferable (supported by default). * * @param supported true to support the flavor */ public void setFileSetDataFlavorSupported(boolean supported) { this.fileSetFlavorSupported = supported; } /** * Sets whether the DataFlavor.javaFileListFlavor should be supported by this Transferable * (supported by default). * * @param supported true to support the flavor */ public void setJavaFileListDataFlavorSupported(boolean supported) { this.javaFileListFlavorSupported = supported; } /** * Sets whether the DataFlavor.stringFlavor should be supported by this Transferable * (supported by default). * * @param supported true to support the flavor */ public void setStringDataFlavorSupported(boolean supported) { this.stringFlavorSupported = supported; } /** * Sets whether the files' full path or just the filenames should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with DataFlavor.stringFlavor. * (*not* enabled by default) * * @param b if true, DataFlavor.stringFlavor returns filenames only, full file paths otherwise. */ public void setStringDataFlavourTransfersFilename(boolean b) { this.stringFlavourTransfersFilename = b; } /** * Returns whether the files' full path or just the filenames will be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with DataFlavor.stringFlavor. * Returns false unless {@link #setStringDataFlavourTransfersFilename(boolean)} has been called. * * @return whether the files' full path or just the filenames will be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with DataFlavor.stringFlavor */ public boolean getStringDataFlavourTransfersFilename() { return this.stringFlavourTransfersFilename; } /** * Sets whether the files' base name (without file extension) should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with DataFlavor.stringFlavor. * (*not* enabled by default) * * @param b if true, DataFlavor.stringFlavor returns filenames without extension, full file name otherwise. */ public void setStringDataFlavourTransfersFileBaseName(boolean b) { this.stringFlavourTransfersFileBaseName = b; } /** * Returns whether the files' base name (without file extension) should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with DataFlavor.stringFlavor. * Returns false unless {@link #setStringDataFlavourTransfersFileBaseName(boolean)} has been called. * * @return whether the files' base name (without file extension) should be returned when * {@link #getTransferData(java.awt.datatransfer.DataFlavor)} is called with DataFlavor.stringFlavor */ public boolean getStringDataFlavourTransfersFileBaseName() { return stringFlavourTransfersFileBaseName; } /** * Returns an instance of the custom FileSet DataFlavor used to transfer files locally. * * @return an instance of the custom FileSet DataFlavor used to transfer files locally */ static DataFlavor getFileSetDataFlavor() { return FILE_SET_DATA_FLAVOR; } /** * Returns the files contained by the specified Transferable as a {@link com.mucommander.commons.file.util.FileSet}, * or null if no file was present in the Transferable or if an error occurred. * *

    3 types of dropped data flavors are supported and used in this order of priority: *

      *
    • FileSet: the local DataFlavor used when files are transferred from muCommander *
    • File list: used when files are transferred from an external application *
    • File paths: alternate flavor used when some text representing one or several file paths is dragged * from an external application *
    * * @param transferable a Transferable instance that contains the files to be retrieved * @return the files contained by the specified Transferable as a FileSet, or null if no file * was present or if an error occurred */ static FileSet getTransferFiles(Transferable transferable) { FileSet files; AbstractFile file; try { // FileSet DataFlavor if (transferable.isDataFlavorSupported(FILE_SET_DATA_FLAVOR)) { files = (FileSet)transferable.getTransferData(FILE_SET_DATA_FLAVOR); } // File list DataFlavor else if(transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { @SuppressWarnings("unchecked") List fileList = (List)transferable.getTransferData(DataFlavor.javaFileListFlavor); files = new FileSet(); for (File aFileList : fileList) { file = FileFactory.getFile(aFileList.getAbsolutePath()); if (file != null) { files.add(file); } } } // Text plain DataFlavor: assume that lines designate file paths else if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) { try (BufferedReader br = new BufferedReader(DataFlavor.getTextPlainUnicodeFlavor().getReaderForText(transferable))) { // Read input line by line and try to create AbstractFile instances files = new FileSet(); String path; while( (path = br.readLine()) != null) { // Try to create an AbstractFile instance, returned instance may be null file = FileFactory.getFile(path); // Safety precaution: if at least one line doesn't resolve as a file, stop reading // and return null. This is to avoid any nasty effect that could arise if a random // piece of text (let's say an email contents) was inadvertently pasted or dropped to muCommander. if (file == null) { return null; } files.add(file); } } } else { return null; } } catch (Exception e) { // Catch UnsupportedFlavorException, IOException LOGGER.debug("Caught exception while processing transferable", e); return null; } return files; } @Override public DataFlavor[] getTransferDataFlavors() { List supportedDataFlavorsV = new ArrayList<>(); if(fileSetFlavorSupported) supportedDataFlavorsV.add(FILE_SET_DATA_FLAVOR); if(javaFileListFlavorSupported) supportedDataFlavorsV.add(DataFlavor.javaFileListFlavor); if(stringFlavorSupported) supportedDataFlavorsV.add(DataFlavor.stringFlavor); if(textUriFlavorSupported) supportedDataFlavorsV.add(TEXT_URI_FLAVOR); DataFlavor[] supportedDataFlavors = new DataFlavor[supportedDataFlavorsV.size()]; supportedDataFlavorsV.toArray(supportedDataFlavors); return supportedDataFlavors; } @Override public boolean isDataFlavorSupported(DataFlavor dataFlavor) { if(dataFlavor.equals(FILE_SET_DATA_FLAVOR)) return fileSetFlavorSupported; else if(dataFlavor.equals(DataFlavor.javaFileListFlavor)) return javaFileListFlavorSupported; else if(dataFlavor.equals(DataFlavor.stringFlavor)) return stringFlavorSupported; else if(dataFlavor.equals(TEXT_URI_FLAVOR)) return textUriFlavorSupported; return false; } @NotNull @Override public Object getTransferData(DataFlavor dataFlavor) throws UnsupportedFlavorException { int nbFiles = fileSet.size(); // Return files stored in a FileSet instance (the one that was passed to the constructor) if(dataFlavor.equals(FILE_SET_DATA_FLAVOR) && fileSetFlavorSupported) { return fileSet; } // Return files stored in a java.util.Vector instance else if(dataFlavor.equals(DataFlavor.javaFileListFlavor) && javaFileListFlavorSupported) { List fileList = new Vector<>(nbFiles); for(int i=0; i Drag and Drop API. ================================================ FILE: src/main/java/com/mucommander/ui/encoding/EncodingListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.encoding; /** * Interface to be implemented by classes that wish to be notified of character encoding selections that occurred * in an {@link EncodingMenu}. * * @author Maxence Bernard * @see EncodingMenu */ public interface EncodingListener { /** * Called when the currently selected encoding has changed. * * @param source component in which the event occurred * @param oldEncoding previously selected encoding * @param newEncoding newly selected encoding */ void encodingChanged(Object source, String oldEncoding, String newEncoding); } ================================================ FILE: src/main/java/com/mucommander/ui/encoding/EncodingMenu.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.encoding; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.DialogOwner; import ru.trolsoft.ui.TMenuSeparator; import ru.trolsoft.ui.TRadioButtonMenuItem; import javax.swing.*; import java.awt.*; import java.util.WeakHashMap; /** * This menu lets the user choose a character encoding among a list of {@link EncodingPreferences#getPreferredEncodings() * preferred encodings}. * The menu contains a checkbox menu item for each of the preferred encodings, and a special item that invokes a dialog * that allows the list of preferred encodings to be customized. * * @see EncodingPreferences * @author Maxence Bernard */ public class EncodingMenu extends JMenu { /** Contains all registered encoding listeners, stored as weak references */ protected final WeakHashMap listeners = new WeakHashMap<>(); /** the dialog/frame that owns this component */ private final DialogOwner dialogOwner; /** The encoding that is currently selected, may be null */ private String selectedEncoding; /** * Creates a new EncodingMenu with no encoding selected. * * @param dialogOwner the frame that owns this menu */ public EncodingMenu(DialogOwner dialogOwner) { this(dialogOwner, null); } /** * Creates a new EncodingMenu with the specified encoding initially selected (may be null). * If the encoding is not one of the preferred encodings, it is added as the first encoding in the menu. * * @param dialogOwner the frame that owns this menu * @param selectedEncoding the encoding initially selected, null for none */ public EncodingMenu(final DialogOwner dialogOwner, String selectedEncoding) { super(Translator.get("encoding")); this.dialogOwner = dialogOwner; this.selectedEncoding = selectedEncoding; populateMenu(); } /** * Adds a checkbox menu item for each of the preferred encodings, and a special item that invokes a dialog * that allows the list of preferred encodings to be customized. */ private void populateMenu() { java.util.List encodings = EncodingPreferences.getPreferredEncodings(); // Add the current encoding if it is not in the list of preferred encodings if (selectedEncoding != null && !encodings.contains(selectedEncoding)) { encodings.add(0, selectedEncoding); } addPreferredEncodings(encodings); add(new TMenuSeparator()); addCustomizeMenu(); } private void addPreferredEncodings(java.util.List encodings) { ButtonGroup group = new ButtonGroup(); for (String enc: encodings) { JMenuItem item = new TRadioButtonMenuItem(enc); // Select the current encoding, if there is one if (selectedEncoding != null && selectedEncoding.equals(enc)) { item.setSelected(true); } // Listen to checkbox actions item.addActionListener(e -> { String oldEncoding = selectedEncoding; selectedEncoding = ((JMenuItem)e.getSource()).getText(); if (!oldEncoding.equals(selectedEncoding)) { // Notify listeners of the new encoding fireEncodingListener(oldEncoding, EncodingMenu.this.selectedEncoding); } }); group.add(item); add(item); } } private void addCustomizeMenu() { // 'Customize' menu item JMenuItem customizeItem = new JMenuItem(Translator.get("customize") + "..."); customizeItem.addActionListener(e -> { Window owner = dialogOwner.getOwner(); if (owner instanceof Frame) { new PreferredEncodingsDialog((Frame) owner).showDialog(); } else { new PreferredEncodingsDialog((Dialog) owner).showDialog(); } removeAll(); populateMenu(); }); add(customizeItem); } /** * Returns the encoding that is currently selected, null if none is selected. * * @return the encoding that is currently selected, null if none is selected. */ public String getSelectedEncoding() { return selectedEncoding; } public void addEncodingListener(EncodingListener listener) { synchronized (listeners) { listeners.put(listener, null); } } public void removeEncodingListener(EncodingListener listener) { synchronized(listeners) { listeners.remove(listener); } } private void fireEncodingListener(String oldEncoding, String newEncoding) { synchronized(listeners) { for (EncodingListener listener : listeners.keySet()) { listener.encodingChanged(this, oldEncoding, newEncoding); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/encoding/EncodingPreferences.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.encoding; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import java.nio.charset.Charset; import java.util.List; import java.util.Vector; /** * This class allows to retrieve and set character encoding preferences. It is used by UI components that let the user * choose an encoding, thus limiting this list to a reasonable amount of choices instead of displaying all supported * character encodings. * * @see EncodingMenu * @see EncodingSelectBox * @author Maxence Bernard */ public class EncodingPreferences { /** Default list of preferred encodings, comma-separated. */ public static final String[] DEFAULT_PREFERRED_ENCODINGS = new String[] { "UTF-8", "UTF-16", "ISO-8859-1", "windows-1251", "windows-1252", "KOI8-R", "Big5", "GB18030", "EUC-KR", "Shift_JIS", "ISO-2022-JP", "EUC-JP", "cp866" }; /** * Returns a user-defined list of preferred encodings. * * @return a user-defined list of preferred encodings. */ public static List getPreferredEncodings() { List vector = TcConfigurations.getPreferences().getListVariable(TcPreference.PREFERRED_ENCODINGS, ","); if(vector==null) { vector = getDefaultPreferredEncodings(); TcConfigurations.getPreferences().setVariable(TcPreference.PREFERRED_ENCODINGS, vector, ","); } return vector; } /** * Returns a default list of preferred encodings, containing some of the most popular encodings. * * @return a default list of preferred encodings. */ public static List getDefaultPreferredEncodings() { List encodingsV = new Vector<>(); for (String encoding : DEFAULT_PREFERRED_ENCODINGS) { // Ensure that the encoding is supported before adding it if (Charset.isSupported(encoding)) encodingsV.add(encoding); } return encodingsV; } /** * Sets the user-defined list of preferred encodings. * * @param encodings the user-defined list of preferred encodings */ public static void setPreferredEncodings(List encodings) { TcConfigurations.getPreferences().setVariable(TcPreference.PREFERRED_ENCODINGS, encodings, ","); } } ================================================ FILE: src/main/java/com/mucommander/ui/encoding/EncodingSelectBox.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.encoding; import java.awt.BorderLayout; import java.awt.Dialog; import java.awt.Frame; import java.awt.Window; import java.util.List; import java.util.WeakHashMap; import javax.swing.JButton; import javax.swing.JPanel; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.combobox.SaneComboBox; import com.mucommander.ui.dialog.DialogOwner; /** * This compound component lets the user choose a character encoding among a list of {@link EncodingPreferences#getPreferredEncodings() * preferred encodings} using a combo box, and customize the list of preferred encodings when the 'customize' button * is pressed. * @author Maxence Bernard */ public class EncodingSelectBox extends JPanel { /** Allows the encoding to be selected */ protected SaneComboBox comboBox; /** Button that invokes the dialog that allows to customize the list of preferred encodings */ protected JButton customizeButton; /** Contains all registered encoding listeners, stored as weak references */ protected final WeakHashMap listeners = new WeakHashMap<>(); /** The encoding that is currently selected, may be null */ protected String currentEncoding; /** * Creates a new EncodingSelectBox with no specific encoding initially selected. * * @param dialogOwner the dialog/frame that owns this component */ public EncodingSelectBox(DialogOwner dialogOwner) { this(dialogOwner, null); } /** * Creates a new EncodingSelectBox with the specified encoding initially selected. * The encoding must be one of the preferred encodings, or null. In the latter case, the first encoding * will be selected. * * @param dialogOwner the dialog/frame that owns this component * @param selectedEncoding the encoding that will be initially selected, null for the first preferred * encoding */ public EncodingSelectBox(final DialogOwner dialogOwner, String selectedEncoding) { super(new BorderLayout()); comboBox = new SaneComboBox<>(); populateComboBox(selectedEncoding); comboBox.addActionListener(e -> { String oldEncoding = currentEncoding; currentEncoding = (String)comboBox.getSelectedItem(); if (currentEncoding == null || !currentEncoding.equals(oldEncoding)) { // Notify listeners of the new encoding fireEncodingListener(oldEncoding, EncodingSelectBox.this.currentEncoding); } }); add(comboBox, BorderLayout.CENTER); // Customize button customizeButton = new JButton("..."); // Mac OS X: small component size if (OsFamily.MAC_OS_X.isCurrent()) { customizeButton.putClientProperty("JComponent.sizeVariant", "small"); } customizeButton.addActionListener(e -> { String selectedEncoding1 = getSelectedEncoding(); Window owner = dialogOwner.getOwner(); if (owner instanceof Frame) { new PreferredEncodingsDialog((Frame) owner).showDialog(); } else { new PreferredEncodingsDialog((Dialog) owner).showDialog(); } comboBox.removeAllItems(); populateComboBox(selectedEncoding1); }); add(customizeButton, BorderLayout.EAST); } /** * Adds a checkbox menu item for each of the preferred encodings, and a special item that invokes a dialog * that allows the list of preferred encodings to be customized. * * @param selectEncoding the encoding that will be selected, null for the first one */ protected void populateComboBox(String selectEncoding) { List encodings = EncodingPreferences.getPreferredEncodings(); // Ignore the specified encoding if it is not in the list of preferred encodings if (selectEncoding != null && !encodings.contains(selectEncoding)) { selectEncoding = null; } // Add preferred encodings to the combo box for (String encoding : encodings) { comboBox.addItem(encoding); } if (selectEncoding != null) { comboBox.setSelectedItem(selectEncoding); } else if (!encodings.isEmpty()) { comboBox.setSelectedItem(encodings.get(0)); } currentEncoding = selectEncoding; } /** * Returns the encoding that is currently selected, null if none is selected. * * @return the encoding that is currently selected, null if none is selected. */ public String getSelectedEncoding() { int index = comboBox.getSelectedIndex(); return index < 0 ? null : comboBox.getItemAt(index); } public void addEncodingListener(EncodingListener listener) { synchronized(listeners) { listeners.put(listener, null); } } public void removeEncodingListener(EncodingListener listener) { synchronized(listeners) { listeners.remove(listener); } } protected void fireEncodingListener(String oldEncoding, String newEncoding) { synchronized(listeners) { for (EncodingListener listener : listeners.keySet()) listener.encodingChanged(this, oldEncoding, newEncoding); } } @Override public void setEnabled(boolean enabled) { comboBox.setEnabled(enabled); customizeButton.setEnabled(enabled); super.setEnabled(enabled); } } ================================================ FILE: src/main/java/com/mucommander/ui/encoding/PreferredEncodingsDialog.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.encoding; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dialog; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Vector; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.layout.YBoxPanel; /** * This dialog allows the list of preferred character encodings to be modified by the end user. Each of the supported * encodings are represented as a checkbox and can individually be selected/unselected. A 'revert to defaults' button * allows the {@link EncodingPreferences#getDefaultPreferredEncodings() default preferred encodings} to be used. * * @see EncodingPreferences * @author Maxence Bernard */ public class PreferredEncodingsDialog extends FocusDialog { /** Contains all the checkbox added to this dialog */ private List checkboxes; /** Minimum dimensions of this dialog */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(300, 0); /** Maximum dimensions of this dialog */ private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(550, 400); /** * Creates a new PreferredEncodingsDialog, without showing it on screen. * * @param owner the frame that invoked this dialog */ PreferredEncodingsDialog(Frame owner) { super(owner, i18n("preferred_encodings"), owner); init(); } /** * Creates a new PreferredEncodingsDialog, without showing it on screen. * * @param owner the dialog that invoked this dialog */ public PreferredEncodingsDialog(Dialog owner) { super(owner, i18n("preferred_encodings"), owner); init(); } protected void init() { // Mac OS X: small window borders if (OsFamily.MAC_OS_X.isCurrent()) { getRootPane().putClientProperty("Window.style", "small"); } Container contentPane = getContentPane(); // Label JLabel label = new JLabel(i18n("preferred_encodings")+":"); // Mac OS X: small component size if (OsFamily.MAC_OS_X.isCurrent()) { label.putClientProperty("JComponent.sizeVariant", "small"); } contentPane.add(label, BorderLayout.NORTH); // Checkboxes YBoxPanel yPanel = new YBoxPanel(); checkboxes = new Vector<>(); for (String enc : Charset.availableCharsets().keySet()) { JCheckBox checkbox = new JCheckBox(enc); // Mac OS X: component size if (OsFamily.MAC_OS_X.isCurrent()) { checkbox.putClientProperty("JComponent.sizeVariant", "small"); } checkboxes.add(checkbox); yPanel.add(checkbox); } selectCheckboxes(EncodingPreferences.getPreferredEncodings()); JScrollPane scrollPane = new JScrollPane(yPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); contentPane.add(scrollPane, BorderLayout.CENTER); // 'Revert to defaults' button JButton defaultsButton = new JButton(i18n("reset")); // Mac OS X: component size if (OsFamily.MAC_OS_X.isCurrent()) { defaultsButton.putClientProperty("JComponent.sizeVariant", "small"); } defaultsButton.addActionListener(e -> selectCheckboxes(EncodingPreferences.getDefaultPreferredEncodings())); JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING)); flowPanel.add(defaultsButton); contentPane.add(flowPanel, BorderLayout.SOUTH); setMinimumSize(MINIMUM_DIALOG_DIMENSION); setMaximumSize(MAXIMUM_DIALOG_DIMENSION); addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { List preferredEncodings = new ArrayList<>(); for (JCheckBox checkbox : checkboxes) { if (checkbox.isSelected()) preferredEncodings.add(checkbox.getText()); } EncodingPreferences.setPreferredEncodings(preferredEncodings); } }); } /** * Selects all the checkboxes which correspond to an encoding that is present in the given vector. * * @param selectedEncodings list of encodings to select */ private void selectCheckboxes(List selectedEncodings) { for (JCheckBox checkbox : checkboxes) { checkbox.setSelected(selectedEncodings.contains(checkbox.getText())); } } } ================================================ FILE: src/main/java/com/mucommander/ui/event/ActivePanelListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.event; import com.mucommander.ui.main.FolderPanel; /** * Interface to be implemented by classes that wish to be notified of active table changes on a particular MainFrame. * Those classes need to be registered to receive those events, this can be done by calling * {@link com.mucommander.ui.main.MainFrame#addActivePanelListener(ActivePanelListener)}. * * @see com.mucommander.ui.main.MainFrame * @author Maxence Bernard */ public interface ActivePanelListener { /** * This method is invoked when the currently active (i.e. that has focus) folder panel has changed on the MainFrame. * * @param folderPanel the new active FolderPanel. */ void activePanelChanged(FolderPanel folderPanel); } ================================================ FILE: src/main/java/com/mucommander/ui/event/LocationAdapter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.event; /** * An abstract adapter class for receiving location events. * The methods in this class are empty. This class exists as * convenience for creating listener objects. * * @author Arik Hadas */ public abstract class LocationAdapter implements LocationListener { /** * {@inheritDoc} */ public void locationChanging(LocationEvent locationEvent) { } /** * {@inheritDoc} */ public void locationChanged(LocationEvent locationEvent) { } /** * {@inheritDoc} */ public void locationCancelled(LocationEvent locationEvent) { } /** * {@inheritDoc} */ public void locationFailed(LocationEvent locationEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/event/LocationEvent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.event; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.main.FolderPanel; /** * Event used to indicate that a folder change is or has occurred. This event is passed to to every LocationListener * that registered to receive those events on a particular FolderPanel. * * @author Maxence Bernard */ public class LocationEvent { /** FolderPanel where location has or is being changed */ private final FolderPanel folderPanel; /** URL of the folder that has or is being changed */ private final FileURL folderURL; /** * Creates a new LocationEvent. * * @param folderPanel FolderPanel where location has or is being changed. * @param folderURL url of the folder that has or is being changed */ public LocationEvent(FolderPanel folderPanel, FileURL folderURL) { this.folderPanel = folderPanel; this.folderURL = folderURL; } /** * Returns the FolderPanel instance where location has or is being changed. */ public FolderPanel getFolderPanel() { return folderPanel; } /** * Returns the URL to the folder that has or is being changed. */ public FileURL getFolderURL() { return folderURL; } } ================================================ FILE: src/main/java/com/mucommander/ui/event/LocationListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.event; /** * Interface to be implemented by classes that wish to be notified of location changes on a particular * FolderPanel. Those classes need to be registered to receive those events, this can be done by calling * {@link LocationManager#addLocationListener(LocationListener)}. * * @see com.mucommander.ui.main.FolderPanel * @author Maxence Bernard */ public interface LocationListener { /** * This method is invoked when the current folder is being changed. * *

    A call to either {@link #locationChanged(LocationEvent)}, {@link #locationCancelled(LocationEvent)} or * {@link #locationFailed(LocationEvent)} will always follow to indicate the outcome of the folder change. * * @param locationEvent describes the location change event */ void locationChanging(LocationEvent locationEvent); /** * This method is invoked when the current folder has changed. * * @param locationEvent describes the location change event */ void locationChanged(LocationEvent locationEvent); /** * This method is invoked when the current folder has been cancelled by the user. * * @param locationEvent describes the location change event */ void locationCancelled(LocationEvent locationEvent); /** * This method is invoked when the current folder could not be changed, as a result * of the folder not existing or failing to list its contents. * * @param locationEvent describes the location change event */ void locationFailed(LocationEvent locationEvent); } ================================================ FILE: src/main/java/com/mucommander/ui/event/LocationManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.event; import java.util.Map; import java.util.WeakHashMap; import com.mucommander.commons.file.filter.FileFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.core.FolderChangeMonitor; import com.mucommander.core.GlobalLocationHistory; import com.mucommander.ui.main.ConfigurableFolderFilter; import com.mucommander.ui.main.FolderPanel; /** * @author Maxence Bernard */ public class LocationManager { private static final Logger LOGGER = LoggerFactory.getLogger(LocationManager.class); /** Contains all registered location listeners, stored as weak references */ private final Map locationListeners = new WeakHashMap<>(); /** The FolderPanel instance this LocationManager manages location events for */ private final FolderPanel folderPanel; /** Current location presented in the FolderPanel */ private AbstractFile currentFolder; /** Filters out unwanted files when listing folder contents */ private final ConfigurableFolderFilter configurableFolderFilter = new ConfigurableFolderFilter(); private FolderChangeMonitor folderChangeMonitor; /** * Creates a new LocationManager that manages location events listeners and broadcasts for the specified FolderPanel. * * @param folderPanel the FolderPanel instance this LocationManager manages location events for */ public LocationManager(FolderPanel folderPanel) { this.folderPanel = folderPanel; addLocationListener(GlobalLocationHistory.getInstance()); } /** * Set the given {@link AbstractFile} as the folder presented in the {@link FolderPanel}. * This method saves the given {@link AbstractFile}, and notify the {@link LocationListener}s that * the location was changed to it. * * @param folder the {@link AbstractFile} that is going to be presented in the {@link FolderPanel} */ public void setCurrentFolder(AbstractFile folder, AbstractFile fileToSelect, boolean changeLockedTab) { AbstractFile[] children = safeLs(folder, configurableFolderFilter); folderPanel.setCurrentFolder(folder, children, fileToSelect, changeLockedTab); this.currentFolder = folder; // Notify listeners that the location has changed fireLocationChanged(folder.getURL()); // After the initial folder is set, initialize the monitoring thread if (folderChangeMonitor == null) { folderChangeMonitor = new FolderChangeMonitor(folderPanel); } } private static AbstractFile[] safeLs(AbstractFile folder, FileFilter filter) { LOGGER.trace("calling ls()"); try { return folder.ls(filter); } catch (Exception e) { LOGGER.error("Couldn't ls children of " + folder.getAbsolutePath() + ", error: " + e.getMessage()); return new AbstractFile[0]; } } /** * Return the folder presented in the {@link FolderPanel} * * @return the {@link AbstractFile} presented in the {@link FolderPanel} */ public AbstractFile getCurrentFolder() { return currentFolder; } public FolderChangeMonitor getFolderChangeMonitor() { return folderChangeMonitor; } /** * Registers a LocationListener to receive notifications whenever the current folder of the associated FolderPanel * has or is being changed. * *

    Listeners are stored as weak references so {@link #removeLocationListener(LocationListener)} * doesn't need to be called for listeners to be garbage collected when they're not used anymore. * * @param listener the LocationListener to register */ public synchronized void addLocationListener(LocationListener listener) { locationListeners.put(listener, null); } /** * Removes the LocationListener from the list of listeners that receive notifications when the current folder of the * associated FolderPanel has or is being changed. * * @param listener the LocationListener to remove */ public synchronized void removeLocationListener(LocationListener listener) { locationListeners.remove(listener); } /** * Notifies all registered listeners that the current folder has changed on associated FolderPanel. * * @param folderURL url of the new current folder in the associated FolderPanel */ private synchronized void fireLocationChanged(FileURL folderURL) { for (LocationListener listener : locationListeners.keySet()) { listener.locationChanged(new LocationEvent(folderPanel, folderURL)); } } /** * Notifies all registered listeners that the current folder is being changed on the associated FolderPanel. * * @param folderURL url of the folder that will become the new location if the folder change is successful */ public synchronized void fireLocationChanging(FileURL folderURL) { for (LocationListener listener : locationListeners.keySet()) { listener.locationChanging(new LocationEvent(folderPanel, folderURL)); } } /** * Notifies all registered listeners that the folder change as notified by {@link #fireLocationChanging(FileURL)} * has been cancelled by the user. * * @param folderURL url of the folder for which a failed attempt was made to make it the current folder */ public synchronized void fireLocationCancelled(FileURL folderURL) { for (LocationListener listener : locationListeners.keySet()) { listener.locationCancelled(new LocationEvent(folderPanel, folderURL)); } } /** * Notifies all registered listeners that the folder change as notified by {@link #fireLocationChanging(FileURL)} * could not be changed, as a result of the folder not existing or failing to list its contents. * * @param folderURL url of the folder for which a failed attempt was made to make it the current folder */ public synchronized void fireLocationFailed(FileURL folderURL) { for (LocationListener listener : locationListeners.keySet()) { listener.locationFailed(new LocationEvent(folderPanel, folderURL)); } } } ================================================ FILE: src/main/java/com/mucommander/ui/event/TableSelectionListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.event; import com.mucommander.ui.main.table.FileTable; /** * Interface to be implemented by classes that wish to be notified of selection changes on a particular * FileTable. Those classes need to be registered to receive those events, this can be done by calling * {@link com.mucommander.ui.main.table.FileTable#addTableSelectionListener(TableSelectionListener) FileTable.addTableSelectionListener()}. * * @see com.mucommander.ui.main.table.FileTable * @author Maxence Bernard */ public interface TableSelectionListener { /** * This method is invoked when the selected file has changed on the specified FileTable . * * @param source the {@link com.mucommander.ui.main.table.FileTable} instance on which the file selection has changed */ void selectedFileChanged(FileTable source); /** * This method is invoked when the files marked have changed on the specified FileTable. * * @param source the {@link com.mucommander.ui.main.table.FileTable} instance on which the files marked have changed */ void markedFilesChanged(FileTable source); } ================================================ FILE: src/main/java/com/mucommander/ui/helper/FocusRequester.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.helper; import java.awt.*; import javax.swing.*; import com.mucommander.ui.PreloadedJFrame; import com.mucommander.ui.main.MainFrame; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The sole purpose of this class is to provide a way to request focus on a component after all currently queued * Swing events have been processed. This is useful for components that are not eligible to receive focus at the time * they request it, for instance when they are not visible yet. * * @author Maxence Bernard */ public class FocusRequester implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(FocusRequester.class); /** The component on which to request focus */ private Component component; /** If true, focus will be requested using Component#requestFocusInWindow() instead of Component#requestFocus() */ private final boolean requestFocusInWindow; private FocusRequester(Component c, boolean requestFocusInWindow) { this.component = c; this.requestFocusInWindow = requestFocusInWindow; } /** * Requests focus on the given component using {@link java.awt.Component#requestFocus()}, after all currently queued * Swing events have been processed. * *

    This method can typically be used when a component has been added to the screen but is not yet visible. * In that case, calling {@link Component#requestFocus()} would have no effect. * * @param c the component on which to request focus * @see java.awt.Component#requestFocus() */ public static synchronized void requestFocus(Component c) { if (c == null) { LOGGER.debug(">>>>>>>>>>>>>>>>> Component is null, returning!"); return; } SwingUtilities.invokeLater(new FocusRequester(c, false)); } /** * Requests focus on the given component, after all currently queued Swing events have been processed. * *

    This method can typically be used when a component has been added to the screen but is not yet visible. * In that case, calling {@link java.awt.Component#requestFocusInWindow()} would have no effect. * * @param c the component on which to request focus * @see java.awt.Component#requestFocusInWindow() */ public static synchronized void requestFocusInWindow(Component c) { if (c == null) { LOGGER.debug(">>>>>>>>>>>>>>>>>> Component is null, returning!"); return; } SwingUtilities.invokeLater(new FocusRequester(c, true)); } @Override public void run() { // Request focus on the component if (requestFocusInWindow) { component.requestFocusInWindow(); } else { component.requestFocus(); } if (component instanceof Frame f) { f.toFront(); } if (component instanceof PreloadedJFrame preloadedJFrame) { var frame = preloadedJFrame.getMainFrameObject(); if (frame instanceof MainFrame mainFrame) { mainFrame.getActiveTable().requestFocus(); } } LOGGER.debug("focus requested on {}", (component.getClass().getName())); this.component = null; } } ================================================ FILE: src/main/java/com/mucommander/ui/helper/MenuToolkit.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.helper; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import javax.swing.*; import javax.swing.event.MenuListener; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.menu.JScrollMenu; import ru.trolsoft.ui.TCheckBoxMenuItem; import ru.trolsoft.ui.TRadioButtonMenuItem; /** * MenuToolkit provides convenient methods that make life easier * when creating menus. * * @author Maxence Bernard */ public class MenuToolkit { private static final int TYPE_ITEM = 0; private static final int TYPE_CHECKBOX = 1; private static final int TYPE_RADIOBUTTON = 2; private MenuToolkit() { } /** * Creates and returns a new JMenu. * * @param title title of the menu * @param mnemonicHelper an optional (can be null) mnemonic helper which will be used along with * the title to set a mnemonic to the menu. * @param menuListener an optional (can be null) menu listener which will listen to the events triggered by the menu. */ public static JMenu addMenu(String title, MnemonicHelper mnemonicHelper, MenuListener menuListener) { JMenu menu = new JMenu(title); initMenu(menu, title, mnemonicHelper, menuListener); return menu; } /** * Creates and returns a new JScrollMenu. * * @param title title of the menu * @param mnemonicHelper an optional (can be null) mnemonic helper which will be used along with * the title to set a mnemonic to the menu. * @param menuListener an optional (can be null) menu listener which will listen to the events triggered by the menu. */ public static JScrollMenu addScrollableMenu(String title, MnemonicHelper mnemonicHelper, MenuListener menuListener) { final JScrollMenu menu = new JScrollMenu(title); initMenu(menu, title, mnemonicHelper, menuListener); return menu; } private static void initMenu(JMenu menu, String title, MnemonicHelper mnemonicHelper, MenuListener menuListener) { setupMnemonic(title, mnemonicHelper, menu); if (menuListener != null) { menu.addMenuListener(menuListener); } } /** * Creates a new JMenuItem and adds it to the given JMenu. * * @param menu menu to add the menu item to. * @param text text used by the menu item. * @param mnemonicHelper an optional (can be null) mnemonic helper which will be used along with * the item's text to set a mnemonic to the menu. * @param accelerator an optional (can be null) keyboard shortcut used by the menu item. * @param actionListener an optional (can be null) action listener which will listen to the events triggered by the menu item. */ public static JMenuItem addMenuItem(JMenu menu, String text, MnemonicHelper mnemonicHelper, KeyStroke accelerator, ActionListener actionListener) { return addMenuItem(menu, text, mnemonicHelper, accelerator, actionListener, TYPE_ITEM); } /** * Creates a new JCheckBoxMenuItem initially unselected and adds it to the given JMenu. * * @param menu menu to add the menu item to. * @param text text used by the menu item. * @param mnemonicHelper an optional (can be null) mnemonic helper which will be used along with * the item's text to set a mnemonic to the menu. * @param accelerator an optional (can be null) keyboard shortcut used by the menu item. * @param actionListener an optional (can be null) action listener which will listen to the events triggered by the menu item. */ public static JCheckBoxMenuItem addCheckBoxMenuItem(JMenu menu, String text, MnemonicHelper mnemonicHelper, KeyStroke accelerator, ActionListener actionListener) { return (JCheckBoxMenuItem) addMenuItem(menu, text, mnemonicHelper, accelerator, actionListener, TYPE_CHECKBOX); } /** * Creates a new JRadioButtonMenuItem initially unselected and adds it to the given JMenu. * * @param menu menu to add the menu item to. * @param text text used by the menu item. * @param mnemonicHelper an optional (can be null) mnemonic helper which will be used along with * the item's text to set a mnemonic to the menu. * @param accelerator an optional (can be null) keyboard shortcut used by the menu item. * @param actionListener an optional (can be null) action listener which will listen to the events triggered by the menu item. */ public static JRadioButtonMenuItem addRadioButtonMenuItem(JMenu menu, String text, MnemonicHelper mnemonicHelper, KeyStroke accelerator, ActionListener actionListener, ButtonGroup group) { JRadioButtonMenuItem item = (JRadioButtonMenuItem) addMenuItem(menu, text, mnemonicHelper, accelerator, actionListener, TYPE_RADIOBUTTON); if (group != null) { group.add(item); } return item; } /** * Creates a new JMenuItem or JCheckBoxMenuItem and adds it to the given JMenu. * * @param menu menu to add the menu item to. * @param text text used by the menu item. * @param mnemonicHelper an optional (can be null) mnemonic helper which will be used along with * the item's text to set a mnemonic to the menu. * @param accelerator an optional (can be null) keyboard shortcut used by the menu item. * @param actionListener an optional (can be null) action listener which will listen to the events triggered by the menu item. * @param menuType specifies whether the menu item to be created is a JCheckBoxMenuItem, JRadioButtonMenuItem or just a regular JMenuItem. */ private static JMenuItem addMenuItem(JMenu menu, String text, MnemonicHelper mnemonicHelper, KeyStroke accelerator, ActionListener actionListener, int menuType) { final JMenuItem menuItem = construct(menuType, text); setupMnemonic(text, mnemonicHelper, menuItem); if (accelerator != null) { menuItem.setAccelerator(accelerator); } if (actionListener != null) { menuItem.addActionListener(actionListener); } menu.add(menuItem); return menuItem; } private static void setupMnemonic(String text, MnemonicHelper mnemonicHelper, JMenuItem menuItem) { if (mnemonicHelper != null) { char mnemonic = mnemonicHelper.getMnemonic(text); if (mnemonic != 0) { menuItem.setMnemonic(mnemonic); } } } /** * Does things that should be done to all menu items created from * MuActions. *

      *
    1. If the provided action has an icon, it would by default get displayed in the menu item. * Since icons have nothing to do in menus, let's make sure the menu item has no icon.
    2. *
    3. If the action has a keyboard shortcut that conflicts with the menu's internal ones * (enter, space and escape), they will not be used.
    4. *
    * * @param item menu item to take care of. */ public static void configureActionMenuItem(JMenuItem item) { item.setIcon(null); if (isInvalidAccelerator(item.getAccelerator())) { item.setAccelerator(null); } } private static boolean isInvalidAccelerator(KeyStroke stroke) { return stroke != null && stroke.getModifiers() == 0 && (stroke.getKeyCode() == KeyEvent.VK_ENTER || stroke.getKeyCode() == KeyEvent.VK_SPACE || stroke.getKeyCode() == KeyEvent.VK_ESCAPE); } public static JMenuItem addMenuItem(JMenu menu, TcAction action, MnemonicHelper mnemonicHelper) { return addMenuItem(menu, action, mnemonicHelper, TYPE_ITEM); } public static JCheckBoxMenuItem addCheckBoxMenuItem(JMenu menu, TcAction action, MnemonicHelper mnemonicHelper) { return (JCheckBoxMenuItem) addMenuItem(menu, action, mnemonicHelper, TYPE_CHECKBOX); } public static JRadioButtonMenuItem addRadioButtonMenuItem(JMenu menu, TcAction action, MnemonicHelper mnemonicHelper) { return (JRadioButtonMenuItem) addMenuItem(menu, action, mnemonicHelper, TYPE_RADIOBUTTON); } private static JMenuItem addMenuItem(JMenu menu, TcAction action, MnemonicHelper mnemonicHelper, int menuType) { final JMenuItem menuItem = construct(menuType, action); if (mnemonicHelper != null && action != null) { char mnemonic = mnemonicHelper.getMnemonic(action.getLabel()); if (mnemonic != 0) { menuItem.setMnemonic(mnemonic); } } // If the provided action has an icon, it would by default get displayed in the menu item. // Since icons have nothing to do in menus, let's make sure the menu item has no icon. menuItem.setIcon(null); menu.add(menuItem); return menuItem; } private static JMenuItem construct(int type, String text) { switch (type) { case TYPE_CHECKBOX: return new TCheckBoxMenuItem(text); case TYPE_RADIOBUTTON: return new TRadioButtonMenuItem(text); default: return new JMenuItem(text); } } private static JMenuItem construct(int type, Action action) { switch (type) { case TYPE_CHECKBOX: return new TCheckBoxMenuItem(action); case TYPE_RADIOBUTTON: return new TRadioButtonMenuItem(action); default: return new JMenuItem(action); } } } ================================================ FILE: src/main/java/com/mucommander/ui/helper/MnemonicHelper.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.helper; import javax.swing.*; import java.util.ArrayList; import java.util.List; /** * MnemonicHelper provides a way to easily set mnemonics to UI components, without having to bother * with remembering which ones have already been assigned to another component. * *

    To use it: simply create a new instance and keep calling {@link #getMnemonic(String)} * to get mnemonics from the giving pieces of text. * * @author Maxence Bernard */ public class MnemonicHelper { /** Current list of previously assigned mnemonics */ private final List takenMnemonics; /** * Creates a new blank MnemonicHelper. */ public MnemonicHelper() { takenMnemonics = new ArrayList<>(); } /** * Finds and returns first character in the given string that's not already as a mnemonic. * *

    Returned mnemonic will be added to current internal list of taken mnemonics * and won't ever be used again by this instance. * * @return the character to be used as a mnemonic, always in lower case, 0 if no * mnemonic was available for this piece of text. 0 is returned if a null string is passed. * @param text text to get a mnemonic from. */ public char getMnemonic(String text) { // Returns 0 in case of null string if (text == null || text.isEmpty()) { return 0; } // Find first letter available for mnemonic (keyboard shortcut) int mnemonicPos = 0; text = text.toLowerCase(); int textLength = text.length(); do { char mnemonic = text.charAt(mnemonicPos++); if (!isMnemonicUsed(mnemonic)) { takenMnemonics.add(mnemonic); return mnemonic; } } while (mnemonicPos < textLength); return 0; } /** * Convenience method that returns a mnemonic for the specified button. * Yields to the same result as if {@link #getMnemonic(String)} were called with JButton.getText(). * * @param button the button to get a mnemonic for * @return the character to be used as a mnemonic, always in lower case, 0 if no * mnemonic was available for this piece of text. 0 is returned if a null string is passed. */ public char getMnemonic(JButton button) { return getMnemonic(button.getText()); } /** * Returns true if the specified character has already been previously * used as a mnemonic, returned by {@link #getMnemonic(String)}. * * @param ch the character which will be tested for an existing mnemonic. * @return whether the character is already used in the mnemonics array. */ private boolean isMnemonicUsed(char ch) { return takenMnemonics.contains(ch); } /** * Clears any previously registered mnemonics by {@link #getMnemonic(String)}. */ public void clear() { takenMnemonics.clear(); } } ================================================ FILE: src/main/java/com/mucommander/ui/helper/ScreenServices.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.helper; import com.mucommander.conf.TcSnapshot; import java.awt.Dimension; import java.awt.Frame; import java.awt.Insets; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.Window; /** * This class offers screen related services * * @author Arik Hadas */ public class ScreenServices { /** * Computes the screen's insets for the specified window and returns them. *

    * While this might seem strange, screen insets can change from one window * to another. For example, on X11 windowing systems, there is no guarantee that * a window will be displayed on the same screen, let alone computer, as the one * the application is running on. * * @param window the window for which screen insets should be computed. * @return the screen's insets for the specified window */ public static Insets getScreenInsets(Window window) { return Toolkit.getDefaultToolkit().getScreenInsets(window.getGraphicsConfiguration()); } /** * Checks whether the specified frame can be moved to the specified coordinates and still * be fully visible. *

    * If x (resp. y) is null, this method won't test * whether the frame is within horizontal (resp. vertical) bounds. * * @param frame frame who's visibility should be tested. * @param x horizontal coordinate of the upper-leftmost corner of the area to check for. * @param y vertical coordinate of the upper-leftmost corner of the area to check for. * @return true if the frame can be moved at the specified location, * false otherwise. */ public static boolean isInsideUsableScreen(Frame frame, int x, int y) { Insets screenInsets = getScreenInsets(frame); Dimension screenSize = TcSnapshot.getScreenSize(); return (x < 0 || (x >= screenInsets.left && x < screenSize.width - screenInsets.right)) && (y < 0 || (y >= screenInsets.top && y < screenSize.height - screenInsets.bottom)); } /** * Returns the maximum dimensions for a full-screen window. * * @param window window who's full screen size should be computed. * @return the maximum dimensions for a full-screen window */ public static Rectangle getFullScreenBounds(Window window) { Toolkit toolkit = Toolkit.getDefaultToolkit(); Dimension screenSize = toolkit.getScreenSize(); Insets screenInsets = toolkit.getScreenInsets(window.getGraphicsConfiguration()); return new Rectangle(screenInsets.left, screenInsets.top, screenSize.width-screenInsets.left-screenInsets.right, screenSize.height-screenInsets.top-screenInsets.bottom); } } ================================================ FILE: src/main/java/com/mucommander/ui/helper/package.html ================================================ Various Swing helper classes. ================================================ FILE: src/main/java/com/mucommander/ui/icon/AnimatedIcon.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.icon; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.Set; /** * javax.swing.Icon implementation that manages animation. *

    * This heavily borrows code from Technomage's furbelow package, distributed * under the GNU Lesser General Public License.
    * The original source code can be found here. * * @author twall, Nicolas Rinaudo */ public abstract class AnimatedIcon implements Icon, AutoCloseable { /** Default number of frames per animation. */ public static final int DEFAULT_FRAME_COUNT = 8; /** Default number of milliseconds between each frame. */ public static final int DEFAULT_FRAME_DELAY = 1000 / DEFAULT_FRAME_COUNT; /** All tracked components. */ private final Set components = new HashSet<>(); /** Timer used to take the animation from one frame to the next. */ private final Timer timer; /** Index of the current frame. */ private int currentFrame; /** Total number of frames in the animation. */ private int frameCount; /** Whether the animation should be running. */ private boolean animate; private boolean disposed = false; /** * Creates a new animated icon. *

    * This is a convenience constructor and is strictly equivalent to calling * {@link #AnimatedIcon(int,int)}({@link #DEFAULT_FRAME_COUNT}, {@link #DEFAULT_FRAME_DELAY}); */ public AnimatedIcon() { this(DEFAULT_FRAME_COUNT, DEFAULT_FRAME_DELAY); } /** * Creates a new animated icon with the specified number of frames. *

    * This is a convenience constructor and is strictly equivalent to calling * {@link #AnimatedIcon(int,int)}(frameCount, {@link #DEFAULT_FRAME_DELAY}); * * @param frameCount number of frames in the animation. */ public AnimatedIcon(int frameCount) { this(frameCount, DEFAULT_FRAME_DELAY); } /** * Creates a new animated icon with the specified number of frames and repaint delay. * @param frameCount number of frames in the animation. * @param repaintDelay number of milliseconds to sleep between each frame. */ public AnimatedIcon(int frameCount, int repaintDelay) { // Initializes the animation timer. timer = new Timer(repaintDelay, new AnimationUpdater(this)); timer.setRepeats(true); // Initializes frame control. setFrameCount(frameCount); setFrameDelay(repaintDelay); } /** * Returns the icon's width. * @return the icon's width. */ public abstract int getIconWidth(); /** * Returns the icon's height. * @return the icon's height. */ public abstract int getIconHeight(); /** * Paints the current frame. * @param c component in which the frame is being painted. * @param g graphics in which to paint the frame. * @param x horizontal coordinate at which to paint the frame. * @param y vertical coordinate at which to paint the frame. */ protected abstract void paintFrame(Component c, Graphics g, int x, int y); /** * Sets the total number of frames in the animation. * @param count total number of frames in the animation. */ public synchronized void setFrameCount(int count) { this.frameCount = count; } /** * Returns the total number of frames in the animation. * @return the total number of frames in the animation. */ public synchronized int getFrameCount() { return frameCount; } /** * Returns the index of the current frame in the animation. * @return the index of the current frame in the animation. */ public synchronized int getFrame() { return currentFrame; } /** * Sets the index of the current frame in the animation. *

    * If the method does actually change the current frame, it will trigger a repaint. * * @param frame index of the current frame in the animation. */ public synchronized void setFrame(int frame) { if (frame != currentFrame) { if (frame == 0) { currentFrame = 0; } else { currentFrame = frame % frameCount; } repaint(); } } /** * Takes the animation to its next frame. *

    * This is a convenience method and is strictly equivalent to calling * {@link #setFrame(int) setFrame}({@link #getFrame() getFrame()} + 1). * */ public synchronized void nextFrame() {setFrame(currentFrame + 1);} /** * Sets the number of milliseconds the animation will sleep between each frame. *

    * If set to 0, the animation will stop. * * @param delay number of milliseconds the animation will sleep between each frame. */ public synchronized void setFrameDelay(int delay) {timer.setDelay(delay);} /** * Starts / stops the animation. * @param a whether the animation should be started or stopped. */ public synchronized void setAnimated(boolean a) { // Starts the animation if necessary. if (a) { if (!timer.isRunning()) { timer.restart(); } } // Stops the animation if necessary. else if (timer.isRunning()) { timer.stop(); } animate = a; } /** * Returns true if the animation is currently running. *

    * Note that this method will return true if the animation is meant to be running, * for example if the icon is not visible but would be animated if it was. * * @return true if the animation is currently running, false. */ public synchronized boolean isAnimated() {return animate;} /** * Returns the number of milliseconds the animation will sleep between each frame. * @return the number of milliseconds the animation will sleep between each frame. */ public synchronized int getFrameDelay() {return timer.getDelay();} /** * Paints the icon's current frame. * @param c component in which to paint the icon. * @param g graphic context in which to paint the icon. * @param x horizontal coordinate at which to paint the icon. * @param y vertical coordinate at which to paint the icon. */ public synchronized void paintIcon(Component c, Graphics g, int x, int y) { // Paints the current frame. paintFrame(c, g, x, y); // Stores the component and starts / restarts the timer if necessary. if (c != null) { AffineTransform transform; if (g instanceof Graphics2D g2d) { transform = g2d.getTransform(); } else { transform = new AffineTransform(); } components.add(new TrackedComponent(c, x, y, (int)(getIconWidth() * transform.getScaleX()), (int)(getIconHeight() * transform.getScaleY()))); // Restarts the timer if necessary. if (!timer.isRunning() && animate) { timer.restart(); } } } /** * Forces the icon to repaint. */ protected synchronized void repaint() { // If the component list is empty, we can stop the timer. if (components.isEmpty()) { timer.stop(); } else { // Repaints all pending components. for (TrackedComponent comp : components) { comp.repaint(); } components.clear(); } } @Override public void close() throws Exception { if (!disposed) { timer.stop(); components.clear(); disposed = true; } } /** * Used to keep track of the various components in which an animated icon is being painted. * @author twall, Nicolas Rinaudo */ private static class TrackedComponent { /** Component in which the icon must be painted. */ private final Component component; /** Horizontal coordinate at which the icon should be painted. */ private final int x; /** Vertical coordinate at which the icon should be painted. */ private final int y; /** Width of the icon (used for clipping). */ private final int width; /** Height of the icon (used for clipping). */ private final int height; /** Component's hashcode. */ private final int hashCode; /** * Creates a new tracked component. * @param c component in which to paint the icon. * @param x horizontal coordinate at which to paint the icon. * @param y vertical coordinate at which to paint the icon. * @param width width of the icon. * @param height height of the icon. */ public TrackedComponent(Component c, int x, int y, int width, int height) { Component ancestor = findNonRendererAncestor(c); // Identifies the component that displays the icon. if (ancestor != c) { Point pt = SwingUtilities.convertPoint(c, x, y, ancestor); c = ancestor; x = pt.x; y = pt.y; } // Stores all the necessary information and computes the tracked component's hashcode. component = c; this.x = x; this.y = y; this.width = width; this.height = height; int code = x; code = 31 * code + y; code = 31 * code + System.identityHashCode(c); this.hashCode = code; } public int hashCode() { return hashCode; } /** * Finds the specified component's first non-renderer ancestor. * @param c component whose ancestors should be explored. */ private Component findNonRendererAncestor(Component c) { Component ancestor = SwingUtilities.getAncestorOfClass(CellRendererPane.class, c); if (ancestor != null && ancestor != c && ancestor.getParent() != null) c = findNonRendererAncestor(ancestor.getParent()); return c; } /** * Forces the tracked component to repaint the animated icon. */ public void repaint() { component.repaint(x, y, width, height); } } /** * Receives timer events and notifies the icon. * @author twall, Nicolas Rinaudo */ private static class AnimationUpdater implements ActionListener { /** Weak reference to the animation. */ private final WeakReference icon; /** * Creates a new animation updater on the specified icon. * @param icon animation to update. */ public AnimationUpdater(AnimatedIcon icon) { this.icon = new WeakReference<>(icon); } /** * Notifies the icon that it should update. * @param event ignored. */ public void actionPerformed(ActionEvent event) { AnimatedIcon i = icon.get(); // Makes sure the animation hasn't been garbage collected. if (i != null) { i.nextFrame(); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/icon/CustomFileIconProvider.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.icon; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.icon.FileIconProvider; import javax.swing.Icon; import javax.swing.ImageIcon; import java.awt.Dimension; import java.util.HashMap; import java.util.Map; /** * This {@link com.mucommander.commons.file.icon.FileIconProvider} returns icons from the * {@link com.mucommander.ui.icon.IconManager.IconSet#FILE IconManager's custom file icon set}, based on files' extension and type. * No caching is performed by this class as {@link IconManager} already takes care of this. * * @author Maxence Bernard */ public class CustomFileIconProvider implements FileIconProvider { /** Has init() method already been called? */ private static boolean initialized; /** Hashtable that associates file extensions with icon names */ private static Map extensionMap; /** Icon for directories */ public final static String FOLDER_ICON_NAME = "folder.png"; /** Default icon for files without a known extension */ public final static String FILE_ICON_NAME = "file.png"; /** Icon for supported archives (browsable) */ public final static String ARCHIVE_ICON_NAME = "archive_supported.png"; /** Icon for parent folder (..) */ public final static String PARENT_FOLDER_ICON_NAME = "parent.png"; /** Transparent icon symbolizing symlinks, painted over an existing icon */ public final static String SYMLINK_ICON_NAME = "link.png"; /** Icon for Mac OS X's applications */ public final static String MAC_OS_X_APP_ICON_NAME = "executable_osx.png"; /** Icon for the root of remote (non-local) locations */ public final static String NETWORK_ICON_NAME = "network.png"; /** Icon for the not accessible remote locations */ public final static String DISCONNECTED_ICON_NAME = "disconnect.png"; /** Icon for android ADB FS locations */ public final static String ANDROID_ICON_NAME = "android.png"; /** Icon for not accessible files (used for quick-lists) **/ public final static String NOT_ACCESSIBLE_FILE = "not_accessible.png"; /** Icon for bookmarks */ public final static String BOOKMARK_ICON_NAME = "bookmark.png"; /** File icon <-> extensions association map. For information about specific file extensions, refer to: *

    */ final static String[][] ICON_EXTENSIONS = { {"archive_unsupported.png", "7z", "ace", "arj", "bin", "bz", "cab", "dmg", "hqx", "ipk", "lha", "lzh", "lzx", "msi", "mpkg", "pak", "pkg", "pq6", "rar", "rk", "rz", "sea", "sit", "sitx", "sqx", "z", "zoo"}, // Unsupported archive formats (no native support), see http://en.wikipedia.org/wiki/Archive_formats {"audio.png", "aac", "aif", "aiff", "aifc", "amr", "ape", "au", "cda", "mp3", "mpa", "mp2", "mpc", "m3u", "m4a", "m4b", "m4p", "nap", "ogg", "pls", "ra", "ram", "wav", "wave", "flac", "wma", "mid", "midi", "smf", "mod", "mtm", "xm", "s3m", "mka"}, // Audio formats, see http://en.wikipedia.org/wiki/Audio_file_format {"cd_image.png", "iso", "nrg"}, // CD/DVD image {"certificate.png", "cer", "crt", "key"}, // Certificate file {"configuration.png", "cnf", "conf", "config", "inf", "ini", "pif", "prefs", "prf"}, // Configuration file {"database.png", "myi", "myd", "frm", "sql", "sqc", "sqr", "mdb", "mde", "mdn", "mdt", "accdb", "accde", "accdr", "accdt"}, // Database file {"executable_windows.png", "bat", "com", "exe"}, // Windows executables {"feed.png", "rdf", "rss"}, // RSS/RDF feed {"font.png", "fnt", "fon", "otf"}, // Non-TrueType font {"font_truetype.png", "ttc", "ttf"}, // TrueType font {"image_bitmap.png", "exif", "ico", "gif", "j2k", "jpg", "jpeg", "jpg2", "jp2", "bmp", "ico", "iff", "mng", "pcd", "pic", "pict", "png", "psd", "psp", "pbm", "pgm", "ppm", "raw", "tga", "tiff", "tif", "wbmp", "xbm", "xcf", "xpm"}, // Bitmap image formats, see http://en.wikipedia.org/wiki/Graphics_file_format and http://en.wikipedia.org/wiki/Image_file_formats {"image_vector.png", "ai", "cgm", "dpx", "dxf", "eps", "emf", "ps", "svg", "svgz", "wmf", "xar"}, // Vector image formats, http://en.wikipedia.org/wiki/Graphics_file_format {"library.png", "dylib", "la", "o", "so"}, // Libraries {"linux.png", "deb", "rpm"}, // Linux packages {"macromedia_actionscript.png", "as"}, // Macromedia Actionscript {"macromedia_flash.png", "swf", "swd", "swa", "swc", "fla", "flv", "flp", "jsfl"}, // Macromedia Flash {"macromedia_freehand.png", "fh", "fhd"}, // Macromedia Freehand {"ms_excel.png", "xls", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xlr", "xlt", "xlv", "xlw", "xlshtml", "xlsmhtml", "xlthtml", "xlsx", "xltx", "xlsm", "xltm", "xlam", "xlsb"}, // Microsoft Excel {"ms_word.png", "doc", "wbk", "wiz", "wpg", "wpk", "wpm", "wpt", "wrs", "wwl", "docx", "dotx", "docm", "dotm"}, // Microsoft Word {"ms_powerpoint.png", "pcb", "pot", "ppa", "ppi", "pps", "ppt", "pwz", "pptx", "potx", "ppsx", "pptm", "potm", "ppsm"}, // Microsoft Office (Powerpoint) {"ms_visualstudio.png", "atp", "dbp", "hxc", "ncb", "pch", "pdb", "sln", "suo", "srf", "vaf", "vam", "vbg", "vbp", "vbproj", "vcproj", "vdp", "vdproj", "vip", "vmx", "vsdir", "vsmacros", "vsmproj", "vup"}, // Microsoft Visual Studio {"ms_windows_shortcut.png", "lnk"}, // MS Windows .lnk shortcut files {"pdf.png", "pdf"}, // Adobe Acrobat / PDF {"source.png", "asm", "asp", "bas", "bcp", "cbl", "cob", "f", "fpp", "inc", "js", "lsp", "m4", "pas", "pl", "py", "src", "vb", "vbe", "vbs", "x"}, // Languages for which there is no special icon (generic source icon) {"source.png", "awk", "csh", "esh", "sh", "ksh", "ws", "wsf"}, // Shell scripts {"source_c.png", "c", "cc"}, // C source {"source_c_header.png", "h", "hh", "hhh"}, // C header {"source_cplusplus.png", "cpp", "c++"}, // C++ source {"source_csharp.png", "c#=", "cs"}, // C# source {"source_java.png", "java", "jsp"}, // Java source {"source_php.png", "php", "php3", "php4", "php5", "phtm", "phtml"}, // PHP source {"source_ruby.png", "rb", "rbx", "rhtml"}, // Ruby source {"source_web.png", "html", "htm", "xhtml", "wml", "wmlc", "wmls", "wmlsc", "hdml", "xhdml", "chtml", "vrml", "torrent", "url", "css"}, // Web formats {"source_xml.png", "xml", "dtd", "xfd", "xfdl", "xmap", "xmi", "xsc", "xsd", "xsl", "xslt", "xtd", "xul", "rss", "jnlp", "plist"}, // XML-based formats {"text.png", "1st", "ans", "asc", "ascii", "diz", "err", "faq", "latex", "log", "man", "msg", "nfo", "readme", "rtf", "sig", "tex", "text", "txt"}, // Text formats {"vcard.png", "vcf"}, // vCard {"video.png", "3g2", "3gp", "3gp2", "3gpp", "asf", "asx", "avi", "dir", "dv", "dxr", "m1v", "m4e", "m4u", "moov", "mov", "movie", "mp4", "mpe", "mpeg", "mpg", "mpv2", "qt", "rm", "rmvb", "rts", "vob", "wmv", "divx", "mkv"} // Video formats }; /** * Initializes the file extension map. */ private static void init() { // Map known file extensions to icon names extensionMap = new HashMap<>(); for (String[] iconExt : ICON_EXTENSIONS) { String iconName = iconExt[0]; for (int j = 1; j < iconExt.length; j++) extensionMap.put(iconExt[j], iconName); } initialized = true; } /** * Returns an icon symbolizing a symlink to the given target icon. * * @param targetIcon the icon representing the symlink's target * @return an icon symbolizing a symlink to the given target */ private static ImageIcon getSymlinkIcon(Icon targetIcon) { return IconManager.getCompositeIcon(targetIcon, IconManager.getIcon(IconManager.IconSet.FILE, SYMLINK_ICON_NAME)); } ///////////////////////////////////// // FileIconProvider implementation // ///////////////////////////////////// public Icon getFileIcon(AbstractFile file, Dimension preferredResolution) { // Call init, if not done already if (!initialized) { init(); } if (file == null) { return IconManager.getIcon(IconManager.IconSet.FILE, DISCONNECTED_ICON_NAME); } // If file is a symlink, get the linked file's icon and paint a semi-transparent symbolic icon on top of it boolean isSymlink = file.isSymlink(); if (isSymlink) { file = file.getCanonicalFile(); } ImageIcon icon; // Retrieve the file's extension, null if the file has no extension String fileExtension = file.getExtension(); if (!file.exists()) { icon = IconManager.getIcon(IconManager.IconSet.FILE, DISCONNECTED_ICON_NAME); } else if (FileProtocols.ADB.equals(file.getURL().getScheme()) && file.isRoot()) { icon = IconManager.getIcon(IconManager.IconSet.FILE, ANDROID_ICON_NAME); } // Special icon for the root of remote (non-local) locations else if (!FileProtocols.FILE.equals(file.getURL().getScheme()) && file.isRoot()) { icon = IconManager.getIcon(IconManager.IconSet.FILE, NETWORK_ICON_NAME); } // If file is a directory, use folder icon. One exception is made for 'app' extension under MAC OS else if (file.isDirectory()) { // Mac OS X application are directories with the .app extension and have a dedicated icon icon = IconManager.getIcon(IconManager.IconSet.FILE, "app".equals(fileExtension) ? MAC_OS_X_APP_ICON_NAME : FOLDER_ICON_NAME); } // If the file is browsable (supported archive or other), use an icon symbolizing an archive else if (file.isBrowsable()) { icon = IconManager.getIcon(IconManager.IconSet.FILE, ARCHIVE_ICON_NAME); } // Regular file icon else { // Determine if the file's extension has an associated icon if (fileExtension == null) // File has no extension, use default file icon icon = IconManager.getIcon(IconManager.IconSet.FILE, FILE_ICON_NAME); else { // Compare extension against lower-cased extensions String iconName = extensionMap.get(fileExtension.toLowerCase()); if (iconName == null) // No icon associated to extension, use default file icon icon = IconManager.getIcon(IconManager.IconSet.FILE, FILE_ICON_NAME); else { // Retrieves the cached (or freshly loaded if not in cache already) ImageIcon instance corresponding to the icon's name icon = IconManager.getIcon(IconManager.IconSet.FILE, iconName); // Returned IconImage should never be null, but if it is (icon file missing), return default file icon if (icon == null) { return IconManager.getIcon(IconManager.IconSet.FILE, FILE_ICON_NAME); } } } } // If file is a symlink, paint a semi-transparent symbolic icon over the linked file's icon if (isSymlink) { return getSymlinkIcon(icon); } return icon; } } ================================================ FILE: src/main/java/com/mucommander/ui/icon/EmptyIcon.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.icon; import java.awt.Component; import java.awt.Graphics; import javax.swing.Icon; /** * Empty {@link Icon} in the specified size that can be used as a place holder * * @author Arik Hadas */ public class EmptyIcon implements Icon { private final int width; private final int height; public EmptyIcon(int size) { this.width = size; this.height = size; } public EmptyIcon(int width, int height) { this.width = width; this.height = height; } public int getIconHeight() { return height; } public int getIconWidth() { return width; } public void paintIcon(Component c, Graphics g, int x, int y) { } } ================================================ FILE: src/main/java/com/mucommander/ui/icon/FileIcons.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.icon; import java.awt.Dimension; import java.awt.Image; import javax.swing.Icon; import javax.swing.ImageIcon; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.icon.FileIconProvider; import com.mucommander.commons.file.impl.adb.AdbFile; import com.mucommander.commons.file.impl.ftp.FTPFile; import com.mucommander.commons.file.impl.http.HTTPFile; import com.mucommander.commons.file.impl.sftp.SFTPFile; import com.mucommander.commons.runtime.OsFamily; /** * FileIcons provides several methods to retrieve file icons for a given file: *
      *
    • {@link #getSystemFileIcon(AbstractFile)}: returns a system icon, provided by the underlying OS/desktop manager. * Under supported platforms, those file icons are the same as the ones displayed in the default file manager.
    • *
    • {@link #getCustomFileIcon(AbstractFile)}: returns a custom icon, fetched from the muCommander icon set and * based on the file's kind (archive, folder...) and extension.
    • *
    • {@link #getFileIcon(AbstractFile)} returns either a system icon or a custom icon, depending on the current * system icons policy. The default policy is {@link #DEFAULT_SYSTEM_ICONS_POLICY} and can be changed using * {@link #setSystemIconsPolicy(String)}.
    • *
    * Icons can be requested indifferently for any type of {@link AbstractFile} files: local files, remote files, * archives entries... * *

    It is important to note that not all platforms have proper support for system file icons. * The {@link #hasProperSystemIcons()} method can be used to determine if the current platform properly supports system * icons. Non-supported platforms may return no icon (null values), or icons that do not resemble the * system ones. * * @author Maxence Bernard */ public class FileIcons { /** Never use system file icons */ public final static String USE_SYSTEM_ICONS_NEVER = "never"; /** Use system file icons only for applications */ public final static String USE_SYSTEM_ICONS_APPLICATIONS = "applications"; /** Always use system file icons */ public final static String USE_SYSTEM_ICONS_ALWAYS = "always"; /** Default policy for system icons */ private final static String DEFAULT_SYSTEM_ICONS_POLICY = OsFamily.MAC_OS_X.isCurrent() ? USE_SYSTEM_ICONS_ALWAYS : USE_SYSTEM_ICONS_APPLICATIONS; /** Default icon scale factor (no rescaling) */ private final static float DEFAULT_SCALE_FACTOR = 1.0f; /** Base width and height of icons for a scale factor of 1 */ private final static int BASE_ICON_DIMENSION = 16; /** Controls if and when system file icons should be used instead of custom icons */ private static String systemIconsPolicy = DEFAULT_SYSTEM_ICONS_POLICY; /** Current icon scale factor */ private static float scaleFactor = DEFAULT_SCALE_FACTOR; /** FileIconProvider instance for custom icons */ private static FileIconProvider customFileIconProvider; /** FileIconProvider instance for system icons */ private static FileIconProvider systemFileIconProvider; /** Current dimension of returned file icons */ private static Dimension iconDimension = new Dimension((int)(BASE_ICON_DIMENSION * DEFAULT_SCALE_FACTOR), (int)(BASE_ICON_DIMENSION * DEFAULT_SCALE_FACTOR)); /* * Initializes the system and custom file icon providers. */ static { setCustomFileIconProvider(new CustomFileIconProvider()); setSystemFileIconProvider(FileFactory.getDefaultFileIconProvider()); } /** * Shorthand for {@link #getFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} called with the * icon dimension returned by {@link #getIconDimension()}. * * @param file the AbstractFile instance for which an icon will be returned * @return an icon for the given file * @see #getSystemIconsPolicy() */ public static Icon getFileIcon(AbstractFile file) { return getFileIcon(file, iconDimension); } /** * Returns an icon for the given file and of the specified dimension. * The returned icon will either be a system icon, or one from the custom icon set, depending on the current * {@link #getSystemIconsPolicy() system icons policy}. * If a system icon should have been returned for the specified file but could not be resolved * ({@link #getSystemFileIcon(AbstractFile, Dimension)} returned null), an icon from the * custom icon set will be returned instead. Therefore, this method never returns null. * * @param file the AbstractFile instance for which an icon will be returned * @param iconDimension the icon's dimension * @return an icon for the given file * @see #getSystemIconsPolicy() */ public static Icon getFileIcon(AbstractFile file, Dimension iconDimension) { boolean systemIcon = USE_SYSTEM_ICONS_ALWAYS.equals(systemIconsPolicy); if (USE_SYSTEM_ICONS_APPLICATIONS.equals(systemIconsPolicy)) { systemIcon = com.mucommander.desktop.DesktopManager.isApplication(file); } if (file instanceof AdbFile && file.isRoot()) { return IconManager.getIcon(IconManager.IconSet.FILE, "android.png"); } else if (file instanceof FTPFile || file instanceof HTTPFile || file instanceof SFTPFile) { return IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.NETWORK_ICON_NAME); } if (systemIcon) { Icon icon = getSystemFileIcon(file, iconDimension); if (icon != null) { return icon; } // If the system icon could not be resolved, return a custom file icon } return getCustomFileIcon(file, iconDimension); } /** * Shorthand for {@link #getCustomFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} called with the * icon dimension returned by {@link #getIconDimension()}. * * @param file the file for which an icon is to be returned * @return a custom icon for the given file */ private static Icon getCustomFileIcon(AbstractFile file) { return getCustomFileIcon(file, iconDimension); } /** * Returns an icon of the specified dimension for the given file. The icon is provided by the * {@link #getCustomFileIconProvider() custom file icon provider}. This method is guaranteed to never return * null. * * @param file the file for which an icon is to be returned * @param iconDimension the icon's dimension * @return a custom icon for the given file * @see #getCustomFileIconProvider() */ private static Icon getCustomFileIcon(AbstractFile file, Dimension iconDimension) { return getFileProviderIcon(customFileIconProvider, file, iconDimension); } /** * Shorthand for {@link #getSystemFileIcon(com.mucommander.commons.file.AbstractFile, java.awt.Dimension)} called with the * icon dimension returned by {@link #getIconDimension()}. * * @param file the file for which an icon is to be returned * @return a system icon for the given file */ public static Icon getSystemFileIcon(AbstractFile file) { return getSystemFileIcon(file, iconDimension); } /** * Returns an icon of the specified dimension for the given file. The returned icon is provided by the * underlying OS/desktop manager, using the {@link com.mucommander.commons.file.icon.FileIconProvider} currently set. * Returns null if the icon couldn't be retrieved, either because the file doesn't exist or for * any other reason. * * @param file the file for which an icon is to be returned * @param iconDimension the icon's dimension * @return a system icon for the given file */ private static Icon getSystemFileIcon(AbstractFile file, Dimension iconDimension) { return getFileProviderIcon(systemFileIconProvider, file, iconDimension); } /** * Returns an icon of the specified dimension for the given file. The return icon is provided by the specified * {@link FileIconProvider}. This method takes care of up/down-scaling the icon returned by the provider if it * doesn't match the specified dimension. * * @param fip the FileIconProvider from which to fetch the icon * @param file the file for which an icon is to be returned * @param iconDimension the icon's dimension * @return an icon for the specified file */ private static Icon getFileProviderIcon(FileIconProvider fip, AbstractFile file, Dimension iconDimension) { Icon icon = fip.getFileIcon(file, iconDimension); if (icon == null) { return null; } if (iconDimension.width == icon.getIconWidth() && iconDimension.height == icon.getIconHeight()) { return icon; // the icon already has the right dimension } // Scale the icon to the target dimension ImageIcon imageIcon = IconManager.getImageIcon(icon); return new ImageIcon(imageIcon.getImage().getScaledInstance(iconDimension.width, iconDimension.height, Image.SCALE_AREA_AVERAGING)); } /** * Returns the {@link com.mucommander.commons.file.icon.FileIconProvider} instance that provides 'custom' file icons. * * @return the FileIconProvider instance that provides 'custom' file icons. */ private static FileIconProvider getCustomFileIconProvider() { return customFileIconProvider; } /** * Sets the {@link com.mucommander.commons.file.icon.FileIconProvider} instance that provides 'custom' file icons. * * @param fip the FileIconProvider instance that provides 'custom' file icons */ private static void setCustomFileIconProvider(FileIconProvider fip) { customFileIconProvider = fip; } /** * Returns the {@link com.mucommander.commons.file.icon.FileIconProvider} instance that provides 'system' file icons. * * @return the FileIconProvider instance that provides 'custom' file icons. */ public static FileIconProvider getSystemFileIconProvider() { return systemFileIconProvider; } /** * Sets the {@link com.mucommander.commons.file.icon.FileIconProvider} instance that provides 'custom' file icons. * * @param fip the FileIconProvider instance that provides 'custom' file icons */ private static void setSystemFileIconProvider(FileIconProvider fip) { systemFileIconProvider = fip; } /** * Returns the dimension of file icons currently returned by this class, which is the base icon dimension (16x16) * multiplied by the current scale factor. * * @return the dimension of file icons currently returned by this class */ public static Dimension getIconDimension() { return iconDimension; } /** * Returns the current icon scale factor, initialized by default to {@link #DEFAULT_SCALE_FACTOR}. * * @return the current icon scale factor */ public static float getScaleFactor() { return scaleFactor; } /** * Sets the current icon scale factor. The given value must be greater than 0. * * @param factor the new icon scale factor to use * @throws IllegalArgumentException if factor is lower or equal to 0 */ public static void setScaleFactor(float factor) { if (scaleFactor <= 0) { throw new IllegalArgumentException("Scale factor must be greater than 0, (" + factor + ")"); } scaleFactor = factor; iconDimension = new Dimension((int)(BASE_ICON_DIMENSION *scaleFactor), (int)(BASE_ICON_DIMENSION*scaleFactor)); } /** * Returns the current system icons policy, controlling when system file icons should be used instead * of custom file icons, see constant fields for possible values. The system icons policy is by default initialized * to {@link #DEFAULT_SYSTEM_ICONS_POLICY}. * * @return the current system icons policy */ public static String getSystemIconsPolicy() { return systemIconsPolicy; } /** * Sets the system icons policy, controlling when system file icons should be used instead of custom file icons. * See constants fields for allowed values. * * @param policy the new system icons policy to use */ public static void setSystemIconsPolicy(String policy) { systemIconsPolicy = policy; } /** * Returns true if the current platform is able to retrieve system icons that match the ones used in * the OS's default file manager. If false is returned and {@link #getSystemFileIcon(com.mucommander.commons.file.AbstractFile)} * is used or {@link #getFileIcon(com.mucommander.commons.file.AbstractFile)} together with a system policy different from * {@link #USE_SYSTEM_ICONS_NEVER}, the returned icon will probably look very bad. * * @return true if the current platform is able to retrieve system icons that match the ones used in the OS's * default file manager */ public static boolean hasProperSystemIcons() { return OsFamily.MAC_OS_X.isCurrent() || OsFamily.WINDOWS.isCurrent(); } } ================================================ FILE: src/main/java/com/mucommander/ui/icon/IconManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.icon; import java.awt.*; import java.awt.image.BufferedImage; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Hashtable; import java.util.Map; import javax.swing.Icon; import javax.swing.ImageIcon; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import com.mucommander.commons.file.util.ResourceLoader; import ru.trolsoft.macosx.RetinaImageIcon; /** * IconManager takes care of loading, caching, rescaling the icons contained inside the application's JAR file. * * @author Maxence Bernard */ @Slf4j public class IconManager { public enum IconSet { /** Designates the file icon set */ FILE("file"), /** Designates the action icon set */ ACTION("action"), /** Designates the toolbar icon set */ STATUS_BAR("status_bar"), /** Designates the table icon set */ COMMON("common"), /** Designates the preferences icon set */ PREFERENCES("preferences"), /** Designates the progress icon set */ PROGRESS("progress"), /** Designates the language icon set */ LANGUAGE("language"), /** Designates the trolcommander icon set */ TROLCOMMANDER("trolcommander"), /** Other images */ MISC("misc"); /** Base folder of all images */ private final static String BASE_IMAGE_FOLDER = "/images/"; /** Icon sets folders within the application's JAR file * -- GETTER -- * Returns the path to the folder that contains the image resource files of the given icon set. * The returned path is relative to the application JAR file's root and contains a trailing slash. */ @Getter private final String folder; /** Caches for the different icon sets */ private final Map cache = new Hashtable<>(); IconSet(String folder) { this.folder = BASE_IMAGE_FOLDER + folder + '/'; } private Map getCache() { return cache; } } /** * Creates a new instance of IconManager. */ private IconManager() {} /** * Creates and returns an ImageIcon instance using the specified icon path and scale factor. No caching. * * @param iconPath path of the icon resource inside the application's JAR file * @param scaleFactor the icon scale factor, 1.0f to have the icon in its original size (no rescaling) */ public static ImageIcon getIcon(String iconPath, float scaleFactor) { URL resourceURL = ResourceLoader.getResourceAsURL(iconPath); if (resourceURL == null) { log.debug("Warning: attempt to load non-existing icon: " + iconPath + " , icon missing ?"); return null; } ImageIcon icon; if (RetinaImageIcon.IS_RETINA) { icon = new RetinaImageIcon(getRetinaUrl(resourceURL)); if (icon.getImageLoadStatus() == MediaTracker.ERRORED) { icon = new ImageIcon(resourceURL); } } else { icon = new ImageIcon(resourceURL); } //ImageIcon icon = new ImageIcon(resourceURL); return scaleFactor == 1.0f ? icon : getScaledIcon(icon, scaleFactor); } private static URL getRetinaUrl(URL url) { String path = url.toString(); if (path.endsWith(".png") && !path.contains("@")) { try { return new URI(path.replace(".png", "@2x.png")).toURL(); } catch (MalformedURLException | URISyntaxException e) { log.error("getRetinaUrl error", e); } } return url; } /** * Convenience method, calls and returns the result of {@link #getIcon(String, float) getIcon(iconPath, scaleFactor)} * with a scale factor of 1.0f (no rescaling). */ public static ImageIcon getIcon(String iconPath) { return getIcon(iconPath, 1.0f); } /** * Returns a scaled version of the given ImageIcon instance, using the specified scale factor. * * @param icon the icon to scale. * @param scaleFactor the icon scale factor, 1.0f to have the icon in its original size (no rescaling) */ public static ImageIcon getScaledIcon(ImageIcon icon, float scaleFactor) { if (scaleFactor == 1.0f || icon == null) { return icon; } Image image = icon.getImage(); return new ImageIcon(image.getScaledInstance((int)(scaleFactor*image.getWidth(null)), (int)(scaleFactor*image.getHeight(null)), Image.SCALE_AREA_AVERAGING)); } /** * Returns a 'composite' icon made by composing the two given icons: the backgroundIcon is painted * first, and the foregroundIcon is superposed, letting its non-transparent pixels reveal the * background icon. * For this method to provide a meaningful result, the two icons should have the same dimensions and the * foreground should have some transparent pixels. * * @param backgroundIcon the icon that is painted first * @param foregroundIcon the icon that is superposed above backgroundIcon, should use transparency * @return a 'composite' icon made by composing the two given icons */ public static ImageIcon getCompositeIcon(Icon backgroundIcon, Icon foregroundIcon) { BufferedImage bi = new BufferedImage(backgroundIcon.getIconWidth(), backgroundIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics g = bi.getGraphics(); backgroundIcon.paintIcon(null, g, 0, 0); foregroundIcon.paintIcon(null, g, 0, 0); return new ImageIcon(bi); } /** * Returns an icon in the specified icon set and with the given name. If a scale factor other than 1.0f is passed, * the return icon will be scaled accordingly. * *

    If the icon set has a cache, first looks for an existing instance in the cache, and if it couldn't be found, * create an instance and store it in the cache for future access. Note that the cached icon is unscaled, i.e. * the scaled icon is not cached. * * @param iconSet an icon set (see public constants for possible values) * @param iconName filename of the icon to retrieve * @param scaleFactor the icon scale factor, 1.0f to have the icon in its original size (no rescaling) * @return an ImageIcon instance corresponding to the specified icon set, name and scale factor, * null if the image wasn't found or couldn't be loaded */ public static ImageIcon getIcon(IconSet iconSet, String iconName, float scaleFactor) { ImageIcon icon = getIcon(iconSet, iconName); return icon == null || scaleFactor == 1.0f ? icon : getScaledIcon(icon, scaleFactor); } /** * Convenience method, calls and returns the result of {@link #getIcon(IconSet, String, float) getIcon(iconSet, iconName, scaleFactor)} * with a scale factor of 1.0f (no rescaling). */ public static ImageIcon getIcon(IconSet iconSet, String iconName) { Map cache = iconSet.getCache(); if (cache == null) { // No caching, simply create the icon return getIcon(iconSet.getFolder() + iconName); } // Look for the icon in the cache ImageIcon icon = cache.get(iconName); if (icon == null) { // Icon is not in the cache, let's create it icon = getIcon(iconSet.getFolder()+iconName); // and add it to the cache if icon exists if (icon != null) { cache.put(iconName, icon); } } return icon; } /** * Returns an icon made of the specified icon and some transparent space around it. * * @param icon the original icon, will be painted at the center of the new icon * @param insets specifies the dimensions of the transparent space around the returned icon * @return an icon made of the specified icon and some transparent space around it */ public static ImageIcon getPaddedIcon(ImageIcon icon, Insets insets) { if (icon instanceof RetinaImageIcon) { return RetinaImageIcon.buildPaddedIcon(icon, insets); } BufferedImage bi = new BufferedImage( icon.getIconWidth() + insets.left + insets.right, icon.getIconHeight() + insets.top + insets.bottom, BufferedImage.TYPE_INT_ARGB); Graphics g = bi.getGraphics(); g.drawImage(icon.getImage(), insets.left, insets.top, null); return new ImageIcon(bi); } /** * Creates and returns an ImageIcon with the same content and dimensions. This method is useful when an ImageIcon * is needed and only an Icon is available. * *

    If the given Icon is already an ImageIcon, the same instance is returned. If it is not, a new ImageIcon is * created and returned. */ public static ImageIcon getImageIcon(Icon icon) { if (icon instanceof ImageIcon) { return (ImageIcon) icon; } BufferedImage bi = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); icon.paintIcon(null, bi.getGraphics(), 0, 0); return new ImageIcon(bi); } } ================================================ FILE: src/main/java/com/mucommander/ui/icon/SpinningDial.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.icon; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Arrays; /** * Animated icon of a spinning dial used to notify users that an application is performing a getTask. *

    * This behaves as any animated icon except for one thing: when the animation is stopped using * {@link #setAnimated(boolean)}, the dial won't be displayed anymore until the animation is * resumed. * *

    * This heavily borrows code from Technomage's furbelow package, distributed * under the GNU Lesser General Public License.
    * The original source code can be found here. * * @author twall, Nicolas Rinaudo */ public class SpinningDial extends AnimatedIcon { /** Default creation animation status. */ public static final boolean DEFAULT_ANIMATE = false; /** Dial's default color. */ public static final Color DEFAULT_COLOR = Color.BLACK; /** Minimum alpha-transparency value that must be applied to the dial's color as it fades out. */ private static final int MIN_ALPHA = 32; /** Icon's default width and height. */ public static final int DEFAULT_SIZE = 16; /** Default number of spokes in the dial. */ public static final int DEFAULT_SPOKES = 12; /** Dial's full size, will be scaled down at paint time. */ private static final int FULL_SIZE = 256; /** Width of each of the dial's strokes. */ private static final float DEFAULT_STROKE_WIDTH = FULL_SIZE / 10f; /** Scale down factor for the dial. */ private static final float FRACTION = 0.6f; /** Icon's width. */ private final int width; /** Icon's height. */ private final int height; /** All images that compose the spinning dial. */ private final Image[] frames; /** Color used to paint the dial. */ private Color color; /** Width of each stroke. */ private float strokeWidth; /** * Creates a new spinning dial. *

    * The new instance will be initialized using default values: *

      *
    • {@link #DEFAULT_SIZE} for its width and height.
    • *
    • {@link #DEFAULT_COLOR} for its color.
    • *
    • {@link #DEFAULT_SPOKES} for its number of spokes.
    • *
    * *

    * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. */ public SpinningDial() {this(DEFAULT_SIZE, DEFAULT_SIZE);} /** * Creates a new spinning dial. *

    * The new instance will be initialized using default values: *

      *
    • {@link #DEFAULT_SIZE} for its width and height.
    • *
    • {@link #DEFAULT_COLOR} for its color.
    • *
    • {@link #DEFAULT_SPOKES} for its number of spokes.
    • *
    * * @param animate whether to animate the dial immediately or not. */ public SpinningDial(boolean animate) {this(DEFAULT_SIZE, DEFAULT_SIZE, animate);} /** * Creates a new spinning dial with the specified color. *

    * The new instance will be initialized using default values: *

      *
    • {@link #DEFAULT_SIZE} for its width and height.
    • *
    • {@link #DEFAULT_SPOKES} for its number of spokes.
    • *
    * *

    * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * * @param c color in which to paint the dial. */ public SpinningDial(Color c) {this(DEFAULT_SIZE, DEFAULT_SIZE, c);} /** * Creates a new spinning dial with the specified color. *

    * The new instance will be initialized using default values: *

      *
    • {@link #DEFAULT_SIZE} for its width and height.
    • *
    • {@link #DEFAULT_SPOKES} for its number of spokes.
    • *
    * * @param c color in which to paint the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(Color c, boolean animate) {this(DEFAULT_SIZE, DEFAULT_SIZE, c, animate);} /** * Creates a new spinning dial with the specified dimensions. *

    * The new instance will be initialized using default values: *

      *
    • {@link #DEFAULT_COLOR} for its color.
    • *
    • {@link #DEFAULT_SPOKES} for its number of spokes.
    • *
    * *

    * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * * @param w width of the icon. * @param h height of the icon. */ public SpinningDial(int w, int h) {this(w, h, DEFAULT_SPOKES);} /** * Creates a new spinning dial with the specified dimensions. *

    * The new instance will be initialized using default values: *

      *
    • {@link #DEFAULT_COLOR} for its color.
    • *
    • {@link #DEFAULT_SPOKES} for its number of spokes.
    • *
    * * @param w width of the icon. * @param h height of the icon. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, boolean animate) {this(w, h, DEFAULT_SPOKES, animate);} /** * Creates a new spinning dial with the specified dimensions and color. *

    * The new instance will use {@link #DEFAULT_SPOKES} for its number of spokes. * *

    * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * * @param w width of the icon. * @param h height of the icon. * @param c color in which to paint the dial. */ public SpinningDial(int w, int h, Color c) {this(w, h, DEFAULT_SPOKES, c);} /** * Creates a new spinning dial with the specified dimensions and color. *

    * The new instance will use {@link #DEFAULT_SPOKES} for its number of spokes. * * @param w width of the icon. * @param h height of the icon. * @param c color in which to paint the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, Color c, boolean animate) {this(w, h, DEFAULT_SPOKES, c, animate);} /** * Creates a new spinning dial with the specified dimensions and number of spokes. *

    * The new instance will use {@link #DEFAULT_COLOR} for its color. * *

    * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. */ public SpinningDial(int w, int h, int spokes) {this(w, h, spokes, DEFAULT_COLOR);} /** * Creates a new spinning dial with the specified dimensions and number of spokes. *

    * The new instance will use {@link #DEFAULT_COLOR} for its color. * * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, int spokes, boolean animate) {this(w, h, spokes, DEFAULT_COLOR, animate);} /** * Creates a new spinning dial with the specified characteristics. *

    * A dial created that way will not be displayed until {@link #setAnimated(boolean)} is * called to animate it. * * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. * @param c color in which to paint the dial. */ public SpinningDial(int w, int h, int spokes, Color c) {this(w, h, spokes, c, DEFAULT_ANIMATE);} /** * Creates a new spinning dial with the specified characteristics. * @param w width of the icon. * @param h height of the icon. * @param spokes number of spokes that compose the dial. * @param c color in which to paint the dial. * @param animate whether to animate the dial immediately or not. */ public SpinningDial(int w, int h, int spokes, Color c, boolean animate) { super(spokes, 1000 / spokes); // Initializes the icon. width = w; height = h; color = c; frames = new Image[getFrameCount()]; strokeWidth = DEFAULT_STROKE_WIDTH; // Animates the icon if necessary. if(animate) setAnimated(true); } /** * Sets the width of the strokes used to paint each of the dial's spokes. * @param width width of the strokes used to paint each of the dial's spokes. */ public synchronized void setStrokeWidth(float width) {strokeWidth = width;} /** * Returns the width of the strokes used to paint each of the dial's spokes. * @return the width of the strokes used to paint each of the dial's spokes. */ public synchronized float getStrokeWidth() {return strokeWidth;} /** * Sets the color used to draw the dial. * @param c color in which to paint the dial. */ public synchronized void setColor(Color c) { // Ignores calls that don't actually change anything. if (!color.equals(c)) { color = c; // Resets stored images to make sure they get repainted with the right color. Arrays.fill(frames, null); } } /** * Returns the color used to paint the dial. * @return the color used to paint the dial. */ public synchronized Color getColor() {return color;} /** * Computes the dial color according to the specified alpha-transparency value. * @param alpha transparency value that must be applied to the dial's color. */ protected Color getSpokeColor(int alpha) {return new Color(color.getRed(), color.getGreen(), color.getBlue(), Math.max(MIN_ALPHA, alpha));} /** * Returns the icon's height. * @return the icon's height. */ @Override public int getIconHeight() { return height; } /** * Returns the icon's width. * @return the icon's width. */ @Override public int getIconWidth() { return width; } /** * Initializes graphics for painting one of the dial's frames. * @param graphics graphics instance to initialize. */ private void initializeGraphics(Graphics2D graphics) { float scale = (float)Math.min(width, height) / FULL_SIZE; graphics.setComposite(AlphaComposite.Clear); graphics.fillRect(0, 0, width, height); graphics.setComposite(AlphaComposite.Src); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); graphics.translate((float)width / 2, (float)height / 2); graphics.scale(scale, scale); } /** * Paints the current frame on the specified component. * @param c component on which to paint the dial. * @param graphics graphic context to use when painting the dial. * @param x horizontal coordinate at which to paint the dial. * @param y vertical coordinate at which to paint the dial. */ @Override public synchronized void paintFrame(Component c, Graphics graphics, int x, int y) { int currentFrame; // Ignores paint calls while not animated. if (isAnimated()) { // Checks whether the current frame has already been generated or not, generates // it if not. if ((frames[currentFrame = getFrame()]) == null) { // Initializes the frame. // Note: getGraphicsConfiguration() returns null if the component has not yet been added to a container GraphicsConfiguration gc = c != null ? c.getGraphicsConfiguration() : null; Image frame = gc != null ? gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT) : new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); // Initializes the frame's g. Graphics2D g = (Graphics2D)frame.getGraphics(); initializeGraphics(g); // Draws each spoke in the dial. int alpha = 255; int radius = FULL_SIZE / 2 - 1 - (int)(strokeWidth / 2); for (int i = 0; i < getFrameCount(); i++) { double cos = Math.cos((Math.PI * 2) - (Math.PI * 2 * (i - currentFrame)) / getFrameCount()); double sin = Math.sin((Math.PI * 2) - (Math.PI * 2 * (i - currentFrame)) / getFrameCount()); g.setColor(getSpokeColor(alpha)); g.drawLine((int)(radius * FRACTION * cos), (int)(radius * FRACTION * sin), (int)(radius * cos), (int)(radius * sin)); alpha = Math.max(MIN_ALPHA, (alpha * 3) / 4); } g.dispose(); // Stores the newly generated frame. frames[currentFrame] = frame; } // Draws the current frame. graphics.drawImage(frames[currentFrame], x, y, null); } } /** * Starts / stops the spinning dial. *

    * If a is false, the animation will stop and the * dial won't be displayed anymore until the animation resumes. * * @param a whether to start or stop the animation. */ @Override public void setAnimated(boolean a) { super.setAnimated(a); // Makes sure the dial disappears when the animation is stopped. if (!a) { repaint(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/AsyncPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.icon.SpinningDial; import ru.trolsoft.ui.ZxSpectrumLoadPane; import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; import java.awt.*; /** * AsyncPanel is a JPanel aimed at components that potentially take a long time to * initialize. It allows to deport their initialization in a separate thread as to not lock the event thread. * It works as follows: *

      *
    1. Initially, AsyncPanel displays a 'please wait component' that symbolizes the fact that the contents of the * panel is being loaded.
    2. *
    3. Then the initialization of the component will be executed in background thread by the method {@link #initTargetComponent()}
    4. *
    5. When AsyncPanel becomes visible on screen, the {@link #getTargetComponent(Exception)} method is called to trigger the * initialization of the real component to display.
    6. *
    7. As soon as the method returns, the wait component is removed and the target component added to AsyncPanel. * If AsyncPanel is the child of a java.awt.Window, the window is repacked to take into account the * new size of this panel.
    8. *
    * *

    This panel tries to be as 'transparent' as possible for the target component: the borders of this panel are empty * and its layout is a BorderLayout where the target component is added to the center. * * @author Maxence Bernard */ public abstract class AsyncPanel extends ZxSpectrumLoadPane { private class InitWorker extends SwingWorker { private Exception exception; volatile boolean finished = false; @Override protected Void doInBackground() { try { initTargetComponent(); } catch (Exception e) { exception = e; } return null; } @Override protected void done() { if (finished) { return; } finished = true; finish(); } private void finish() { JComponent targetComponent = getTargetComponent(exception); remove(waitComponent); setBorder(new EmptyBorder(0, 0, 0, 0)); add(targetComponent, BorderLayout.CENTER); updateLayout(); // Force update viewer/editor window on Windows 8.1 if (OsFamily.WINDOWS.isCurrent()) { setSize(getSize()); } stop(); } } /** The component displayed while the target component is being loaded */ private final JComponent waitComponent; /** This field becomes true when this panel has become visible on screen. */ private boolean visibleOnScreen; private InitWorker worker; /** * Creates a new AsyncPanel with the default wait component. */ protected AsyncPanel() { this(getDefaultWaitComponent()); } /** * Creates a new AsyncPanel that displays the given component while the target component is being * loaded. * * @param waitComponent the component to display while the target component is being loaded */ private AsyncPanel(JComponent waitComponent) { super(new BorderLayout()); start(); this.waitComponent = waitComponent; add(waitComponent, BorderLayout.CENTER); // Starts loading the component when this panel has become visible on screen addAncestorListener(new AncestorListener() { public void ancestorAdded(AncestorEvent e) { if (visibleOnScreen) { return; } visibleOnScreen = true; removeAncestorListener(this); // loadTargetComponent(); worker = new InitWorker(); worker.execute(); } public void ancestorRemoved(AncestorEvent event) {} public void ancestorMoved(AncestorEvent event) {} }); } protected void cancel() { stop(); if (worker != null) { worker.cancel(true); } } /* * Loads the target component by calling {@link #getTargetComponent()} and replace the wait component by it. */ // private void loadTargetComponent() { // new Thread() { // @Override // public void init() { // JComponent targetComponent = getTargetComponent(); // // remove(waitComponent); // setBorder(new EmptyBorder(0, 0, 0, 0)); // // add(targetComponent, BorderLayout.CENTER); // // updateLayout(); // } // }.start(); // } /** * Returns the default component to be displayed while the target component is being loaded. * * @return the default component to be displayed while the target component is being loaded */ private static JComponent getDefaultWaitComponent() { JLabel label = new JLabel(Translator.get("loading")); label.setIcon(new SpinningDial(24, 24, true)); // Center the label both horizontally and vertically JPanel tempPanel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.fill = GridBagConstraints.BOTH; tempPanel.add(label, gbc); return tempPanel; } /** * Packs the parent Window that contains this component, if any. This method is called once the target component * has been made initialized and added to this panel. This method can be overridden by subclasses if additional work * needs to be done to update the layout. */ protected void updateLayout() { } public abstract JComponent getTargetComponent(Exception e); public abstract void initTargetComponent() throws Exception; } ================================================ FILE: src/main/java/com/mucommander/ui/layout/CompareImagesPanel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.icon.SpinningDial; import javax.imageio.ImageIO; import javax.swing.*; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.io.IOException; /** * Created on 24/01/14. */ public class CompareImagesPanel extends JPanel { private static final int MAX_PREVIEW_WIDTH = 150; private static final int MAX_PREVIEW_HEIGHT = 100; private final Image[] images = new Image[2]; private final int[] imgWidth = new int[2]; private final int[] imgHeight = new int[2]; private final int[] imgSrcWidth = new int[2]; private final int[] imgSrcHeight = new int[2]; private final JLabel[] lblImageSize = new JLabel[2]; private final JDialog parent; private final ImageIcon arrow; private final SpinningDial[] dials = new SpinningDial[2]; private class LoadImagesTask extends SwingWorker { private final AbstractFile file; private final int index; public LoadImagesTask(AbstractFile file, int index) { super(); this.file = file; this.index = index; } @Override protected Void doInBackground() { if (file == null) { return null; } try { Image image = loadImage(file); imgSrcWidth[index] = image.getWidth(null); imgSrcHeight[index] = image.getHeight(null); image = scaleImage(image); images[index] = image; imgWidth[index] = image.getWidth(null); imgHeight[index] = image.getHeight(null); } catch (Exception e) { e.printStackTrace(); } return null; } @Override protected void done() { synchronized (CompareImagesPanel.this) { if (lblImageSize[index] != null) { lblImageSize[index].setText(imgSrcWidth[index] + " x " + imgSrcHeight[index]); } int panelWidth = 10 + imgWidth[0] + imgWidth[1] + arrow.getIconWidth(); int panelHeight = Math.max(imgHeight[0], imgHeight[1]); panelHeight = Math.max(panelHeight, arrow.getIconHeight()); panelWidth = Math.max(panelWidth, getWidth()); setPreferredSize(new Dimension(panelWidth, panelHeight)); if (dials[index] != null) { dials[index].setAnimated(false); } repaint(); CompareImagesPanel.this.revalidate(); parent.revalidate(); parent.pack(); } } } public CompareImagesPanel(AbstractFile file1, AbstractFile file2, JDialog parent, JLabel lblSize1, JLabel lblSize2) { super(); this.parent = parent; this.lblImageSize[0] = lblSize1; this.lblImageSize[1] = lblSize2; new LoadImagesTask(file1, 0).execute(); new LoadImagesTask(file2, 1).execute(); dials[0] = new SpinningDial(); dials[0].setAnimated(true); if (file2 != null) { dials[1] = new SpinningDial(); dials[1].setAnimated(true); } arrow = IconManager.getIcon(IconManager.IconSet.MISC, "replace_arrow.png"); } @Override public void paint(Graphics g) { super.paint(g); if (images[0] != null) { g.drawImage(images[0], (MAX_PREVIEW_WIDTH-imgWidth[0])/2, (getHeight() - imgHeight[0])/2, null); } else { dials[0].paintIcon(this, g, (MAX_PREVIEW_WIDTH-dials[0].getIconWidth())/2, (getHeight() - dials[0].getIconHeight())/2); } if (images[1] != null) { g.drawImage(images[1], getWidth() - (MAX_PREVIEW_WIDTH+imgWidth[1])/2, (getHeight() - imgHeight[1])/2, null); } else if (dials[1] != null){ dials[1].paintIcon(this, g, getWidth() - (MAX_PREVIEW_WIDTH+dials[1].getIconWidth())/2, (getHeight() - dials[1].getIconHeight())/2); } if (lblImageSize[1] != null) { arrow.paintIcon(this, g, (getWidth() - arrow.getIconWidth())/2, (getHeight() - arrow.getIconHeight())/2); } } private static Image loadImage(AbstractFile file) throws IOException { return ImageIO.read(file.getInputStream()); } private static Image scaleImage(Image src) { final int srcWidth = src.getWidth(null); final int srcHeight = src.getHeight(null); if (srcWidth <= MAX_PREVIEW_HEIGHT && srcHeight <= MAX_PREVIEW_HEIGHT) { return src; } final double zoomX = 1.0 * srcWidth / MAX_PREVIEW_WIDTH; final double zoomY = 1.0 * srcHeight / MAX_PREVIEW_HEIGHT; final double zoom = Math.max(zoomX, zoomY); int outWidth = (int)(1.0 * srcWidth / zoom); int outHeight = (int)(1.0 * srcHeight / zoom); return src.getScaledInstance(outWidth, outHeight, Image.SCALE_FAST); } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/FluentPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import java.awt.Component; import java.awt.LayoutManager; import javax.swing.JPanel; /** * This panel should be used instead of creating temporary panels only for layout purpose. * Using Fluent Interface technique, the add component calls can be chained and the resulting * code becomes more readable. * * Note: The caller to {@link #add(Component)} should be very careful not to assume * the returned object is the object that was just added (as in {@link JPanel#add(Component)}) * * @author Arik Hadas */ public class FluentPanel extends JPanel { public FluentPanel(LayoutManager layoutManager) { super(layoutManager); } @Override public FluentPanel add(Component comp) { super.add(comp); return this; } public FluentPanel add(Component comp, String constraints) { super.add(comp, constraints); return this; } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/InformationPane.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import com.mucommander.ui.text.MultiLineLabel; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; /** * InformationPane is a panel which is suitable for use in dialogs, to give information about the action that the * dialog is about to perform.
    * It is made of 3 components: *

      *
    • A 'main' label which provides a brief description of the action to be performed. * By default, this label uses the standard label's font size and a bold style. *
    • A 'caption' label which is displayed under the main label and provides additional information about the action * to be performed. This label uses a smaller font size and plain style. *
    • An optional icon displayed to the left of labels. InformationPane makes it easy to use one of the standard * JOptionPane icons: error, information, warning, question. *
    * *

    Here is a textual representation of the layout, all components are vertically aligned to the top: *

     *
     * --------------------------
     * | [ICON] | Main label    |
     * |        | Caption label |
     * |________|_______________|
     * 
     * 
    * * @author Maxence Bernard */ public class InformationPane extends JPanel { /** Label used to display the icon, can be null if no icon is used */ private JLabel iconLabel; /** Label used to display the main message */ private final MultiLineLabel mainLabel; /** Label used to display the caption message */ private final MultiLineLabel captionLabel; /** Designates the 'error' predefined icon */ public final static int ERROR_ICON = 1; /** Designates the 'information' predefined icon */ public final static int INFORMATION_ICON = 2; /** DesignateS the 'warning' predefined icon */ public final static int WARNING_ICON = 3; /** Designates the 'question' predefined icon */ public final static int QUESTION_ICON = 4; /** * Creates a new InformationPane with no main and caption message. */ public InformationPane() { this("", ""); } /** * Creates a new InformationPane using the given main and caption messages and no icon. The font style for the main * label is set to Font.BOLD. * * @param mainMessage the message to display in the main label * @param captionMessage the message to display in the caption label */ public InformationPane(String mainMessage, String captionMessage) { this(mainMessage, captionMessage, Font.BOLD, null); } /** * Creates a new InformationPane using the given main message, caption message, main label font style and * predefined icon. * * @param mainMessage the message to display in the main label * @param captionMessage the message to display in the caption label * @param mainMessageFontStyle the font style to use in the main label * @param predefinedIconId an id designating the predefined icon to display, see constant fields for allowed values */ public InformationPane(String mainMessage, String captionMessage, int mainMessageFontStyle, int predefinedIconId) { this(mainMessage, captionMessage, mainMessageFontStyle, getPredefinedIcon(predefinedIconId)); } /** * Creates a new InformationPane using the given main message, caption message, main label font style and icon. * * @param mainMessage the message to display in the main label * @param captionMessage the message to display in the caption label * @param mainMessageFontStyle the font style to use in the main label * @param icon an icon to display, null for no icon */ public InformationPane(String mainMessage, String captionMessage, int mainMessageFontStyle, Icon icon) { super(new BorderLayout()); if (icon != null) { setIcon(icon); } YBoxPanel yPanel = new YBoxPanel(5); mainLabel = new MultiLineLabel(mainMessage); if (mainMessageFontStyle != Font.PLAIN) { setMainLabelFontStyle(mainMessageFontStyle); } yPanel.add(mainLabel); yPanel.addSpace(5); captionLabel = new MultiLineLabel(captionMessage); Font labelFont = mainLabel.getFont(); captionLabel.setFont(labelFont.deriveFont(Font.PLAIN, labelFont.getSize()-2)); yPanel.add(captionLabel); add(yPanel, BorderLayout.CENTER); } /** * Returns an Icon instance corresponding to the given predefined icon id. * * @param predefinedIconId an id designating a predefined icon, see constant fields for allowed values * @return an Icon instance corresponding to the given predefined icon id */ public static Icon getPredefinedIcon(int predefinedIconId) { String optionPaneIcon = getPredefinedIconName(predefinedIconId); if (optionPaneIcon == null) { return null; } return UIManager.getIcon("OptionPane." + optionPaneIcon); } @Nullable private static String getPredefinedIconName(int predefinedIconId) { switch (predefinedIconId) { case ERROR_ICON: return "errorIcon"; case INFORMATION_ICON: return "informationIcon"; case WARNING_ICON: return "warningIcon"; case QUESTION_ICON: return "questionIcon"; default: return null; } } /** * Returns the main label displayed in this InformationPane. * * @return the main label displayed in this InformationPane */ public MultiLineLabel getMainLabel() { return mainLabel; } /** * Returns the caption label displayed in this InformationPane. * * @return the caption label displayed in this InformationPane */ public MultiLineLabel getCaptionLabel() { return captionLabel; } /** * Changes the icon displayed in this InformationPane to the given one. If null is specified, this * InformationPane will not display any icon. * * @param icon the new icon to display, null for no icon */ public void setIcon(Icon icon) { if(icon==null) { if(iconLabel!=null) { remove(iconLabel); iconLabel = null; } } else { if(iconLabel==null) { iconLabel = new JLabel(" "); add(iconLabel, BorderLayout.WEST); } iconLabel.setIcon(icon); } } /** * Changes the font style used by the main label. Allowed values are Font.PLAIN, * Font.BOLD and Font.ITALIC or a mix of those combined as a bitwise mask. * * @param fontStyle the new font style for the main label */ public void setMainLabelFontStyle(int fontStyle) { Font labelFont = mainLabel.getFont(); mainLabel.setFont(labelFont.deriveFont(fontStyle, labelFont.getSize())); } /** * Returns the font style used by the main label as a bitwise mask. * * @return the font style used by the main label as a bitwise mask */ public int getMainLabelFontStyle() { return mainLabel.getFont().getStyle(); } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/ProportionalGridPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import javax.swing.*; import java.awt.*; /** * @author Maxence Bernard */ public class ProportionalGridPanel extends JPanel { private final int nbColumns; private final GridBagConstraints gbc; public ProportionalGridPanel(int nbColumns) { super(new GridBagLayout()); this.nbColumns = nbColumns; gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.insets = new Insets(2, 3, 2, 3); // gbc.gridwidth = 1; // gbc.gridheight = 1; // gbc.ipadx = 0; // gbc.ipady = 0; // gbc.weightx = 0; // gbc.weighty = 0; // gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; } @Override public Component add(Component component) { add(component, gbc); if (gbc.gridx < nbColumns-1) { gbc.gridx++; } else { gbc.gridy++; gbc.gridx = 0; } return component; } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/ProportionalSplitPane.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import com.mucommander.desktop.DesktopManager; import lombok.Getter; import javax.swing.*; import javax.swing.plaf.basic.BasicSplitPaneDivider; import javax.swing.plaf.basic.BasicSplitPaneUI; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * ProportionalSplitPane is a JSplitPane that is able to maintain the divider's location constant proportionally when * the window it is attached to is resized, or when its orientation is changed. * *

    Another added feature is the ability to restore a split ratio of 0.5f (same size for both panels) when * the divider component is double-clicked. * * @author Maxence Bernard */ public class ProportionalSplitPane extends JSplitPane implements ComponentListener, MouseListener { /** Last known width of the window this split pane is attached to. */ private int windowWidth; /** Last known absolute divider location */ private int lastDividerLocation = -1; /** Current proportional divider location, initially 0.5f (same size for both panels) * -- GETTER -- * Returns current pane split ratio. */ @Getter private float splitRatio = 0.5f; /** Window this split pane is attached to */ private Window window; public ProportionalSplitPane(Window window) { super(); init(window, null, null); } public ProportionalSplitPane(Window window, int orientation) { super(orientation); init(window, null, null); } public ProportionalSplitPane(Window window, int orientation, boolean continuousLayout) { super(orientation, continuousLayout); init(window, null, null); } public ProportionalSplitPane(Window window, int orientation, JComponent leftComponent, JComponent rightComponent) { super(orientation, leftComponent, rightComponent); init(window, leftComponent, rightComponent); } public ProportionalSplitPane(Window window, int orientation, boolean continuousLayout, JComponent leftComponent, JComponent rightComponent) { super(orientation, continuousLayout, leftComponent, rightComponent); init(window, leftComponent, rightComponent); } private void init(Window window, JComponent leftComponent, JComponent rightComponent) { this.window = window; window.addComponentListener(this); BasicSplitPaneDivider divider = getDividerComponent(); divider.addComponentListener(this); divider.addMouseListener(this); // Set null minimum size for both components so that divider can be moved all the way left/up and right/down Dimension nullDimension = new Dimension(0,0); if (leftComponent != null) { leftComponent.setMinimumSize(nullDimension); } if (rightComponent != null) { rightComponent.setMinimumSize(nullDimension); } } /** * Updates the divider component's location to keep the current proportional divider location. */ public void updateDividerLocation() { if (!window.isVisible()) { return; } // Reset the last divider location to make sure that the next call to moveComponent doesn't // needlessly recalculate the split ratio. lastDividerLocation = -1; //setDividerLocation((int)(splitRatio*(getOrientation()==HORIZONTAL_SPLIT?getWidth():getHeight()))); setDividerLocation(splitRatio); } /** * Sets the constant, proportional divider's location. The given float but be comprised between 0 and 1, 0 meaning * completely left (or top), 1 right completely (or bottom). * * @param splitRatio the proportional divider's location, comprised between 0 and 1. */ public void setSplitRatio(float splitRatio) { this.splitRatio = splitRatio; updateDividerLocation(); } /** * Returns the split pane divider component. */ public BasicSplitPaneDivider getDividerComponent() { return ((BasicSplitPaneUI)getUI()).getDivider(); } /** * Disables all the JSplitPane accessibility shortcuts that are registered by default: *

      *
    • Navigate in - Tab *
    • Navigate out - Ctrl+Tab, Ctrl+Shift+Tab *
    • Navigate between - Tab, F6 *
    • Give focus splitter bar - F8 *
    • Change size - Arrow keys, home, and end (moves the pane splitter appropriately) *
    */ public void disableAccessibilityShortcuts() { InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); inputMap.clear(); inputMap.setParent(null); } @Override public void setOrientation(int orientation) { super.setOrientation(orientation); // Maintain the divider's proportional location updateDividerLocation(); } /** * Sets the divider location when the ContentPane has been resized so that it stays at the * same proportional (not absolute) location. */ @Override public void componentResized(ComponentEvent e) { Object source = e.getSource(); if (source == window) { // Note: the window/split pane may not be visible when this method is called for the first time // Makes sure that windowWidth is never 0 in #componentMoved. windowWidth = window.getWidth(); updateDividerLocation(); } } @Override public void componentMoved(ComponentEvent e) { if (e.getSource() == getDividerComponent()) { // Ignore this event if the divider's location hasn't changed, or if the initial divider's location // hasn't been set yet if (lastDividerLocation == -1) { lastDividerLocation = getDividerLocation(); return; } else if (lastDividerLocation == getDividerLocation()) { return; } // This is a bit tricky: we want to ignore events triggered by the divider moving because the window was // resized in such a way that it didn't have a choice (window width smaller than current divider location). // Such events are managed by the componentResized method. if (windowWidth != window.getWidth()) { windowWidth = window.getWidth(); return; } // Divider has been moved, calculate new split ratio lastDividerLocation = getDividerLocation(); splitRatio = lastDividerLocation / (float)(getOrientation() == HORIZONTAL_SPLIT?getWidth() : getHeight()); } } @Override public void componentShown(ComponentEvent e) { // Called when the window is made visible if (e.getSource() == window) { // Set initial divider's location updateDividerLocation(); } } @Override public void componentHidden(ComponentEvent e) { } @Override public void mouseClicked(MouseEvent mouseEvent) { if (DesktopManager.isLeftMouseButton(mouseEvent) && mouseEvent.getClickCount() == 2) { setSplitRatio(0.5f); doLayout(); } } @Override public void mousePressed(MouseEvent mouseEvent) { } @Override public void mouseReleased(MouseEvent mouseEvent) { } @Override public void mouseEntered(MouseEvent mouseEvent) { } @Override public void mouseExited(MouseEvent mouseEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/SpringUtilities.java ================================================ /* * Copyright (c) 1995, 2008, Oracle and/or its affiliates. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of Oracle or the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.mucommander.ui.layout; import javax.swing.Spring; import javax.swing.SpringLayout; import java.awt.Component; import java.awt.Container; /** * A 1.4 file that provides utility methods for * creating form- or grid-style layouts with SpringLayout. * These utilities are used by several programs, such as * SpringBox and SpringCompactGrid. */ public class SpringUtilities { /** * A debugging utility that prints to stdout the component's * minimum, preferred, and maximum sizes. */ public static void printSizes(Component c) { System.out.println("minimumSize = " + c.getMinimumSize()); System.out.println("preferredSize = " + c.getPreferredSize()); System.out.println("maximumSize = " + c.getMaximumSize()); } /** * Aligns the first rows * cols * components of parent in * a grid. Each component is as big as the maximum * preferred width and height of the components. * The parent is made just big enough to fit them all. * * @param rows number of rows * @param cols number of columns * @param initialX x location to start the grid at * @param initialY y location to start the grid at * @param xPad x padding between cells * @param yPad y padding between cells */ public static void makeGrid(Container parent, int rows, int cols, int initialX, int initialY, int xPad, int yPad) { SpringLayout layout; try { layout = (SpringLayout)parent.getLayout(); } catch (ClassCastException exc) { System.err.println("The first argument to makeGrid must use SpringLayout."); return; } Spring xPadSpring = Spring.constant(xPad); Spring yPadSpring = Spring.constant(yPad); Spring initialXSpring = Spring.constant(initialX); Spring initialYSpring = Spring.constant(initialY); int max = rows * cols; //Calculate Springs that are the max of the width/height so that all //cells have the same size. Spring maxWidthSpring = layout.getConstraints(parent.getComponent(0)). getWidth(); Spring maxHeightSpring = layout.getConstraints(parent.getComponent(0)). getHeight(); for (int i = 1; i < max; i++) { SpringLayout.Constraints cons = layout.getConstraints( parent.getComponent(i)); maxWidthSpring = Spring.max(maxWidthSpring, cons.getWidth()); maxHeightSpring = Spring.max(maxHeightSpring, cons.getHeight()); } //Apply the new width/height Spring. This forces all the //components to have the same size. for (int i = 0; i < max; i++) { SpringLayout.Constraints cons = layout.getConstraints( parent.getComponent(i)); cons.setWidth(maxWidthSpring); cons.setHeight(maxHeightSpring); } //Then adjust the x/y constraints of all the cells so that they //are aligned in a grid. SpringLayout.Constraints lastCons = null; SpringLayout.Constraints lastRowCons = null; for (int i = 0; i < max; i++) { SpringLayout.Constraints cons = layout.getConstraints( parent.getComponent(i)); if (i % cols == 0) { //start of new row lastRowCons = lastCons; cons.setX(initialXSpring); } else { //x position depends on previous component cons.setX(Spring.sum(lastCons.getConstraint(SpringLayout.EAST), xPadSpring)); } if (i / cols == 0) { //first row cons.setY(initialYSpring); } else { //y position depends on previous row cons.setY(Spring.sum(lastRowCons.getConstraint(SpringLayout.SOUTH), yPadSpring)); } lastCons = cons; } //Set the parent's size. SpringLayout.Constraints pCons = layout.getConstraints(parent); pCons.setConstraint(SpringLayout.SOUTH, Spring.sum( Spring.constant(yPad), lastCons.getConstraint(SpringLayout.SOUTH))); pCons.setConstraint(SpringLayout.EAST, Spring.sum( Spring.constant(xPad), lastCons.getConstraint(SpringLayout.EAST))); } /* Used by makeCompactGrid. */ private static SpringLayout.Constraints getConstraintsForCell( int row, int col, Container parent, int cols) { SpringLayout layout = (SpringLayout) parent.getLayout(); Component c = parent.getComponent(row * cols + col); return layout.getConstraints(c); } /** * Aligns the first rows * cols * components of parent in * a grid. Each component in a column is as wide as the maximum * preferred width of the components in that column; * height is similarly determined for each row. * The parent is made just big enough to fit them all. * * @param rows number of rows * @param cols number of columns * @param initialX x location to start the grid at * @param initialY y location to start the grid at * @param xPad x padding between cells * @param yPad y padding between cells */ public static void makeCompactGrid(Container parent, int rows, int cols, int initialX, int initialY, int xPad, int yPad) { SpringLayout layout; try { layout = (SpringLayout)parent.getLayout(); } catch (ClassCastException exc) { System.err.println("The first argument to makeCompactGrid must use SpringLayout."); return; } //Align all cells in each column and make them the same width. Spring x = Spring.constant(initialX); for (int c = 0; c < cols; c++) { Spring width = Spring.constant(0); for (int r = 0; r < rows; r++) { width = Spring.max(width, getConstraintsForCell(r, c, parent, cols). getWidth()); } for (int r = 0; r < rows; r++) { SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, cols); constraints.setX(x); constraints.setWidth(width); } x = Spring.sum(x, Spring.sum(width, Spring.constant(xPad))); } //Align all cells in each row and make them the same height. Spring y = Spring.constant(initialY); for (int r = 0; r < rows; r++) { Spring height = Spring.constant(0); for (int c = 0; c < cols; c++) { height = Spring.max(height, getConstraintsForCell(r, c, parent, cols). getHeight()); } for (int c = 0; c < cols; c++) { SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, cols); constraints.setY(y); constraints.setHeight(height); } y = Spring.sum(y, Spring.sum(height, Spring.constant(yPad))); } //Set the parent's size. SpringLayout.Constraints pCons = layout.getConstraints(parent); pCons.setConstraint(SpringLayout.SOUTH, y); pCons.setConstraint(SpringLayout.EAST, x); } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/XAlignedComponentPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import javax.swing.*; import javax.swing.text.JTextComponent; import java.awt.*; /** * XAlignedComponentPanel is a panel which makes life easier when having * to display several rows of label + text field. *

    On each row, labels are right-aligned and text fields left-aligned, * looking something like this:
    *

     *       Label1 [Component1]
    * LongerLabel2 [Component2]
    * Label3 [Component3]
    *
    * *

    Vertical space between labels and components, and horizontal space between rows can both be specified. * * @author Maxence Bernard */ public class XAlignedComponentPanel extends JPanel { /** Gridbag layout constraints */ private final GridBagConstraints c; /** Number of pixels between labels and components */ private final int xSpace; /** * First component in this panel, which will be given the focus when focus is requested on this panel using * {@link #requestFocus()}. */ private JComponent firstComponent; public final static int DEFAULT_XSPACE = 5; /** * Creates an initially empty panel, using the default vertical space as defined by {@link #DEFAULT_XSPACE} to * separate labels and components for all rows added later. */ public XAlignedComponentPanel() { this(DEFAULT_XSPACE); } /** * Creates an initially empty panel, using the given vertical space to separate labels and components for * all rows added later. * * @param xSpace number of pixels to be inserted between labels and components. */ public XAlignedComponentPanel(int xSpace) { // Set grid bag layout setLayout(new GridBagLayout()); setAlignmentX(LEFT_ALIGNMENT); // Number of pixels between labels and components this.xSpace = xSpace; // Init gridbag constraints. this.c = new GridBagConstraints(); this.c.anchor = GridBagConstraints.EAST; } public void setLabelLeftAligned(boolean aligned) { this.c.anchor = aligned ? GridBagConstraints.WEST : GridBagConstraints.EAST; } /** * Adds a new row with the given label and component, the component taking all the horizontal space left * by the widest label in this XAlignedComponentPanel. * * @param label text that describes the component * @param component JComponent instance, will take all remaining width space * @param ySpaceAfter number of pixels to be inserted after this row */ public void addRow(String label, JComponent component, int ySpaceAfter) { addRow(new JLabel(label), component, ySpaceAfter); } /** * Adds a new row with the given label and component, the component taking all the horizontal space left * by the widest label in this XAlignedComponentPanel. * * @param label the label component that describes the component * @param component JComponent instance that will take all remaining width space * @param ySpaceAfter number of pixels to be inserted after this row */ public void addRow(JComponent label, JComponent component, int ySpaceAfter) { if (firstComponent == null) { firstComponent = component; } // Prepare grid bag constraints for label c.gridwidth = GridBagConstraints.RELATIVE; c.fill = GridBagConstraints.NONE; c.weightx = 0.0; c.insets = new Insets(0, 0, ySpaceAfter, xSpace); add(label, c); // Prepare grid bag constraints for component c.gridwidth = GridBagConstraints.REMAINDER; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1.0; c.insets = new Insets(0, 0, ySpaceAfter, 0); add(component, c); } /** * Adds a new row with the specified component left-aligned and taking all available width space. * * @param component JComponent instance that will take all available width space * @param ySpaceAfter number of pixels to be inserted after this row */ public void addRow(JComponent component, int ySpaceAfter) { // Prepare grid bag constraints c.gridwidth = GridBagConstraints.REMAINDER; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1.0; c.insets = new Insets(0, 0, ySpaceAfter, 0); add(component, c); } /** * Overrides JPanel#requestFocus() method to request focus on the first component * and select its contents if it is an instance of JTextComponent. */ @Override public void requestFocus() { if (firstComponent == null) super.requestFocus(); else { if (firstComponent instanceof JTextComponent) { JTextComponent textComponent = (JTextComponent) firstComponent; String text = textComponent.getText(); if(!text.isEmpty()) { textComponent.setSelectionStart(0); textComponent.setSelectionEnd(text.length()); } } firstComponent.requestFocus(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/XBoxPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import javax.swing.*; import java.awt.*; /** * Convenience class to make panels with a horizontal BoxLayout easier to use. * * @author Maxence Bernard */ public class XBoxPanel extends JPanel { /** Custom insets, can be null if custom insets haven't been specified with {@link #setInsets(Insets)} */ private Insets insets; /** * Creates a new JPanel with a vertical BoxLayout (BoxLayout.X_AXIS). */ public XBoxPanel() { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); } /** * Creates a new JPanel with a vertical BoxLayout (BoxLayout.X_AXIS) and * adds some initial space to the panel. */ public XBoxPanel(int nbPixels) { setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); add(Box.createRigidArea(new Dimension(nbPixels, 0))); } /** * Aligns the given component on the left and adds it to this panel. */ @Override public Component add(Component comp) { if (comp instanceof JComponent) { ((JComponent) comp).setAlignmentX(LEFT_ALIGNMENT); } return super.add(comp); } /** * Adds a vertical separation of the given size to this panel. */ public void addSpace(int nbPixels) { add(Box.createRigidArea(new Dimension(nbPixels, 0))); } /** * Sets this panel's insets. */ public void setInsets(Insets insets) { this.insets = insets; } /** * Returns this panel's insets. */ @Override public Insets getInsets() { return insets == null ? super.getInsets() : insets; } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/YBoxPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.layout; import javax.swing.*; import java.awt.*; /** * Convenience class to make use of panels with a vertical BoxLayout simpler. * * @author Maxence Bernard */ public class YBoxPanel extends JPanel { /** Custom insets, can be null if custom insets haven't been specified with {@link #setInsets(Insets)} */ private Insets insets; /** * Creates a new JPanel with a vertical BoxLayout (BoxLayout.Y_AXIS). */ public YBoxPanel() { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); } /** * Creates a new JPanel with a vertical BoxLayout (BoxLayout.Y_AXIS) and * adds some initial space to the panel. */ public YBoxPanel(int nbVertSpace) { setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); add(Box.createRigidArea(new Dimension(0, nbVertSpace))); } /** * Aligns the given component on the left and adds it to this panel. */ @Override public Component add(Component comp) { if (comp instanceof JComponent) { ((JComponent) comp).setAlignmentX(LEFT_ALIGNMENT); } return super.add(comp); } /** * Adds a vertical separation of the given size to this panel. */ public void addSpace(int nbVertSpace) { add(Box.createRigidArea(new Dimension(0, nbVertSpace))); } /** * Adds the given component to this panel, inserting the specified amount of horizontal * space before the component. */ @Override public Component add(Component component, int nbHorizSpace) { JPanel tempPanel = new XBoxPanel(nbHorizSpace); tempPanel.add(component); return add(tempPanel); } /** * Sets this panel's insets. */ public void setInsets(Insets insets) { this.insets = insets; } /** * Returns this panel's insets. */ @Override public Insets getInsets() { return insets == null ? super.getInsets() : insets; } } ================================================ FILE: src/main/java/com/mucommander/ui/layout/package.html ================================================ Utility classes meant to help with Swing component layout. ================================================ FILE: src/main/java/com/mucommander/ui/list/DynamicHorizontalWrapList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.list; import com.mucommander.commons.collections.AlteredVector; import javax.swing.*; import java.awt.*; /** * This class represent a DynamicList with a following horizontal-wrap layout: *

     *     1   2   3
     *     4   5   6
     *     7   8   9
     *     10..
     * 
    * * In which the number of items per-row is dynamically determined by the * width of the list and the given item-width parameter. * * @author Arik Hadas */ public class DynamicHorizontalWrapList extends DynamicList { // The width of each item in the list private final int itemWidth; // Saves the last width of the parent container to detect if there // should be a change in the number of items per-row. private int lastParentWidth; public DynamicHorizontalWrapList(AlteredVector items, int itemWidth) { this(items, itemWidth, 0); } public DynamicHorizontalWrapList(AlteredVector items, int itemWidth, int horizontalPadding) { super(items); this.itemWidth = itemWidth + horizontalPadding; setLayoutOrientation(JList.HORIZONTAL_WRAP); } @Override public void repaint() { Container parent = getParent(); if (parent != null) { Rectangle parentBounds = parent.getBounds(); int parentWidth = parentBounds.width; if (lastParentWidth != parentWidth) { lastParentWidth = parentWidth; int itemsPerRow = parentWidth / itemWidth; setFixedCellWidth(itemWidth + ((parentWidth - itemWidth * itemsPerRow)/ itemsPerRow)); setVisibleRowCount(getComponentCount() / itemsPerRow); } } super.repaint(); } } ================================================ FILE: src/main/java/com/mucommander/ui/list/DynamicList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.list; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.commons.collections.VectorChangeListener; import com.mucommander.utils.text.Translator; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; /** * DynamicList extends JList to work with an {@link AlteredVector} of items which values can be dynamically modified * and automatically reflected in the list, also keeping the current selection consistent. * *

    It also provides actions to: *

      *
    • move the currently selected item up (mapped to 'Shift+UP' and 'Shift+LEFT') *
    • move the currently selected item down (mapped to 'Shift+DOWN' and 'Shift+RIGHT') *
    • remove the currently selected item and selects the previous one (if any) (mapped to 'DELETE' and 'BACKSPACE') *
    * *

    This list only works in 'single selection mode', that means only one item can be selected at a time. * * @author Maxence Bernard */ public class DynamicList extends JList { /** Items displayed in the JList */ private final AlteredVector items; /** Custom ListModel that handles modifications made to the AlteredVector */ private final DynamicListModel model; /** Action instance which moves the currently selected item up when triggered */ private final MoveUpAction moveUpAction; /** Action instance which moves the currently selected item down when triggered */ private final MoveDownAction moveDownAction; /** Action instance which, when triggered, removes the currently selected item from the list * and selects the previous item (if any). */ private final RemoveAction removeAction; /** * Custom ListModel that handles modifications made to the AlteredVector and reflect them changes in the JList. */ private class DynamicListModel extends AbstractListModel implements VectorChangeListener { public int getSize() { return items.size(); } public E getElementAt(int i) { if (i < 0 || i >= items.size()) { return null; } return items.elementAt(i); } private void notifyAdded(int fromIndex, int toIndex) { fireIntervalAdded(this, fromIndex, toIndex); } private void notifyRemoved(int fromIndex, int toIndex) { fireIntervalRemoved(this, fromIndex, toIndex); } private void notifyModified(int index) { fireContentsChanged(this, index, index); } ////////////////////////// // VectorChangeListener // ////////////////////////// public void elementsAdded(int startIndex, int nbAdded) { model.notifyAdded(startIndex, startIndex+nbAdded-1); } public void elementsRemoved(int startIndex, int nbRemoved) { model.notifyRemoved(startIndex, startIndex+nbRemoved-1); } public void elementChanged(int index) { model.notifyModified(index); } } /** * Action which moves the currently selected item up when triggered. */ private class MoveUpAction extends AbstractAction { private MoveUpAction() { } public void actionPerformed(ActionEvent actionEvent) { moveItem(getSelectedIndex(), true); // Request focus back on the list requestFocus(); } } /** * Action which moves the currently selected item down when triggered. */ private class MoveDownAction extends AbstractAction { private MoveDownAction() { } public void actionPerformed(ActionEvent actionEvent) { moveItem(getSelectedIndex(), false); // Request focus back on the list requestFocus(); } } /** * Action which, when triggered, removes the currently selected item from the list and selects the previous item (if any). */ private class RemoveAction extends AbstractAction { private RemoveAction() { putValue(Action.NAME, Translator.get("delete")); } public void actionPerformed(ActionEvent actionEvent) { int selectedIndex = getSelectedIndex(); if (!isIndexValid(selectedIndex)) return; items.removeElementAt(selectedIndex); // Select previous item (if there is one) and make sure it is visible. int nbItems = items.size(); if (nbItems > 0) selectAndScroll(Math.min(selectedIndex, nbItems-1)); // Request focus back on the list requestFocus(); } } /** * Creates a new DynamicList using the items stored in the given {@link AlteredVector}. * These items (if any) will be visible whenever this list is visible, and the first item (if any) will be selected. * *

    Any change made to the AlteredVector will be automatically reflected in the list, except for changes * made to the item instances themselves for which {@link #itemModified(int, boolean)} will need to * be called explicitly. * * @param items items to add to the list */ public DynamicList(AlteredVector items) { this.items = items; // Use a custom ListModel this.model = new DynamicListModel(); setModel(model); // Listen to changes made to the Vector this.items.addVectorChangeListener(model); // Allow only one item to be selected at a time setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // Select first item, if there is at least one if (!items.isEmpty()) { setSelectedIndex(0); } // create action instances this.moveUpAction = new MoveUpAction(); this.moveDownAction = new MoveDownAction(); this.removeAction = new RemoveAction(); InputMap inputMap = getInputMap(); ActionMap actionMap = getActionMap(); // Map 'Delete' and 'Backspace' to RemoveAction Class actionClass = removeAction.getClass(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), actionClass); actionMap.put(actionClass, removeAction); // Map 'Shift+Up'/'Meta+Up' and 'Shift+Left'/'Meta+Left' to MoveUpAction actionClass = moveUpAction.getClass(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_DOWN_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.META_DOWN_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.SHIFT_DOWN_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, KeyEvent.META_DOWN_MASK), actionClass); actionMap.put(actionClass, moveUpAction); // Map 'Shift+Down'/'Meta+Down' and 'Shift+Right'/'Meta+Right' to MoveDownAction actionClass = moveDownAction.getClass(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.SHIFT_DOWN_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.META_DOWN_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.SHIFT_DOWN_MASK), actionClass); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, KeyEvent.META_DOWN_MASK), actionClass); actionMap.put(actionClass, moveDownAction); } /** * Returns the items displayed by this DynamicList. */ public AlteredVector getItems() { return items; } /** * Selects the item located at the given index and if necessary scrolls the list to make sure * that the new selection is visible within the viewport. * * @param index index of the item to select */ public void selectAndScroll(int index) { setSelectedIndex(index); ensureIndexIsVisible(index); } /** * Returns true if the given index is without the bounds of the items Vector. * * @param index index to test * @return true if the given index is without the bounds of the items. */ public boolean isIndexValid(int index) { return index >= 0 && index < items.size(); } /** * This method should be called whenever an item in the items vector has been modified in order to properly * repaint the list and reflect the change. * * @param index index of the item in the Vector that has been modified * @param selectItem if true, the modified item will be selected */ public void itemModified(int index, boolean selectItem) { // Make sure that the given index is not out of bounds if (!isIndexValid(index)) { return; } // Notify ListModel in order to properly repaint list model.notifyModified(index); } /** * Moves the item located at the given index up or down, swapping its place with the previous or next item. * * @param index the item to move * @param moveUp if true the item at the given index will be moved up, if not moved down */ private void moveItem(int index, boolean moveUp) { // Make sure that the given index is not out of bounds if (!isIndexValid(index)) { return; } int newIndex; // Calculate the new index for the item to move if (moveUp) { // Item is already at the top, do nothing if (index < 1) { return; } newIndex = index-1; } else { // Item is already at the bottom, do nothing if (index >= items.size()-1) { return; } newIndex = index+1; } // Swap values in the Vector E tmp = items.elementAt(index); items.setElementAt(items.elementAt(newIndex), index); items.setElementAt(tmp, newIndex); // Select moved item and make sure it is visible selectAndScroll(newIndex); } /** * Returns an Action that can be used for instance in a JButton to * move the item currently selected item up, swapping it with the previous item. * * @return an Action that moves the currently selected item up. */ Action getMoveUpAction() { return moveUpAction; } /** * Returns an Action that can be used for instance in a JButton to * move the item currently selected item down, swapping it with the following item. * * @return an Action that moves the currently selected item down. */ Action getMoveDownAction() { return moveDownAction; } /** * Returns an Action that can be used for instance in a JButton to * remove the currently selected item. * * @return an Action that removes the currently selected item. */ public Action getRemoveAction() { return removeAction; } } ================================================ FILE: src/main/java/com/mucommander/ui/list/FileList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.list; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import javax.swing.*; import java.awt.*; /** * FileList is a JList that displays information about a list of files: each row displays a file's name * and icon. * *

    Since all AbstractFile methods are I/O bound and potentially lock-prone, it is not a good idea to * call them on request from the main event thread. To work around this, the constructor can preload all the information * subsequently needed by this list. This has a cost since all files will have to queried at init time, even if some * are not used (displayed) afterwards. On the other hand, navigation throughout the list will be faster. * Preloading can be enabled or disabled in the constructor but it should always enabled unless it is * known for certain that the underlying files are not I/O bound and cannot lock. * * @author Maxence Bernard */ public class FileList extends JList { /** Files to display */ protected FileSet files; /** True if file attribute preloading has been enabled */ protected boolean fileAttributesPreloaded; /** Preloaded filenames, null if preloading is not enabled */ protected String[] filenames; /** Preloaded file icons, null if preloading is not enabled */ protected Icon[] icons; /** Custom font by the JLabel */ protected Font customFont; /** * Creates a new FileList where each file in the given {@link FileSet} is displayed on a separate row. * * @param files the set of files to display * @param preloadFileAttributes enables/disables file attribute preloading. It should always enabled unless it is known * for certain that the underlying files are not I/O bound and cannot lock. */ public FileList(final FileSet files, boolean preloadFileAttributes) { this.files = files; int nbFiles = files.size(); // Very important: allows the JList to operate in fixed cell height mode, which makes it substantially faster // to initialize when there is a large number of rows. if (nbFiles > 0) { setPrototypeCellValue(files.elementAt(0)); } if (preloadFileAttributes) { filenames = new String[nbFiles]; icons = new Icon[nbFiles]; AbstractFile file; for(int i=0; i() { public int getSize() { return files.size(); } public AbstractFile getElementAt(int index) { return files.elementAt(index); } }); customFont = new JLabel().getFont(); customFont = customFont.deriveFont(customFont.getStyle(), customFont.getSize()-2); // Use a custom ListCellRenderer setCellRenderer(new DefaultListCellRenderer() { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); label.setFont(customFont); if (FileList.this.fileAttributesPreloaded) { label.setText(filenames[index]); label.setIcon(icons[index]); } else { AbstractFile file = (AbstractFile)value; label.setText(file.getName()); label.setIcon(file.getIcon()); } return label; } }); } } ================================================ FILE: src/main/java/com/mucommander/ui/list/SortableListPanel.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.list import com.mucommander.commons.collections.AlteredVector import com.mucommander.ui.button.ArrowButton import com.mucommander.utils.text.Translator import java.awt.BorderLayout import java.awt.Color import java.awt.Dimension import java.awt.GridLayout import javax.swing.JPanel import javax.swing.JScrollPane /** * SortableListPanel is a JPanel which contains a scrollable [DynamicList] in the center and two buttons * 'Move up' and 'Move down' buttons on the right side of the list which allow to move the items up and down and * easily reorder them within the list. * * @author Maxence Bernard */ class SortableListPanel( items: AlteredVector ) : JPanel(BorderLayout()) { /** * Returns the [DynamicList] used by this SortableListPanel. */ @JvmField val dynamicList: DynamicList = DynamicList(items) /** * Creates a new SortableListPanel with a [DynamicList] that uses the provided items [AlteredVector]. */ init { add( JScrollPane(dynamicList), BorderLayout.CENTER ) val buttonPanel = JPanel(GridLayout(2, 1)).apply { add( ArrowButton(dynamicList.getMoveUpAction(), ArrowButton.Direction.UP).apply { preferredSize = Dimension(19, 0) // Constrain the button's size which by default is huge under Windows/Java 1.5 setFocusable(false) // Make the button non-focusable so that it doesn't steal focus from the list setToolTipText(Translator.get("sortable_list.move_up")) } ) add( ArrowButton(dynamicList.getMoveDownAction(), ArrowButton.Direction.DOWN).apply { preferredSize = Dimension(19, 0) // Constrain the button's size which by default is huge under Windows/Java 1.5 setFocusable(false) // Make the button non-focusable so that it doesn't steal focus from the list setToolTipText(Translator.get("sortable_list.move_down")) } ) } add(buttonPanel, BorderLayout.EAST) } } ================================================ FILE: src/main/java/com/mucommander/ui/list/package.html ================================================ Contains various components used to deal with JList. ================================================ FILE: src/main/java/com/mucommander/ui/macosx/AppleScript.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.macosx; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import com.mucommander.commons.file.AbstractFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.process.AbstractProcess; import com.mucommander.process.ProcessListener; import com.mucommander.process.ProcessRunner; /** * This class allows to init AppleScript code under Mac OS X, relying on the osacript command available * that comes with any install of Mac OS X. This command is used instead of the Cocoa-Java library which has been * deprecated by Apple.
    * Calls to {@link #execute(String, StringBuilder)} on any OS other than Mac OS X will always fail. * *

    * Important notes about character encoding: *

      *
    • AppleScript 1.10- (Mac OS X 10.4 or lower) expects MacRoman encoding, not UTF-8. That * means the script should only contain characters that are part of the MacRoman charset; any character * that cannot be expressed in MacRoman will not be properly interpreted. * The only way to pass Unicode text to a script is by reading it from a file. * See http://www.satimage.fr/software/en/unicode_and_applescript.html * for more information on how to do so. *
    • *
    • AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and will properly interpret any Unicode * character: "AppleScript is now entirely Unicode-based. Comments and text constants in scripts may contain * any Unicode characters, and all text processing is done in Unicode". * See http://www.apple.com/applescript/features/unicode.html * for more information. *
    • *
    * * @author Maxence Bernard */ public class AppleScript { private static final Logger LOGGER = LoggerFactory.getLogger(AppleScript.class); /** The UTF-8 encoding */ public final static String UTF8 = "UTF-8"; /** The MacRoman encoding */ private final static String MACROMAN = "MacRoman"; /** * Executes the given AppleScript and returns true if it completed its execution normally, i.e. without * any error. * The script's output is accumulated in the given StringBuilder. If the script completed its execution * normally, the buffer will contain the script's standard output. If the script failed because of an error in it, * the buffer will contain details about the error. * *

    If the caller is not interested in the script's output, a null value can be passed which will * speed the execution up a little. * * @param appleScript the AppleScript to execute * @param outputBuffer the StringBuilder that will hold the script's output, null for no output * @return true if the script was successfully executed, false if the */ public static boolean execute(String appleScript, StringBuilder outputBuffer) { return execute(appleScript, outputBuffer, null); } public static boolean execute(String appleScript, StringBuilder outputBuffer, AbstractFile currentDirectory) { // No point in going any further if the current OS is not Mac OS X if (!OsFamily.MAC_OS_X.isCurrent()) { return false; } LOGGER.debug("Executing AppleScript: {}", appleScript); // Use the 'osascript' command to execute the AppleScript. The '-s o' flag tells osascript to print errors to // stdout rather than stderr. The AppleScript is piped to the process instead of passing it as an argument // ('-e' flag), for better control over the encoding and to remove any limitations on the maximum script size. String[] tokens = new String[] { "osascript", "-s", "o", }; try { // Execute the osascript command. ProcessListener processListener = outputBuffer == null ? null : new ScriptOutputListener(outputBuffer, AppleScript.getScriptEncoding()); AbstractProcess process = ProcessRunner.execute(tokens, currentDirectory, processListener, null); // Pipe the script to the osascript process. try (OutputStreamWriter pout = new OutputStreamWriter(process.getOutputStream(), getScriptEncoding())) { pout.write(appleScript); } // Wait for the process to die int returnCode = process.waitFor(); LOGGER.debug("osascript returned code={}, output={}", returnCode, outputBuffer); if (returnCode != 0) { LOGGER.debug("osascript terminated abnormally"); return false; } return true; } catch(Exception e) { // IOException, InterruptedException // Shouldn't normally happen LOGGER.debug("Unexcepted exception while executing AppleScript", e); return false; } } /** * Returns the encoding that AppleScript uses on the current runtime environment: *

      *
    • {@link #UTF8} for AppleScript 2.0+ (Mac OS X 10.5 and up)
    • *
    • {@link #MACROMAN} for AppleScript 1.10- (Mac OS X 10.4 or lower)
    • *
    * * If {@link #MACROMAN} is used, the scripts passed to {@link #execute(String, StringBuilder)} should not contain * characters that are not part of the MacRoman charset or they will not be properly interpreted. * * @return the encoding that AppleScript uses on the current runtime environment */ public static String getScriptEncoding() { // - AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and expects a script in UTF-8 encoding. // - AppleScript 1.3- (Mac OS X 10.4 or lower) expects MacRoman encoding, not UTF-8. return OsVersion.MAC_OS_X_10_5.isCurrentOrHigher() ? UTF8 : MACROMAN; } /** * This ProcessListener accumulates the output of the 'osascript' command and suppresses the trailing '\n' character * from the script's output. */ private static class ScriptOutputListener implements ProcessListener { private final StringBuilder outputBuffer; private final String outputEncoding; private ScriptOutputListener(StringBuilder outputBuffer, String outputEncoding) { this.outputBuffer = outputBuffer; this.outputEncoding = outputEncoding; } @Override public void processOutput(byte[] buffer, int offset, int length) { try { outputBuffer.append(new String(buffer, offset, length, outputEncoding)); } catch(UnsupportedEncodingException e) { // The encoding is necessarily supported } } @Override public void processOutput(String s) { } @Override public void processDied(int returnValue) { // Remove the trailing "\n" character that osascript returns. int len = outputBuffer.length(); if (len > 0 && outputBuffer.charAt(len-1) == '\n') { outputBuffer.setLength(len - 1); } } } // The following commented method executes an AppleScript using the deprecated Cocoa-Java library. // We're now using the 'osascript' command instead, but this method is kept for the record in case Apple one day // decides to un-deprecate the Cocoa-Java library. // /** // * Executes the given AppleScript and returns the script's output if it was successfully executed, null // * if the script couldn't be compiled or if an error occurred while executing it. // * An empty string "" is returned if the script doesn't output anything. // * // * @param appleScript the AppleScript to compile and execute // * @return the script's output, null if an error occurred while compiling or executing the script // */ // private static String executeAppleScript(String appleScript) { // AppLogger.finer("Executing AppleScript "+appleScript); // // int pool = -1; // // try { // // Quote from Apple Cocoa-Java doc: // // An autorelease pool is used to manage Foundation’s autorelease mechanism for Objective-C objects. // // NSAutoreleasePool provides Java applications access to autorelease pools. Typically it is not // // necessary for Java applications to use NSAutoreleasePools since Java manages garbage collection. // // However, some situations require an autorelease pool; for instance, if you start off a thread that // // calls Cocoa, there won’t be a top-level pool. // pool = NSAutoreleasePool.push(); // // NSMutableDictionary errorInfo = new NSMutableDictionary(); // NSAppleEventDescriptor eventDescriptor = new NSAppleScript(appleScript).execute(errorInfo); // if(eventDescriptor==null) { // AppLogger.fine("Caught AppleScript error: "+errorInfo.objectForKey(NSAppleScript.AppleScriptErrorMessage)); // // return null; // } // // String output = eventDescriptor.stringValue(); // Returns null if the script didn't output anything // AppLogger.finer("AppleScript output="+output); // // return output==null?"":output; // } // catch(Error e) { // // Can happen if Cocoa-java is not in the classpath // AppLogger.fine("Unexcepted error while executing AppleScript (cocoa-java not available?)", e); // // return null; // } // catch(Exception e) { // // Try block is not supposed to throw any exception, but this is low-level stuff so just to be safe // AppLogger.fine("Unexcepted exception while executing AppleScript", e); // // return null; // } // finally { // if(pool!=-1) // NSAutoreleasePool.pop(pool); // } // } } ================================================ FILE: src/main/java/com/mucommander/ui/macosx/AppleScriptBuilder.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.macosx; public class AppleScriptBuilder { private final StringBuilder sb = new StringBuilder(); public AppleScriptBuilder append(String s) { sb.append(s); return this; } public AppleScriptBuilder nl() { sb.append('\n'); return this; } public AppleScriptBuilder appendQuoted(String s) { sb.append('"'); sb.append(s); sb.append('"'); return this; } @Override public String toString() { return sb.toString(); } } ================================================ FILE: src/main/java/com/mucommander/ui/macosx/EAWTHandler.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.macosx; import com.mucommander.TrolCommander; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.WindowManager; import java.awt.*; import java.awt.desktop.*; import java.io.File; /** * This class registers the About, Preferences and Quit handlers using the com.apple.eawt API available */ class EAWTHandler { EAWTHandler() { Desktop desktop = Desktop.getDesktop(); desktop.setAboutHandler(this::showAbout); desktop.setPreferencesHandler(this::showPreferences); desktop.setQuitHandler(this::handleQuitRequestWith); desktop.setOpenFileHandler(this::openFiles); } private void showAbout(AboutEvent e) { OSXIntegration.showAbout(); } private void showPreferences(PreferencesEvent e) { OSXIntegration.showPreferences(); } private void handleQuitRequestWith(QuitEvent quitEvent, QuitResponse quitResponse) { if (OSXIntegration.doQuit()) { quitResponse.performQuit(); } else { quitResponse.cancelQuit(); } } public void openFiles(OpenFilesEvent openFilesEvent) { // Wait until the application has been launched. This step is required to properly handle the case where the // application is launched with a file to open, for instance when drag-n-dropping a file to the Dock icon // when trolCommander is not started yet. In this case, this method is called while Launcher is still busy // launching the application (no mainframe exists yet). TrolCommander.waitUntilLaunched(); for (File f : openFilesEvent.getFiles()) { AbstractFile file = FileFactory.getFile(f.toString()); if (file == null) { continue; } FolderPanel activePanel = WindowManager.getCurrentMainFrame().getActivePanel(); if (file.isBrowsable()) { activePanel.tryChangeCurrentFolder(file); } else { activePanel.tryChangeCurrentFolder(file.getParent(), file, false); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/macosx/IMacOsWindow.java ================================================ package com.mucommander.ui.macosx; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import javax.swing.*; public interface IMacOsWindow extends RootPaneContainer { default void initLookAndFeel() { if (OsVersion.MAC_OS_X_10_4.isCurrentOrLower() || OsVersion.MAC_OS_X_10_13.isCurrentOrHigher()) { getRootPane().putClientProperty("apple.awt.brushMetalLook", TcConfigurations.getPreferences().getVariable(TcPreference.USE_BRUSHED_METAL, TcPreferences.DEFAULT_USE_BRUSHED_METAL)); } } } ================================================ FILE: src/main/java/com/mucommander/ui/macosx/OSXIntegration.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.macosx; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.dialog.about.AboutDialog; import com.mucommander.ui.dialog.shutdown.QuitDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; /** * This class handles Mac OS X specifics when muCommander is started: *
      *
    • Turns on/off brush metal based on preferences (default is on) *
    • Turns screen menu bar based on preferences (default is on, no GUI for that pref) *
    • Registers handlers for the 'About', 'Preferences' and 'Quit' application menu items *
    * *

    The com.apple.eawt API is used to handle 'About', 'Preferences' and 'Quit' events and report back to the OS. * * @see EAWTHandler * @author Maxence Bernard */ public class OSXIntegration { public OSXIntegration() { if (!OsFamily.MAC_OS_X.isCurrent()) { return; } // At the time of writing, the 'brushed metal' look causes the JVM to crash randomly under Leopard (10.5) // so we disable brushed metal on that OS version but leave it for earlier versions where it works fine. // See http://www.mucommander.com/forums/viewtopic.php?f=4&t=746 for more info about this issue. if (OsVersion.MAC_OS_X_10_4.isCurrentOrLower() || OsVersion.MAC_OS_X_10_13.isCurrentOrHigher()) { // Turn on/off brush metal look (default is off because still buggy when scrolling and panning dialog windows) : // "Allows you to display your main windows with the 'textured' Aqua window appearance. // This property should be applied only to the primary application window, // and should not affect supporting windows like dialogs or preference windows." System.setProperty("apple.awt.brushMetalLook", ""+ TcConfigurations.getPreferences().getVariable(TcPreference.USE_BRUSHED_METAL, TcPreferences.DEFAULT_USE_BRUSHED_METAL)); } // Enables/Disables screen menu bar (default is on) : // "if you are using the Aqua look and feel, this property puts Swing menus in the Mac OS X menu bar." System.setProperty("apple.laf.useScreenMenuBar", ""+ TcConfigurations.getPreferences().getVariable(TcPreference.USE_SCREEN_MENU_BAR, TcPreferences.DEFAULT_USE_SCREEN_MENU_BAR)); // Catch 'About', 'Preferences' and 'Quit' events try { new EAWTHandler(); } catch (Throwable t) { t.printStackTrace(); } } /** * Shows the 'About' dialog. */ static void showAbout() { MainFrame mainFrame = WindowManager.getCurrentMainFrame(); // Do nothing (return) when in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } new AboutDialog(mainFrame).showDialog(); } /** * Shows the 'Preferences' dialog. */ static void showPreferences() { MainFrame mainFrame = WindowManager.getCurrentMainFrame(); // Do nothing (return) when in 'no events mode' if (mainFrame == null || mainFrame.getNoEventsMode()) { return; } ActionManager.performAction(com.mucommander.ui.action.impl.ShowPreferencesAction.Descriptor.ACTION_ID, mainFrame); } /** * Quits the application after displaying a confirmation dialog if it hasn't been disabled * in the preferences. Return true if the operation has been aborted by user. */ static boolean doQuit() { // Ask the user for confirmation and abort if user refused to quit. if (!QuitDialog.confirmQuit()) { return false; } // We got a green -> quit! WindowManager.quit(); return true; } } ================================================ FILE: src/main/java/com/mucommander/ui/macosx/TabbedPaneUICustomizer.java ================================================ package com.mucommander.ui.macosx; import java.awt.Graphics; import java.awt.Insets; import javax.swing.JTabbedPane; import javax.swing.plaf.TabbedPaneUI; import com.apple.laf.AquaTabbedPaneContrastUI; import com.apple.laf.AquaTabbedPaneUI; public class TabbedPaneUICustomizer { private static final Insets EMPTY_INSETS = new Insets(0, 0, 0, 0); private static final Insets CONTENT_BORDER_INSETS = new Insets(5, 0, 0, 0); public static void customizeTabbedPaneUI(JTabbedPane tabbedPane) { TabbedPaneUI tabbedPaneUI = tabbedPane.getUI(); if (tabbedPaneUI instanceof AquaTabbedPaneContrastUI) { tabbedPane.setUI(new CompactAquaTabbedPaneContrastUI()); } else if (tabbedPaneUI instanceof AquaTabbedPaneUI) { tabbedPane.setUI(new CompactAquaTabbedPaneUI()); } } private static class CompactAquaTabbedPaneUI extends AquaTabbedPaneUI { @Override protected Insets getContentBorderInsets(final int arg0) { return CONTENT_BORDER_INSETS; } @Override protected Insets getTabAreaInsets(final int arg0) { return EMPTY_INSETS; } @Override protected Insets getContentDrawingInsets(final int arg0) { return EMPTY_INSETS; } /** * No content border */ @Override protected void paintContentBorder(final Graphics g, final int tabPlacement, final int selectedIndex) { } } private static class CompactAquaTabbedPaneContrastUI extends AquaTabbedPaneContrastUI { @Override protected Insets getContentBorderInsets(int arg0) { return CONTENT_BORDER_INSETS; } @Override protected Insets getTabAreaInsets(int arg0) { return EMPTY_INSETS; } @Override protected Insets getContentDrawingInsets(int arg0) { return EMPTY_INSETS; } /** * No content border */ @Override protected void paintContentBorder(final Graphics g, final int tabPlacement, final int selectedIndex) { } } } ================================================ FILE: src/main/java/com/mucommander/ui/macosx/package.html ================================================ OSX UI integration. ================================================ FILE: src/main/java/com/mucommander/ui/main/BreadcrumbBar.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License* along with this program. * If not, see . */ package com.mucommander.ui.main import com.mucommander.commons.file.AbstractFile import com.mucommander.ui.theme.* import java.awt.Cursor import java.awt.FlowLayout import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.util.* import javax.swing.JLabel import javax.swing.JTextField /** * Renders the current directory as a horizontal row of hyperlink-style labels separated by `›` glyphs. * Clicking a label navigates the owning [FolderPanel] to the corresponding ancestor directory. * * * Extends [JTextField] (rather than `JPanel`) so that the look-and-feel paints the correct native border automatically. * On macOS Aqua the border renderer checks `instanceof JTextComponent`; a plain `JPanel` would not receive the beveled * round-rect treatment. * * [.paintComponent] is overridden to suppress text rendering and just fill the interior with the background color. * * Uses [AbstractFile.getParent] to walk the hierarchy, so it works uniformly for local paths (Windows, Unix) and remote file systems (SFTP, FTP…). */ internal class BreadcrumbBar(private val folderPanel: FolderPanel) : JTextField(), ThemeListener { init { isEditable = false setFocusable(false) setLayout(FlowLayout(FlowLayout.LEFT, 0, 0)) setBackground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_BACKGROUND_COLOR)) ThemeManager.addCurrentThemeListener(this) } /** * Rebuilds the breadcrumb labels for the given [AbstractFile]. * Walks up via [AbstractFile.getParent] to collect all ancestors, then renders them root-first. */ fun setFile(file: AbstractFile?) { removeAll() if (file == null) { revalidate() repaint() return } // Collect ancestors from current directory up to the root val stack: Deque = ArrayDeque() var f: AbstractFile? = file while (f != null) { stack.push(f) // push → top of deque is the root after the loop f = f.getParent() } var first = true for (ancestor in stack) { if (!first) { add(makeSeparatorLabel()) } first = false val isLast = stack.peekLast() === ancestor if (isLast) { add(makePlainLabel(ancestor.getName())) } else { add(makeLinkLabel(ancestor.getName(), ancestor.absolutePath)) } } revalidate() repaint() } /** A label that looks and behaves like a hyperlink. */ private fun makeLinkLabel(text: String, targetPath: String?): JLabel { val escapedText = escapeHtml(text) val normalContent = "$escapedText" val hoverContent = "$escapedText" return JLabel(normalContent).apply { setForeground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_FOREGROUND_COLOR)) setFont(ThemeManager.getCurrentFont(Theme.LOCATION_BAR_FONT)) setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)) setBackground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_BACKGROUND_COLOR)) addMouseListener(object : MouseAdapter() { override fun mousePressed(e: MouseEvent?) { folderPanel.tryChangeCurrentFolder(targetPath) } override fun mouseEntered(e: MouseEvent?) = setText(hoverContent) override fun mouseExited(e: MouseEvent?) = setText(normalContent) }) } } /** A plain (non-clickable) label for the current directory segment. */ private fun makePlainLabel(text: String): JLabel = JLabel("" + escapeHtml(text) + "").apply { setForeground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_FOREGROUND_COLOR)) setFont(ThemeManager.getCurrentFont(Theme.LOCATION_BAR_FONT)) setBackground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_BACKGROUND_COLOR)) } private fun makeSeparatorLabel(): JLabel = JLabel(SEPARATOR_GLYPH).apply { setForeground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_FOREGROUND_COLOR)) setFont(ThemeManager.getCurrentFont(Theme.LOCATION_BAR_FONT)) setBackground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_BACKGROUND_COLOR)) } /** Escapes the minimal HTML special characters that can appear in file names. */ private fun escapeHtml(text: String): String { return text.replace("&", HTML_AMP) .replace("<", HTML_LT) .replace(">", HTML_GT) } override fun colorChanged(event: ColorChangedEvent) { if (event.colorId == Theme.LOCATION_BAR_BACKGROUND_COLOR) { setBackground(event.color) } } override fun fontChanged(event: FontChangedEvent?) { } companion object { /** The `›` glyph rendered between path segments. */ private const val SEPARATOR_GLYPH = " \u203A " private const val HTML_AMP = "&" private const val HTML_LT = "<" private const val HTML_GT = ">" } } ================================================ FILE: src/main/java/com/mucommander/ui/main/ConfigurableFolderFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreferences; import com.mucommander.conf.TcPreferencesAPI; import com.mucommander.ui.main.tree.FoldersTreePanel; import static com.mucommander.conf.TcPreference.*; /** * Filters out files that are unwanted when displaying a folder, based on user preferences. *

    * This class is used for displaying the {@link FolderPanel} main folder panel and the * {@link FoldersTreePanel folder tree view}. * * @author Maxence Bernard, Mariusz Jakubowski */ public class ConfigurableFolderFilter extends AndFileFilter implements ConfigurationListener { private final FileFilter hiddenFileFilter = new AttributeFileFilter(FileAttribute.HIDDEN, true); private final FileFilter dsFileFilter = new DSStoreFileFilter(); /** Filter used to IMAGE_FILTER out system files and folders that should not be displayed to inexperienced users. */ private final FileFilter systemFileFilter = new AttributeFileFilter(FileAttribute.SYSTEM, true); public ConfigurableFolderFilter() { configureFilters(); TcConfigurations.addPreferencesListener(this); } private void configureFilters() { final TcPreferencesAPI pref = TcConfigurations.getPreferences(); // Filters out hidden files, null when 'show hidden files' option is enabled if (!pref.getVariable(SHOW_HIDDEN_FILES, TcPreferences.DEFAULT_SHOW_HIDDEN_FILES)) { // This IMAGE_FILTER is inverted and matches non-hidden files addFileFilter(hiddenFileFilter); } // Filters out Mac OS X .DS_Store files, null when 'show DS_Store files' option is enabled if (!pref.getVariable(SHOW_DS_STORE_FILES, TcPreferences.DEFAULT_SHOW_DS_STORE_FILES)) addFileFilter(dsFileFilter); if (!pref.getVariable(SHOW_SYSTEM_FOLDERS, TcPreferences.DEFAULT_SHOW_SYSTEM_FOLDERS)) addFileFilter(systemFileFilter); } /** * Adds or removes filters based on configuration changes. */ @Override public void configurationChanged(ConfigurationEvent event) { // Show or hide hidden files switch (event.getVariable()) { case TcPreferences.SHOW_HIDDEN_FILES: if (event.getBooleanValue()) { removeFileFilter(hiddenFileFilter); } else { addFileFilter(hiddenFileFilter); } break; // Show or hide .DS_Store files (Mac OS X option) case TcPreferences.SHOW_DS_STORE_FILES: if (event.getBooleanValue()) { removeFileFilter(dsFileFilter); } else { addFileFilter(dsFileFilter); } break; // Show or hide system folders (Mac OS X option) case TcPreferences.SHOW_SYSTEM_FOLDERS: if (event.getBooleanValue()) { removeFileFilter(systemFileFilter); } else { addFileFilter(systemFileFilter); } break; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/DSStoreFileFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.mucommander.commons.file.filter.AbstractFilenameFilter; import com.mucommander.commons.file.filter.FilenameFilter; /** * DSStoreFileFilter is a {@link FilenameFilter} that matches Mac OS X '.DS_Store' files. * * @author Maxence Bernard */ public class DSStoreFileFilter extends AbstractFilenameFilter { public boolean accept(String filename) { return !".DS_Store".equals(filename); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/DrivePopupButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import java.awt.*; import java.awt.event.ActionEvent; import java.net.MalformedURLException; import java.util.*; import java.util.List; import java.util.regex.PatternSyntaxException; import javax.swing.*; import javax.swing.filechooser.FileSystemView; import com.mucommander.adb.AndroidMenu; import com.mucommander.adb.AdbUtils; import com.mucommander.bonjour.BonjourDirectory; import com.mucommander.utils.FileIconsCache; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import com.mucommander.bonjour.BonjourMenu; import com.mucommander.bonjour.BonjourService; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkListener; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.filter.PathFilter; import com.mucommander.commons.file.filter.RegexpPathFilter; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.OpenLocationAction; import com.mucommander.ui.button.PopupButton; import com.mucommander.ui.dialog.server.FTPPanel; import com.mucommander.ui.dialog.server.HTTPPanel; import com.mucommander.ui.dialog.server.NFSPanel; import com.mucommander.ui.dialog.server.SFTPPanel; import com.mucommander.ui.dialog.server.SMBPanel; import com.mucommander.ui.dialog.server.ServerConnectDialog; import com.mucommander.ui.dialog.server.ServerPanel; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import ru.trolsoft.ui.TMenuSeparator; /** * DrivePopupButton is a button which, when clicked, pops up a menu with a list of volumes items that be used * to change the current folder. * * @author Maxence Bernard */ @Slf4j public class DrivePopupButton extends PopupButton implements BookmarkListener, ConfigurationListener, LocationListener { /** * FolderPanel instance that contains this button */ private final FolderPanel folderPanel; /** * Current volumes */ private static AbstractFile[] volumes; /** * static FileSystemView instance, has a (non-null) value only under Windows */ private static FileSystemView fileSystemView; /** * Caches extended drive names, has a (non-null) value only under Windows */ private static Map extendedNameCache; /** * Caches drive icons */ private static final Map iconCache = new HashMap<>(); /** * Filters out volumes from the list based on the exclude regexp defined in the configuration, null if the regexp * is not defined. */ private static PathFilter volumeFilter; static { if (OsFamily.WINDOWS.isCurrent()) { fileSystemView = FileSystemView.getFileSystemView(); extendedNameCache = new HashMap<>(); } try { String excludeRegexp = TcConfigurations.getPreferences().getVariable(TcPreference.VOLUME_EXCLUDE_REGEXP); if (excludeRegexp != null) { volumeFilter = new RegexpPathFilter(excludeRegexp, true); volumeFilter.setInverted(true); } } catch (PatternSyntaxException e) { log.info("Invalid regexp for conf variable " + TcPreferences.VOLUME_EXCLUDE_REGEXP, e); } // Initialize the volumes list volumes = getDisplayableVolumes(); } /** * Creates a new DrivePopupButton which is to be added to the given FolderPanel. * * @param folderPanel the FolderPanel instance this button will be added to */ DrivePopupButton(FolderPanel folderPanel) { this.folderPanel = folderPanel; // Listen to location events to update the button when the current folder changes folderPanel.getLocationManager().addLocationListener(this); // Listen to bookmark changes to update the button if a bookmark corresponding to the current folder // has been added/edited/removed BookmarkManager.addBookmarkListener(this); // Listen to configuration changes to update the button if the system file icons policy has changed TcConfigurations.addPreferencesListener(this); // Use new JButton decorations introduced in Mac OS X 10.5 (Leopard) //if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { //setMargin(new Insets(6,8,6,8)); //putClientProperty("JComponent.sizeVariant", "small"); //putClientProperty("JComponent.sizeVariant", "large"); //putClientProperty("JButton.buttonType", "textured"); //} } /** * Updates the button's label and icon to reflect the current folder and match one of the current volumes: * <

      *
    • If the specified folder corresponds to a bookmark, the bookmark's name will be displayed *
    • If the specified folder corresponds to a local file, the enclosing volume's name will be displayed *
    • If the specified folder corresponds to a remote file, the protocol's name will be displayed *
    * The button's icon will be the current folder's one. */ private void updateButton() { AbstractFile currentFolder = folderPanel.getCurrentFolder(); setText(buildLabel(currentFolder)); // setToolTipText(newToolTip); // Set the folder icon based on the current system icons policy setIcon(FileIcons.getFileIcon(currentFolder)); } private String buildLabel(AbstractFile currentFolder) { String currentPath = currentFolder != null ? currentFolder.getAbsolutePath() : null; FileURL currentURL = currentFolder != null ? currentFolder.getURL() : null; // String newToolTip = null; // First tries to find a bookmark matching the specified folder List bookmarks = BookmarkManager.getBookmarks(); String newLabel = null; for (Bookmark b : bookmarks) { if (currentPath != null && currentPath.equals(b.getLocation())) { // Note: if several bookmarks match current folder, the first one will be used newLabel = b.getName(); break; } } if (newLabel != null) { return newLabel; } // If no bookmark matched current folder String protocol = currentURL != null ? currentURL.getScheme() : null; if (!FileProtocols.FILE.equals(protocol)) { // Remote file, use the protocol's name return protocol != null ? protocol.toUpperCase() : ""; } else { // Local file, use volume's name // Patch for Windows UNC network paths (weakly characterized by having a host different from 'localhost'): // display 'SMB' which is the underlying protocol if (OsFamily.WINDOWS.isCurrent() && !FileURL.LOCALHOST.equals(currentURL.getHost())) { return "SMB"; } else { // getCanonicalPath() must be avoided under Windows for the following reasons: // a) it is not necessary, Windows doesn't have symlinks // b) it triggers the dreaded 'No disk in drive' error popup dialog. // c) when network drives are present but not mounted (e.g. X:\ mapped onto an SMB share), // getCanonicalPath which is I/O bound will take a looooong time to execute int bestIndex = getBestIndex(getVolumePath(currentFolder)); return volumes[bestIndex].getName(); // Not used because the call to FileSystemView is slow // if(fileSystemView!=null) // newToolTip = getWindowsExtendedDriveName(volumes[bestIndex]); } } } private int getBestIndex(String currentPath) { int bestLength = -1; int bestIndex = 0; for (int i = 0; i < volumes.length; i++) { String volumePath = getVolumePath(volumes[i]).toLowerCase(); int len = volumePath.length(); if (currentPath.startsWith(volumePath) && len > bestLength) { bestIndex = i; bestLength = len; } } return bestIndex; } @NotNull private String getVolumePath(AbstractFile file) { if (OsFamily.WINDOWS.isCurrent()) { return file.getAbsolutePath(false); } else { return file.getCanonicalPath(false); } } /** * Returns the extended name of the given local file, e.g. "Local Disk (C:)" for C:\. The returned value is * interesting only under Windows. This method is I/O bound and very slow so it should not be called from the main * event thread. * * @param localFile the file for which to return the extended name * @return the extended name of the given local file */ private static String getExtendedDriveName(AbstractFile localFile) { // Note: fileSystemView.getSystemDisplayName(java.io.File) is unfortunately very very slow String name = fileSystemView.getSystemDisplayName((java.io.File) localFile.getUnderlyingFileObject()); if (name == null || name.isEmpty()) { // This happens for CD/DVD drives when they don't contain any disc return localFile.getName(); } return name; } /** * Returns the list of volumes to be displayed in the popup menu. * *

    The raw list of volumes is fetched using {@link LocalFile#getVolumes()} and then * filtered using the regexp defined in the {@link TcPreferences#VOLUME_EXCLUDE_REGEXP} configuration variable * (if defined). * * @return the array of volumes to be displayed in the popup menu */ private static AbstractFile[] getDisplayableVolumes() { AbstractFile[] volumes = LocalFile.getVolumes(); if (volumeFilter != null) { return volumeFilter.filter(volumes); } return volumes; } @Override public JPopupMenu getPopupMenu() { JPopupMenu popupMenu = new JPopupMenu(); // Update the list of volumes in case new ones were mounted volumes = getDisplayableVolumes(); // Add volumes final MainFrame mainFrame = folderPanel.getMainFrame(); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // Provides mnemonics and ensures uniqueness addVolumes(popupMenu, mainFrame, mnemonicHelper); popupMenu.add(new TMenuSeparator()); addBookmarks(popupMenu, mainFrame, mnemonicHelper); popupMenu.add(new TMenuSeparator()); // Add 'Network shares' shortcut if (FileFactory.isRegisteredProtocol(FileProtocols.SMB)) { TcAction action = new CustomOpenLocationAction(mainFrame, new Bookmark(Translator.get("drive_popup.network_shares"), "smb:///", null)); action.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.NETWORK_ICON_NAME)); setMnemonic(popupMenu.add(action), mnemonicHelper); } if (BonjourDirectory.isActive()) { // Add Bonjour services menu setMnemonic(popupMenu.add(new BonjourMenu() { @Override public TcAction getMenuItemAction(BonjourService bs) { return new CustomOpenLocationAction(mainFrame, bs); } }), mnemonicHelper); } addAdbDevices(popupMenu, mainFrame, mnemonicHelper); popupMenu.add(new TMenuSeparator()); // Add 'connect to server' shortcuts setMnemonic(popupMenu.add(new ServerConnectAction("SMB...", SMBPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("FTP...", FTPPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("SFTP...", SFTPPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("HTTP...", HTTPPanel.class)), mnemonicHelper); setMnemonic(popupMenu.add(new ServerConnectAction("NFS...", NFSPanel.class)), mnemonicHelper); return popupMenu; } private void addVolumes(JPopupMenu popupMenu, MainFrame mainFrame, MnemonicHelper mnemonicHelper) { boolean useExtendedDriveNames = fileSystemView != null; List itemsV = new ArrayList<>(); int nbVolumes = volumes.length; for (int i = 0; i < nbVolumes; i++) { TcAction action = new CustomOpenLocationAction(mainFrame, volumes[i]); String volumeName = volumes[i].getName(); // If several volumes have the same filename, use the volume's path for the action's label instead of the // volume's path, to disambiguate for (int j = 0; j < nbVolumes; j++) { if (j != i && volumes[j].getName().equalsIgnoreCase(volumeName)) { action.setLabel(volumes[i].getAbsolutePath()); break; } } JMenuItem item = popupMenu.add(action); setMnemonic(item, mnemonicHelper); // Set icon from cache Icon icon = iconCache.get(volumes[i]); if (icon != null) { item.setIcon(icon); } if (useExtendedDriveNames) { // Use the last known value (if any) while we update it in a separate thread String previousExtendedName = extendedNameCache.get(volumes[i]); if (previousExtendedName != null) { item.setText(previousExtendedName); } } itemsV.add(item); // JMenu offers no way to retrieve a particular JMenuItem, so we have to keep them } new RefreshDriveNamesAndIcons(popupMenu, itemsV).start(); } private void addBookmarks(JPopupMenu popupMenu, MainFrame mainFrame, MnemonicHelper mnemonicHelper) { // Add bookmarks List bookmarks = BookmarkManager.getBookmarks(); if (!bookmarks.isEmpty()) { addBookmarksGroup(popupMenu, mainFrame, mnemonicHelper, bookmarks, null); } else { // No bookmark : add a disabled menu item saying there is no bookmark popupMenu.add(Translator.get("bookmarks_menu.no_bookmark")).setEnabled(false); } } private void addBookmarksGroup(JComponent parentMenu, MainFrame mainFrame, MnemonicHelper mnemonicHelper, List bookmarks, String parent) { for (Bookmark b : bookmarks) { if ((b.getParent() == null && parent == null) || (parent != null && parent.equals(b.getParent()))) { if (b.getName().equals(BookmarkManager.BOOKMARKS_SEPARATOR) && b.getLocation().isEmpty()) { parentMenu.add(new TMenuSeparator()); continue; } if (b.getLocation().isEmpty()) { JMenu groupMenu = new JMenu(b.getName()); parentMenu.add(groupMenu); addBookmarksGroup(groupMenu, mainFrame, mnemonicHelper, bookmarks, b.getName()); setMnemonic(groupMenu, mnemonicHelper); } else { JMenuItem item = createBookmarkMenuItem(parentMenu, mainFrame, b); setMnemonic(item, mnemonicHelper); } } } } private JMenuItem createBookmarkMenuItem(JComponent parentMenu, MainFrame mainFrame, Bookmark b) { JMenuItem item; if (parentMenu instanceof JPopupMenu) { item = ((JPopupMenu) parentMenu).add(new CustomOpenLocationAction(mainFrame, b)); } else { item = ((JMenu) parentMenu).add(new CustomOpenLocationAction(mainFrame, b)); } //JMenuItem item = popupMenu.add(new CustomOpenLocationAction(mainFrame, b)); String location = b.getLocation(); if (!location.contains("://")) { AbstractFile file = FileFactory.getFile(location); if (file != null) { Icon icon = FileIconsCache.getInstance().getIcon(file); if (icon != null) { item.setIcon(icon); } // Image image = FileIconsCache.getInstance().getImageIcon(file); // if (image != null) { // item.setIcon(new ImageIcon(image)); // } } } else if (location.startsWith("ftp://") || location.startsWith("sftp://") || location.startsWith("http://")) { item.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.NETWORK_ICON_NAME)); } else if (location.startsWith("adb://")) { item.setIcon(IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.ANDROID_ICON_NAME)); } return item; } private void addAdbDevices(JPopupMenu popupMenu, MainFrame mainFrame, MnemonicHelper mnemonicHelper) { if (AdbUtils.checkAdb()) { setMnemonic(popupMenu.add(new AndroidMenu() { @Override public TcAction getMenuItemAction(String deviceSerial) { FileURL url = getDeviceURL(deviceSerial); return new CustomOpenLocationAction(mainFrame, url); } @Nullable private FileURL getDeviceURL(String deviceSerial) { try { return FileURL.getFileURL("adb://" + deviceSerial); } catch (MalformedURLException e) { log.error("Device URL error", e); return null; } } }), mnemonicHelper); } } /** * Calls to getExtendedDriveName(String) are very slow, so they are performed in a separate thread * to not lock the main even thread. The popup menu gets first displayed with the short drive names, and * then refreshed with the extended names as they are retrieved. */ private static class RefreshDriveNamesAndIcons extends Thread { private final JPopupMenu popupMenu; private final List items; RefreshDriveNamesAndIcons(JPopupMenu popupMenu, List items) { super("RefreshDriveNamesAndIcons"); this.popupMenu = popupMenu; this.items = items; } @Override public void run() { final boolean useExtendedDriveNames = fileSystemView != null; for (int i = 0; i < items.size(); i++) { final JMenuItem item = items.get(i); final String extendedName = getExtendedDriverName(useExtendedDriveNames, volumes[i]); final Icon icon = getIcon(volumes[i]); SwingUtilities.invokeLater(() -> { if (useExtendedDriveNames) { item.setText(extendedName); } if (icon != null) { item.setIcon(icon); } }); } // Re-calculate the popup menu's dimensions SwingUtilities.invokeLater(() -> { popupMenu.invalidate(); popupMenu.pack(); }); } @Nullable private Icon getIcon(AbstractFile file) { // Set system icon for volumes, only if system icons are available on the current platform final Icon icon = FileIcons.hasProperSystemIcons() ? FileIcons.getSystemFileIcon(file) : null; if (icon != null) { iconCache.put(file, icon); } return icon; } @Nullable private String getExtendedDriverName(boolean useExtendedDriveNames, AbstractFile file) { if (useExtendedDriveNames) { // Under Windows, show the extended drive name (e.g. "Local Disk (C:)" instead of just "C:") but use // the simple drive name for the mnemonic (i.e. 'C' instead of 'L'). String extendedName = getExtendedDriveName(file); // Keep the extended name for later (see above) extendedNameCache.put(file, extendedName); return extendedName; } return null; } } /** * Convenience method that sets a mnemonic to the given JMenuItem, using the specified MnemonicHelper. * * @param menuItem the menu item for which to set a mnemonic * @param mnemonicHelper the MnemonicHelper instance to be used to determine the mnemonic's character. */ private void setMnemonic(JMenuItem menuItem, MnemonicHelper mnemonicHelper) { menuItem.setMnemonic(mnemonicHelper.getMnemonic(menuItem.getText())); } @Override public void bookmarksChanged() { // Refresh label in case a bookmark with the current location was changed updateButton(); } /** * Listens to certain configuration variables. */ @Override public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); // Update the button's icon if the system file icons policy has changed if (var.equals(TcPreferences.USE_SYSTEM_FILE_ICONS)) { updateButton(); } } @Override public Dimension getPreferredSize() { // Limit button's maximum width to something reasonable and leave enough space for location field, // as bookmarks name can be as long as users want them to be. // Note: would be better to use JButton.setMaximumSize() but it doesn't seem to work Dimension d = super.getPreferredSize(); if (d.width > 160) { d.width = 160; } return d; } /** * This action pops up {@link com.mucommander.ui.dialog.server.ServerConnectDialog} for a specified protocol. */ private class ServerConnectAction extends AbstractAction { private final Class serverPanelClass; private ServerConnectAction(String label, Class serverPanelClass) { super(label); this.serverPanelClass = serverPanelClass; } public void actionPerformed(ActionEvent actionEvent) { new ServerConnectDialog(folderPanel, serverPanelClass).showDialog(); } } /** * This modified {@link OpenLocationAction} changes the current folder on the {@link FolderPanel} that contains * this button, instead of the currently active {@link FolderPanel}. */ private class CustomOpenLocationAction extends OpenLocationAction { CustomOpenLocationAction(MainFrame mainFrame, Bookmark bookmark) { super(mainFrame, new HashMap<>(), bookmark); } CustomOpenLocationAction(MainFrame mainFrame, AbstractFile file) { super(mainFrame, new HashMap<>(), file); } CustomOpenLocationAction(MainFrame mainFrame, BonjourService bs) { super(mainFrame, new HashMap<>(), bs); } CustomOpenLocationAction(MainFrame mainFrame, FileURL url) { super(mainFrame, new HashMap<>(), url); } @Override protected FolderPanel getFolderPanel() { return folderPanel; } } @Override public void locationChanged(LocationEvent e) { // Update the button's label to reflect the new current folder updateButton(); } public void locationChanging(LocationEvent locationEvent) { } public void locationCancelled(LocationEvent locationEvent) { } public void locationFailed(LocationEvent locationEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/FolderPanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.mucommander.auth.CredentialsMapping; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileURL; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.core.FolderChangeMonitor; import com.mucommander.core.LocalLocationHistory; import com.mucommander.core.LocationChanger; import com.mucommander.core.LocationChanger.ChangeFolderThread; import com.mucommander.ui.PreloadedJFrame; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.impl.FocusNextAction; import com.mucommander.ui.action.impl.FocusPreviousAction; import com.mucommander.ui.dnd.FileDragSourceListener; import com.mucommander.ui.dnd.FileDropTargetListener; import com.mucommander.ui.event.LocationManager; import com.mucommander.ui.event.TableSelectionListener; import com.mucommander.ui.main.quicklist.*; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.full.FileTableConfiguration; import com.mucommander.ui.main.tabs.ConfFileTableTab; import com.mucommander.ui.main.tabs.FileTableTab; import com.mucommander.ui.main.tabs.FileTableTabs; import com.mucommander.ui.main.tree.FoldersTreePanel; import com.mucommander.ui.quicklist.QuickList; import com.mucommander.ui.quicklist.QuickListContainer; import com.mucommander.ui.tabs.ActiveTabListener; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.swing.*; import java.awt.*; import java.awt.dnd.DropTarget; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.util.HashSet; import java.util.Set; /** * Folder pane that contains the table that displays the contents of the current directory and allows navigation, the * drive button, and the location field. * * @author Maxence Bernard, Arik Hadas */ @Slf4j public class FolderPanel implements FocusListener, QuickListContainer, ActiveTabListener { @Getter private final JPanel panel; @Getter private boolean previewMode; /** The following constants are used to identify the left and right folder panels */ public enum FolderPanelType { LEFT, RIGHT } /** * -- GETTER -- * Returns the MainFrame that contains this panel. */ @Getter final MainFrame mainFrame; /** * -- GETTER -- * Returns the LocationManager instance that notifies registered listeners of location changes that occur in this FolderPanel. */ @Getter private final LocationManager locationManager = new LocationManager(this); /** * -- GETTER -- * Returns the DrivePopupButton contained by this panel. */ /* We're NOT using JComboBox anymore because of its strange behavior: it calls actionPerformed() each time an item is highlighted with the arrow (UP/DOWN) keys, so there is no way to tell if it's the final selection (ENTER) or not. */ @Getter private final DrivePopupButton driveButton; /** * -- GETTER -- * Returns the LocationTextField contained by this panel. */ @Getter private final LocationTextField locationTextField; /** * -- GETTER -- * Returns the FileTable contained by this panel. */ @Getter private final FileTable fileTable; /** * -- GETTER -- * Returns the FileTable tabs contained by this panel. */ @Getter private final FileTableTabs tabs; /** * -- GETTER -- * Returns a panel with a folders tree. */ @Getter private final FoldersTreePanel foldersTreePanel; private final JSplitPane treeSplitPane; @Getter private final FileDragSourceListener fileDragSourceListener; private final LocationChanger locationChanger; /** Is directory tree visible * -- GETTER -- * Returns true if a directory tree is visible. */ @Getter private boolean treeVisible = false; /** Saved width of a directory tree (when it's not visible) */ private int oldTreeWidth = 150; /** Array of all the existing pop ups for this panel's FileTable **/ private QuickList[] fileTablePopups; private PreviewPanel previewPanel; private final JPanel locationPanel; private final TableSelectionListener previewTableSelectionListener = new TableSelectionListener() { @Override public void selectedFileChanged(FileTable source) { if (previewMode) { AbstractFile file = source.getFolderPanel().getFileTable().getSelectedFile(); previewPanel.loadFile(file); } } @Override public void markedFilesChanged(FileTable source) { } }; /* TODO branch private boolean branchView; */ FolderPanel(MainFrame mainFrame, ConfFileTableTab[] initialTabs, int indexOfSelectedTab, FileTableConfiguration conf) { panel = PreloadedJFrame.getJPanel(new BorderLayout()); log.trace(" initialTabs:"); for (FileTableTab tab:initialTabs) { log.trace("\t{}", tab.getLocation() != null ? tab.getLocation().toString() : null); } this.mainFrame = mainFrame; // No decoration for this panel panel.setBorder(null); locationPanel = PreloadedJFrame.getJPanel(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.HORIZONTAL; c.gridy = 0; // create and add drive button this.driveButton = new DrivePopupButton(this); c.weightx = 0; c.gridx = 0; locationPanel.add(driveButton, c); // Create location text field and wrap it in a LocationBar that can // alternate between the text field and a breadcrumb view (Ctrl key). this.locationTextField = new LocationTextField(this); LocationBar locationBar = new LocationBar(this, locationTextField); // Give location field all the remaining space until the PoupupsButton c.weightx = 1; c.gridx = 1; // Add some space between drive button and location combo box (none by default) c.insets = new Insets(0, 4, 0, 0); locationPanel.add(locationBar, c); panel.add(locationPanel, BorderLayout.NORTH); // create the FileTable fileTable = new FileTable(mainFrame, this, conf); locationChanger = new LocationChanger(mainFrame, this, locationManager); // create the Tabs (Must be called after the fileTable was created and current folder was set) tabs = new FileTableTabs(mainFrame, this, initialTabs); // Select the tab that was previously selected on last init tabs.selectTab(indexOfSelectedTab); tabs.addActiveTabListener(this); // create folders tree on a JSplitPane foldersTreePanel = new FoldersTreePanel(this); foldersTreePanel.setVisible(false); treeSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, foldersTreePanel.getPanel(), tabs); treeSplitPane.setDividerSize(0); treeSplitPane.setDividerLocation(0); // Remove default border treeSplitPane.setBorder(null); panel.add(treeSplitPane, BorderLayout.CENTER); // Disable Ctrl+Tab and Shift+Ctrl+Tab focus traversal keys disableCtrlFocusTraversalKeys(locationTextField); disableCtrlFocusTraversalKeys(foldersTreePanel.getTree()); disableCtrlFocusTraversalKeys(fileTable); disableCtrlFocusTraversalKeys(tabs); registerCycleThruFolderPanelAction(locationTextField); registerCycleThruFolderPanelAction(foldersTreePanel.getTree()); // No need to register cycle actions for FileTable, they already are // Listen to focus event in order to notify MainFrame of changes of the current active panel/table fileTable.addFocusListener(this); locationTextField.addFocusListener(this); tabs.addFocusListener(this); // Drag and Drop support // Enable drag support on the FileTable this.fileDragSourceListener = new FileDragSourceListener(this); fileDragSourceListener.enableDrag(fileTable); // Allow the location field to change the current directory when a file/folder is dropped on it FileDropTargetListener dropTargetListener = new FileDropTargetListener(this, true); locationTextField.setDropTarget(new DropTarget(locationTextField, dropTargetListener)); driveButton.setDropTarget(new DropTarget(driveButton, dropTargetListener)); } /** * Removes the Control+Tab and Shift+Control+Tab focus traversal keys from the given component so that those * shortcuts can be used for other purposes. * * @param component the component for which to remove the Control+Tab and Shift+Control+Tab focus traversal keys */ private void disableCtrlFocusTraversalKeys(Component component) { // Remove Ctrl+Tab from forward focus traversal keys Set keyStrokeSet = new HashSet<>(); keyStrokeSet.add(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, 0)); component.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, keyStrokeSet); // Remove Shift+Ctrl+Tab from backward focus traversal keys keyStrokeSet = new HashSet<>(); keyStrokeSet.add(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_TAB, java.awt.event.InputEvent.SHIFT_DOWN_MASK)); component.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, keyStrokeSet); } /** * Registers the {@link FocusNextAction} and {@link FocusPreviousAction} actions onto the given component's * input map. * * @param component the component for which to register the cycle actions */ private void registerCycleThruFolderPanelAction(JComponent component) { ActionKeymap.registerActionAccelerators( ActionManager.getActionInstance(FocusNextAction.Descriptor.ACTION_ID, mainFrame), component, JComponent.WHEN_FOCUSED); ActionKeymap.registerActionAccelerators( ActionManager.getActionInstance(FocusPreviousAction.Descriptor.ACTION_ID, mainFrame), component, JComponent.WHEN_FOCUSED); } /** * Returns the visited folders history, wrapped in a FolderHistory object. * * @return the visited folders history, wrapped in a FolderHistory object */ public LocalLocationHistory getFolderHistory() { return getTabs().getCurrentTab().getLocationHistory(); } /** * Allows the user to easily change the current folder and type a new one: requests focus * on the location field and selects the folder string. */ public void changeCurrentLocation() { locationTextField.selectAll(); locationTextField.requestFocus(); } /** * Returns the FolderChangeMonitor which monitors changes in the current folder and automatically refreshes it. * * @return the FolderChangeMonitor which monitors changes in the current folder and automatically refreshes it */ public FolderChangeMonitor getFolderChangeMonitor() { return locationManager.getFolderChangeMonitor(); } public void setProgressValue(int value) { SwingUtilities.invokeLater(() -> locationTextField.setProgressValue(value)); } public void tryChangeCurrentFolderInternal(FileURL folderURL, Runnable callback) { locationChanger.tryChangeCurrentFolderInternal(folderURL, callback); } public ChangeFolderThread tryChangeCurrentFolder(AbstractFile folder) { return locationChanger.tryChangeCurrentFolder(folder, false); } public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL, AbstractFile selectThisFileAfter, boolean findWorkableFolder) { return locationChanger.tryChangeCurrentFolder(FileFactory.getFile(folderURL), selectThisFileAfter, findWorkableFolder, false); } public ChangeFolderThread tryChangeCurrentFolder(AbstractFile folder, AbstractFile selectThisFileAfter, boolean findWorkableFolder) { return locationChanger.tryChangeCurrentFolder(folder, selectThisFileAfter, findWorkableFolder, false); } public ChangeFolderThread tryChangeCurrentFolder(String folderPath) { return locationChanger.tryChangeCurrentFolder(folderPath); } public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL) { return locationChanger.tryChangeCurrentFolder(folderURL); } public ChangeFolderThread tryChangeCurrentFolder(FileURL folderURL, CredentialsMapping credentialsMapping) { return locationChanger.tryChangeCurrentFolder(folderURL, credentialsMapping, false); } public ChangeFolderThread tryRefreshCurrentFolder() { return locationChanger.tryRefreshCurrentFolder(); } public ChangeFolderThread tryRefreshCurrentFolder(AbstractFile selectThisFileAfter) { return locationChanger.tryRefreshCurrentFolder(selectThisFileAfter); } public ChangeFolderThread getChangeFolderThread() { return locationChanger.getChangeFolderThread(); } public long getLastFolderChangeTime() { return locationChanger.getLastFolderChangeTime(); } /** * Returns the folder that is currently being displayed by this panel. * * @return the folder that is currently being displayed by this panel */ public AbstractFile getCurrentFolder() { return locationManager.getCurrentFolder(); } /** * This method updates the UI with the given folder * If the currently selected tab is locked and the given flag "changeLockedTab" is off, * then a new tab is opened with the given folder, * otherwise, the currently presented folder is replaces with the given folder * * @param folder - the folder to be set * @param children - the children of the given folder * @param fileToSelect - the file that would be selected after changing the folder * @param changeLockedTab - flag that indicates whether to change the presented folder in * the currently selected tab although it's locked (used when switching tabs) */ public void setCurrentFolder(AbstractFile folder, AbstractFile[] children, AbstractFile fileToSelect, boolean changeLockedTab) { // Change the current folder in the table and select the given file if not null if (fileToSelect == null) { fileTable.setCurrentFolder(folder, children); } else { fileTable.setCurrentFolder(folder, children, fileToSelect); } } /** * Shows the popup which is located the given index in fileTablePopups. * * @param index - index of the FileTablePopup in fileTablePopups. */ public void showQuickList(int index) { if (fileTablePopups == null) { // Initialize quick lists fileTablePopups = new QuickList[] { new ParentFoldersQL(this), new RecentLocationsQL(this), new RecentExecutedFilesQL(this), new BookmarksQL(this), new RootFoldersQL(this), new TabsQL(this), new RecentViewedQL(this), new RecentEditedQL(this), new EditorBookmarksQL(this) }; } fileTablePopups[index].show(); } /** * Returns width of a folders tree. * @return a width of a folders tree */ public int getTreeWidth() { return treeVisible ? treeSplitPane.getDividerLocation() : oldTreeWidth; } /** * Sets a width of a folders tree. * @param width new width */ void setTreeWidth(int width) { if (!treeVisible) { oldTreeWidth = width; } else { treeSplitPane.setDividerLocation(width); treeSplitPane.doLayout(); } } /** * Enables/disables a directory tree visibility. Invoked by {@link com.mucommander.ui.action.impl.ToggleTreeAction}. */ public void setTreeVisible(boolean treeVisible) { if (this.treeVisible != treeVisible) { this.treeVisible = treeVisible; if (!treeVisible) { // save width of a tree panel oldTreeWidth = treeSplitPane.getDividerLocation(); } foldersTreePanel.setVisible(treeVisible); // hide completely divider if a tree isn't visible treeSplitPane.setDividerLocation(treeVisible ? oldTreeWidth : 0); treeSplitPane.setDividerSize(treeVisible ? 5 : 0); foldersTreePanel.requestFocus(); } } @Override public String toString() { return getClass().getName()+"@"+hashCode() +" currentFolder="+getCurrentFolder()+" hasFocus="+panel.hasFocus(); } @Override public void focusGained(FocusEvent e) { // Notify MainFrame that we are in control now! (our table/location field is active) mainFrame.setActiveTable(fileTable); } @Override public void focusLost(FocusEvent e) { if (!TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_QUICK_SEARCH_MATCHES_FIRST, TcPreferences.DEFAULT_SHOW_QUICK_SEARCH_MATCHES_FIRST)) { fileTable.getQuickSearch().stop(); } } @Override public Point calcQuickListPosition(Dimension dim) { return new Point( Math.max((getWidth() - (int)dim.getWidth()) / 2, 0), getLocationTextField().getHeight() + Math.max((panel.getHeight() - (int)dim.getHeight()) / 3, 0) ); } @Override public Component containerComponent() { return panel; } @Override public Component nextFocusableComponent() { return fileTable; } @Override public void activeTabChanged() { boolean isCurrentTabLocked = tabs.getCurrentTab().isLocked(); locationTextField.setEnabled(!isCurrentTabLocked); driveButton.setEnabled(!isCurrentTabLocked); } public void setPreviewMode(boolean previewMode) { this.previewMode = previewMode; if (previewMode) { panel.remove(treeSplitPane); locationPanel.setVisible(false); if (previewPanel == null) { previewPanel = new PreviewPanel(); } panel.add(previewPanel, BorderLayout.CENTER); mainFrame.getActivePanel().getFileTable().addTableSelectionListener(previewTableSelectionListener); previewPanel.loadFile(mainFrame.getActiveTable().getSelectedFile()); } else { panel.remove(previewPanel); panel.add(treeSplitPane, BorderLayout.CENTER); locationPanel.setVisible(true); mainFrame.getActivePanel().getFileTable().removeTableSelectionListener(previewTableSelectionListener); } panel.doLayout(); panel.repaint(); } @Override public int getWidth() { return panel.getWidth(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/LocationBar.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License* along with this program. * If not, see . */ package com.mucommander.ui.main import java.awt.* import java.awt.event.* import javax.swing.JPanel import javax.swing.SwingUtilities import javax.swing.Timer /** * A wrapper panel that sits in place of the location text field in each FolderPanel. * It hosts two cards in a [CardLayout]: * - The regular [LocationTextField] (always visible by default). * - A [BreadcrumbBar] that renders the current path as a row of clickable hyperlink-style labels, one per directory * segment, separated by `›` glyphs. * * When the Ctrl key (or Meta on macOS) is held for {@value #BREADCRUMB_SHOW_DELAY_MS}ms *and* the location text field * does not have keyboard focus (i.e. the user is not actively editing the path), the breadcrumb card is shown. * The delayed appearance prevents the breadcrumb from flickering during quick keyboard shortcuts (Ctrl+C, Ctrl+V, etc.). * * To reduce visual noise, the breadcrumb is only shown for the panel where the mouse is currently hovering. * If the mouse is not over either folder panel, breadcrumbs are shownin both panels. The breadcrumb dynamically follows * the mouse - if the user moves the mouse from one panel to another while still holding Ctrl, the breadcrumb switches * accordingly. * The breadcrumb is immediately hidden when Ctrl is released, or if any other key is pressedor the mouse is clicked * before the delay expires. */ class LocationBar(private val folderPanel: FolderPanel, private val locationTextField: LocationTextField) : JPanel() { private val breadcrumbBar: BreadcrumbBar private val cardLayout: CardLayout = CardLayout() /** Timer that delays showing the breadcrumb to avoid noise from quick keyboard shortcuts */ private val showBreadcrumbTimer: Timer /** Tracks whether Ctrl/Meta is currently being held down */ private var modifierHeld = false init { setLayout(cardLayout) setOpaque(false) breadcrumbBar = BreadcrumbBar(folderPanel) add(locationTextField, CARD_TEXT_FIELD) add(breadcrumbBar, CARD_BREADCRUMB) // On macOS the menu shortcut key is Cmd (META); on all other platforms it is Ctrl. // We want Ctrl to always work, and Cmd to also work on macOS. val menuShortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() val isMac = menuShortcutMask == InputEvent.META_DOWN_MASK // Initialize the timer that delays showing the breadcrumb showBreadcrumbTimer = Timer(BREADCRUMB_SHOW_DELAY_MS) { _: ActionEvent? -> // Only show breadcrumb if: // 1. Text field is currently visible (not already showing breadcrumb) // 2. Mouse is over this panel OR mouse is over neither panel if (locationTextField.isShowing() && shouldShowBreadcrumbForMousePosition()) { breadcrumbBar.setFile(folderPanel.currentFolder) cardLayout.show(this@LocationBar, CARD_BREADCRUMB) } } showBreadcrumbTimer.isRepeats = false // Listen for mouse events globally Toolkit.getDefaultToolkit().addAWTEventListener({ event: AWTEvent? -> if (event is MouseEvent) { when (event.getID()) { MouseEvent.MOUSE_PRESSED -> cancelBreadcrumbTimer() MouseEvent.MOUSE_MOVED -> if (modifierHeld) { updateBreadcrumbVisibility() } } } }, AWTEvent.MOUSE_EVENT_MASK or AWTEvent.MOUSE_MOTION_EVENT_MASK) KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher { e: KeyEvent? -> val isCtrl = e!!.getKeyCode() == KeyEvent.VK_CONTROL val isMeta = isMac && e.getKeyCode() == KeyEvent.VK_META if (e.getID() == KeyEvent.KEY_PRESSED) { if (isCtrl || isMeta) { // Ctrl/Meta pressed - start the timer to show breadcrumb after delay if (!locationTextField.hasFocus() && !showBreadcrumbTimer.isRunning) { modifierHeld = true showBreadcrumbTimer.restart() } } else { // Any other key pressed - cancel the timer to prevent breadcrumb from showing // This filters out keyboard shortcuts like Ctrl+C, Ctrl+V, etc. cancelBreadcrumbTimer() } } else if (e.getID() == KeyEvent.KEY_RELEASED) { if (isCtrl || isMeta) { // Ctrl/Meta released - cancel timer and hide breadcrumb if showing cancelBreadcrumbTimer() // Hide breadcrumb only when neither trigger modifier remains held val ctrlStillHeld = (e.modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0 val metaStillHeld = isMac && (e.modifiersEx and InputEvent.META_DOWN_MASK) != 0 if (!ctrlStillHeld && !metaStillHeld) { modifierHeld = false SwingUtilities.invokeLater { cardLayout.show(this@LocationBar, CARD_TEXT_FIELD) } } } } false // never consume — other Ctrl shortcuts must keep working } } /** * Cancels the breadcrumb show timer if it's running. */ private fun cancelBreadcrumbTimer() { if (showBreadcrumbTimer.isRunning) { showBreadcrumbTimer.stop() } } /** * Updates the breadcrumb visibility based on current mouse position. * Shows breadcrumb if mouse is over this panel or neither panel. * Hides breadcrumb if mouse is over the other panel. * Called when mouse moves while Ctrl/Meta is held. */ private fun updateBreadcrumbVisibility() { SwingUtilities.invokeLater { val shouldShow = shouldShowBreadcrumbForMousePosition() val isShowingBreadcrumb = breadcrumbBar.isShowing() if (shouldShow && !isShowingBreadcrumb) { // Mouse moved to this panel or neutral area - show breadcrumb breadcrumbBar.setFile(folderPanel.currentFolder) cardLayout.show(this@LocationBar, CARD_BREADCRUMB) } else if (!shouldShow && isShowingBreadcrumb) { // Mouse moved to other panel - hide breadcrumb cardLayout.show(this@LocationBar, CARD_TEXT_FIELD) } } } /** * Determines whether the breadcrumb should be shown based on the current mouse position. * Returns true if: * - Mouse is over this panel, OR * - Mouse is over neither panel (show in both) * Returns false if: * - Mouse is over the other panel (don't show noise in the non-hovered panel) */ private fun shouldShowBreadcrumbForMousePosition(): Boolean { val pointerInfo = MouseInfo.getPointerInfo() ?: return true val mouseLocation = pointerInfo.location // Check if mouse is over this panel val thisPanel = folderPanel.panel if (isMouseOverComponent(thisPanel, mouseLocation)) { return true } // Check if mouse is over the other panel val mainFrame = folderPanel.getMainFrame() val leftPanel = mainFrame.leftPanel val rightPanel = mainFrame.rightPanel val otherPanel = if (folderPanel === leftPanel) rightPanel else leftPanel if (otherPanel != null) { val otherPanelComponent = otherPanel.panel if (isMouseOverComponent(otherPanelComponent, mouseLocation)) { return false // Mouse is over the other panel, don't show breadcrumb here } } return true // Mouse is over neither panel, show breadcrumb in both } /** * Checks if the mouse at the given screen location is over the specified component. */ private fun isMouseOverComponent(component: Component?, screenLocation: Point): Boolean { if (component == null || !component.isShowing()) { return false } val componentLocation = Point(screenLocation) SwingUtilities.convertPointFromScreen(componentLocation, component) return component.contains(componentLocation) } /** * Always report the text field's preferred size so that switching cards does * not cause the location bar row to grow or shrink. */ override fun getPreferredSize(): Dimension? { return locationTextField.getPreferredSize() } override fun getMinimumSize(): Dimension? { return locationTextField.getMinimumSize() } companion object { private const val CARD_TEXT_FIELD = "textField" private const val CARD_BREADCRUMB = "breadcrumb" /** Delay in milliseconds before showing breadcrumb when Ctrl/Meta is held */ private const val BREADCRUMB_SHOW_DELAY_MS = 50 } } ================================================ FILE: src/main/java/com/mucommander/ui/main/LocationTextField.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.impl.local.UNCFile; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.autocomplete.AutocompleterTextComponent; import com.mucommander.ui.autocomplete.CompleterFactory; import com.mucommander.ui.autocomplete.TextFieldCompletion; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.progress.ProgressTextField; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeListener; import com.mucommander.ui.theme.ThemeManager; /** * A TextField which is located on each panel and used to display the location presented in the panel's file-table, * and for letting the user change this location. * * This TextField support: * - auto-completion * - location changing progress indicator * - Theme settings * * @author Maxence Bernard, Arik Hadas */ public class LocationTextField extends ProgressTextField implements LocationListener, FocusListener, ThemeListener { /** FolderPanel this text field is displayed in */ private final FolderPanel folderPanel; /** True while a folder is being changed after a path was entered in the location field and validated by the user */ private boolean folderChangeInitiatedByLocationField; /** Used to save the path that was entered by the user after validation of the location textfield */ private String locationFieldTextSave; /** For windows path, regex that finds trailing space characters at the end of a path */ private static final Pattern windowsTrailingSpacePattern; static { if (OsFamily.WINDOWS.isCurrent()) { windowsTrailingSpacePattern = Pattern.compile("[ ]+[\\\\]*$"); } else { windowsTrailingSpacePattern = null; } } /** * Creates a new LocationTextField for use in the given FolderPanel. * * @param folderPanel FolderPanel this text field is displayed in */ LocationTextField(FolderPanel folderPanel) { // Use a custom text field that can display loading progress when changing folders super(0, ThemeManager.getCurrentColor(Theme.LOCATION_BAR_PROGRESS_COLOR)); this.folderPanel = folderPanel; // Applies theme values. setFont(ThemeManager.getCurrentFont(Theme.LOCATION_BAR_FONT)); setDisabledTextColor(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_FOREGROUND_COLOR)); setForeground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_FOREGROUND_COLOR)); setBackground(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_BACKGROUND_COLOR)); setSelectedTextColor(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_SELECTED_FOREGROUND_COLOR)); setSelectionColor(ThemeManager.getCurrentColor(Theme.LOCATION_BAR_SELECTED_BACKGROUND_COLOR)); // Listen to location changes to update popup menu choices and disable this component while the location is // being changed folderPanel.getLocationManager().addLocationListener(this); // Listen to focus events to temporarily disable the MainFrame's JMenuBar when this component has the keyboard focus. // Not doing so would trigger unwanted menu bar actions when typing. addFocusListener(this); enableAutoCompletion(); ThemeManager.addCurrentThemeListener(this); } /** * Adds auto-completion capabilities to this text field. */ private void enableAutoCompletion() { new TextFieldCompletion(new AutocompleterTextComponent(this) { @Override public void OnEnterPressed(KeyEvent keyEvent) { if (textFieldValidated()) // if a malformed url was entered. folderChangeCompleted(false); // /!\ Consume the event so to prevent JTextField from firing an ActionEvent keyEvent.consume(); } @Override public void OnEscPressed(KeyEvent keyEvent) { textFieldCancelled(); } }, CompleterFactory.getLocationCompleter()); } /** * Re-enable this text field after a folder change was completed, cancelled by the user or has failed. * *

    If the folder change was the result of the user manually entering a path in the location field and the folder * change failed or was cancelled, keeps the path intact and request focus on the text field so the user can modify it. */ private void folderChangeCompleted(boolean folderChangedSuccessfully) { if (folderChangedSuccessfully || !folderChangeInitiatedByLocationField) { // Set the location field's contents to the new current folder's path setText(folderPanel.getCurrentFolder().getAbsolutePath()); } // Re-enable this text field setEnabled(true); // If the location was entered and validated in the location field and the folder change failed or was cancelled... if (!folderChangedSuccessfully && folderChangeInitiatedByLocationField) { // Restore the text that was entered by the user setText(locationFieldTextSave); // Select the text to grab user's attention and make it easier to modify selectAll(); // Request focus (focus was on FileTable) requestFocus(); } // Reset field for next folder change folderChangeInitiatedByLocationField = false; } @Override public void locationChanging(LocationEvent e) { // Change the location field's text to the folder being changed, only if the folder change was not initiated // by the location field (to preserve the path entered by the user while the folder is being changed) if (!folderChangeInitiatedByLocationField) { FileURL folderURL = e.getFolderURL(); String locationText; if (folderURL.getScheme().equals(FileProtocols.FILE)) { locationText = urlToFileLocationString(folderURL); } // Display the full URL for protocols other than 'file' else { locationText = folderURL.toString(false); } setText(locationText); } // Disable component until the folder has been changed, canceled or failed. // Note: if the focus currently is in the location field, the focus manager will release focus and give it // to the next component (i.e. FileTable) setEnabled(false); } private String urlToFileLocationString(FileURL folderURL) { String locationText; if (FileURL.LOCALHOST.equals(folderURL.getHost())) { // Do not display the URL's scheme & host for local files locationText = folderURL.getPath(); // Under for OSes with 'root drives' (Windows, OS/2), remove the leading '/' character if (LocalFile.hasRootDrives()) { locationText = PathUtils.removeLeadingSeparator(locationText, "/"); } } else { // For network files with FILE scheme display the URL in UNC format locationText = "\\\\" + folderURL.getHost() + folderURL.getPath().replace('/', '\\'); if (!locationText.endsWith(UNCFile.SEPARATOR)) { locationText += UNCFile.SEPARATOR; } } return locationText; } public void locationChanged(LocationEvent e) { // Re-enable component and change the location field's text to the new current folder's path folderChangeCompleted(true); } public void locationCancelled(LocationEvent e) { // Re-enable component and change the location field's text to the new current folder's path. // If the path was entered in the location field, keep the path to give the user a chance to correct it. folderChangeCompleted(false); } public void locationFailed(LocationEvent e) { // Re-enable component and change the location field's text to the new current folder's path. // If the path was entered in the location field, keep the path to give the user a chance to correct it. folderChangeCompleted(false); } /** * * @return true if a malformed url was entered, false otherwise. */ private boolean textFieldValidated() { String location = getText().trim(); // Under Windows, trim the entered path for the following reason. // If a file 'A' (e.g. "C:\temp") exists and 'A ' (e.g. "C:\temp ") is requested, the java.io.File will resolve // (file.exists() will return true), but this file will be a strange one, listing bogus children files with // weird attributes (in the case of a directory). // Windows (or java.io.File under Windows) is somehow space-tolerant but then unable to deal with // those files properly. So if a path ends with space characters, we remove them to prevent those weirdnesses. // Note that Win32 doesn't allow creating files with trailing spaces (in Explorer, command prompt...), but // those files can still be manually crafted and thus exist on one's hard drive. // Mucommander should in theory be able to access such files without any problem but this hasn't been tested. if (OsFamily.WINDOWS.isCurrent() && location.indexOf(":\\") == 1) { // Looks for trailing spaces and if some Matcher matcher = windowsTrailingSpacePattern.matcher(location); if (matcher.find()) { location = location.substring(0, matcher.start()); } } // Save the path that was entered in case the location change fails or is cancelled locationFieldTextSave = location; // Indicate we search for location corresponding to the given string. // it will be false we'll find one. boolean tryToInterpretEnteredString = true; // Look for a bookmark which name is the entered string (case insensitive) Bookmark b = BookmarkManager.getBookmark(location); if (b != null) { // Change the current folder to the bookmark's location setText(location = b.getLocation()); tryToInterpretEnteredString = false; } // Look for a volume whose name is the entered string (case insensitive) AbstractFile[] volumes = LocalFile.getVolumes(); for (int i = 0; tryToInterpretEnteredString && i < volumes.length; i++) { if (volumes[i].getName().equalsIgnoreCase(location)) { // Change the current folder to the volume folder setText(location = volumes[i].getAbsolutePath()); tryToInterpretEnteredString = false; } } // Todo: replace this by env:// filesystem ? // Look for a system variable which name is the entered string (case insensitive) if (tryToInterpretEnteredString && location.startsWith("$")) { String variableKey = location.substring(1); String variableValue = System.getenv(variableKey); if (variableValue != null) { setText(location = variableValue); } } // Remember that the folder change was initiated by the location field folderChangeInitiatedByLocationField = true; // Change folder return folderPanel.tryChangeCurrentFolder(location) == null; } private void textFieldCancelled() { AbstractFile currentFolder = folderPanel.getCurrentFolder(); setText(currentFolder != null ? currentFolder.getAbsolutePath() : ""); transferFocus(); } @Override public void focusGained(FocusEvent e) { // Disable menu bar when this component has gained focus folderPanel.getMainFrame().getJFrame().getJMenuBar().setEnabled(false); } @Override public void focusLost(FocusEvent e) { // // If we are not in the middle of a folder change, and focus has been // // lost then ensure location field's text is set to the current directory. // if (!folderPanel.isFolderChanging()) // locationField.setText(folderPanel.getCurrentFolder().getAbsolutePath()); // Enable menu bar when this component has lost focus folderPanel.getMainFrame().getJFrame().getJMenuBar().setEnabled(true); } // - Theme listening ------------------------------------------------------------- // ------------------------------------------------------------------------------- /** * Receives theme color changes notifications. */ public void colorChanged(ColorChangedEvent event) { switch (event.getColorId()) { case Theme.LOCATION_BAR_PROGRESS_COLOR: setProgressColor(event.getColor()); break; case Theme.LOCATION_BAR_FOREGROUND_COLOR: setDisabledTextColor(event.getColor()); setForeground(event.getColor()); break; case Theme.LOCATION_BAR_BACKGROUND_COLOR: setBackground(event.getColor()); break; case Theme.LOCATION_BAR_SELECTED_FOREGROUND_COLOR: setSelectedTextColor(event.getColor()); break; case Theme.LOCATION_BAR_SELECTED_BACKGROUND_COLOR: setSelectionColor(event.getColor()); break; } } /** * Receives theme font changes notifications. */ public void fontChanged(FontChangedEvent event) { if (event.getFontId() == Theme.LOCATION_BAR_FONT) { setFont(event.getFont()); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/MainFrame.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.apple.eawt.FullScreenUtilities; import com.mucommander.commons.file.AbstractArchiveEntryFile; import com.mucommander.commons.file.AbstractArchiveFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.conf.TcSnapshot; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.PreloadedJFrame; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.impl.CloseWindowAction; import com.mucommander.ui.button.ToolbarMoreButton; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.layout.ProportionalSplitPane; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.commandbar.CommandBar; import com.mucommander.ui.main.menu.MainMenuBar; import com.mucommander.ui.main.statusbar.StatusBar; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.full.FileTableConfiguration; import com.mucommander.ui.main.table.SortInfo; import com.mucommander.ui.main.tabs.ConfFileTableTab; import com.mucommander.ui.main.toolbar.ToolBar; import com.mucommander.ui.terminal.TcTerminal; import lombok.Getter; import javax.swing.*; import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.*; import java.util.List; /** * This is the main frame, which contains all other UI components visible on a mucommander window. * * @author Maxence Bernard */ public class MainFrame implements LocationListener { private final JFrame frameInstance; /** * -- GETTER -- * Returns the ProportionalSplitPane component that splits the two panels. */ @Getter private ProportionalSplitPane splitPane; private FolderPanel leftFolderPanel; private FolderPanel rightFolderPanel; private FileTable leftTable; private FileTable rightTable; /** Active table in the MainFrame * -- GETTER -- * Returns the currently active table. *

    The returned table doesn't necessarily have focus, the focus can be in some other component * of the active * , or nowhere in the MainFrame if it is currently not in the foreground. *

    Use * to test if the table currently has focus. * */ @Getter private FileTable activeTable; private TcTerminal tcTerminal; private JSplitPane terminalSplitPane; /** Toolbar panel */ private JPanel toolbarPanel; /** Toolbar component */ private ToolBar toolbar; /** Status bar instance * -- GETTER -- * Returns the status bar, where information about selected files and volume are displayed. * Note that a non-null instance of * is returned even if it is currently hidden. */ @Getter private StatusBar statusBar; /** Command bar instance * -- GETTER -- * Returns the * , i.e. the component that contains shortcuts to certains actions such as * View, Edit, Copy, Move, etc... * Note that a non-null instance of * is returned even if it is currently hidden. */ @Getter private CommandBar commandBar; /** Is no events mode enabled ? */ private boolean noEventsMode; /** Is this MainFrame active in the foreground ? * -- GETTER -- * Returns true if this MainFrame is currently active in the foreground. */ @Getter private boolean foregroundActive; /** Is single panel view? * -- GETTER -- * Returns true if only one panel is show */ @Getter private boolean singlePanel; /** Contains all registered ActivePanelListener instances, stored as weak references */ private final Map activePanelListeners = Collections.synchronizedMap(new WeakHashMap<>()); private JPanel insetsPane; public MainFrame(ConfFileTableTab leftTab, FileTableConfiguration leftTableConf, ConfFileTableTab rightTab, FileTableConfiguration rightTableConf) { this(new ConfFileTableTab[] {leftTab}, 0, leftTableConf, new ConfFileTableTab[] {rightTab}, 0, rightTableConf); } /** * Creates a new main frame set to the given initial folders. * * @param leftTabs left panel tabs configuration * @param indexOfLeftSelectedTab index of left selected tab * @param leftTableConf left table configuration * @param rightTabs right panel tabs configuration * @param indexOfRightSelectedTab index of right selected tab * @param rightTableConf right table configuration */ public MainFrame(ConfFileTableTab[] leftTabs, int indexOfLeftSelectedTab, FileTableConfiguration leftTableConf, ConfFileTableTab[] rightTabs, int indexOfRightSelectedTab, FileTableConfiguration rightTableConf) { frameInstance = PreloadedJFrame.getJFrame(this); FolderPanel leftPanel = new FolderPanel(this, leftTabs, indexOfLeftSelectedTab, leftTableConf); FolderPanel rightPanel = new FolderPanel(this, rightTabs, indexOfRightSelectedTab, rightTableConf); init(leftPanel, rightPanel); for (boolean isLeft = true; ; isLeft = false) { FileTable fileTable = isLeft ? leftTable : rightTable; fileTable.sortBy(Column.valueOf(TcConfigurations.getSnapshot().getVariable(TcSnapshot.getFileTableSortByVariable(0, isLeft), TcSnapshot.DEFAULT_SORT_BY).toUpperCase()), !TcConfigurations.getSnapshot().getVariable(TcSnapshot.getFileTableSortOrderVariable(0, isLeft), TcSnapshot.DEFAULT_SORT_ORDER).equals(TcSnapshot.SORT_ORDER_DESCENDING)); FolderPanel folderPanel = isLeft ? leftFolderPanel : rightFolderPanel; folderPanel.setTreeWidth(TcConfigurations.getSnapshot().getVariable(TcSnapshot.getTreeWidthVariable(0, isLeft), 150)); folderPanel.setTreeVisible(TcConfigurations.getSnapshot().getVariable(TcSnapshot.getTreeVisiblityVariable(0, isLeft), false)); if (!isLeft) break; } } /** * Copy constructor */ public MainFrame(MainFrame mainFrame) { frameInstance = PreloadedJFrame.getJFrame(this); FolderPanel leftFolderPanel = mainFrame.getLeftPanel(); FolderPanel rightFolderPanel = mainFrame.getRightPanel(); FileTable leftFileTable = leftFolderPanel.getFileTable(); FileTable rightFileTable = rightFolderPanel.getFileTable(); init(new FolderPanel(this, new ConfFileTableTab[] { new ConfFileTableTab(leftFolderPanel.getCurrentFolder().getURL()) }, 0, leftFileTable.getConfiguration()), new FolderPanel(this, new ConfFileTableTab[] { new ConfFileTableTab(rightFolderPanel.getCurrentFolder().getURL()) }, 0, rightFileTable.getConfiguration())); // TODO: Sorting should be part of the FileTable configuration this.leftTable.sortBy(leftFileTable.getSortInfo()); this.rightTable.sortBy(rightFileTable.getSortInfo()); } private void init(FolderPanel leftFolderPanel, FolderPanel rightFolderPanel) { // Set the window icon setWindowIcon(); DesktopManager.customizeMainFrame(frameInstance); //initLookAndFeel(); if (OsFamily.MAC_OS_X.isCurrent()) { FullScreenUtilities.setWindowCanFullScreen(frameInstance, true); // Lion Fullscreen support } // Enable window resize frameInstance.setResizable(true); // The toolbar should have no inset, this is why it is left out of the insetsPane JPanel contentPane = new JPanel(new BorderLayout()); frameInstance.setContentPane(contentPane); // Initializes the folder panels and file tables. this.leftFolderPanel = leftFolderPanel; this.rightFolderPanel = rightFolderPanel; leftTable = leftFolderPanel.getFileTable(); rightTable = rightFolderPanel.getFileTable(); activeTable = leftTable; // create the toolbar and corresponding panel wrapping it, and show it only if it hasn't been disabled in the preferences. // Note: Toolbar.setVisible() has to be called no matter if Toolbar is visible or not, in order for it to be properly initialized this.toolbar = new ToolBar(this); this.toolbarPanel = ToolbarMoreButton.wrapToolBar(toolbar); this.toolbarPanel.setVisible(TcConfigurations.getPreferences().getVariable(TcPreference.TOOLBAR_VISIBLE, TcPreferences.DEFAULT_TOOLBAR_VISIBLE)); contentPane.add(toolbarPanel, BorderLayout.NORTH); insetsPane = new JPanel(new BorderLayout()) { // Add an x=3,y=3 gap around content pane @Override public Insets getInsets() { return new Insets(0, 3, 3, 3); // No top inset } }; // Below the toolbar there is the pane with insets contentPane.add(insetsPane, BorderLayout.CENTER); // Listen to location change events to display the current folder in the window's title leftFolderPanel.getLocationManager().addLocationListener(this); rightFolderPanel.getLocationManager().addLocationListener(this); // create menu bar (has to be created after toolbar) MainMenuBar menuBar = new MainMenuBar(this); frameInstance.setJMenuBar(menuBar); // create the split pane that separates folder panels and allows to resize how much space is allocated to the // both of them. The split orientation is loaded from and saved to the preferences. // Note: the vertical/horizontal terminology used in muCommander is just the opposite of the one used // in JSplitPane which is anti-natural / confusing. int splitOrientation = TcConfigurations.getSnapshot().getVariable(TcSnapshot.getSplitOrientation(0), TcSnapshot.DEFAULT_SPLIT_ORIENTATION).equals(TcSnapshot.VERTICAL_SPLIT_ORIENTATION) ? JSplitPane.HORIZONTAL_SPLIT : JSplitPane.VERTICAL_SPLIT; splitPane = new ProportionalSplitPane(frameInstance, splitOrientation,false, MainFrame.this.leftFolderPanel.getPanel(), MainFrame.this.rightFolderPanel.getPanel()) { }; // Remove any default border the split pane has splitPane.setBorder(null); // Adds buttons that allow to collapse and expand the split pane in both directions splitPane.setOneTouchExpandable(true); // Disable all the JSPlitPane accessibility shortcuts that are registered by default, as some of them // conflict with default trolCommander action shortcuts (e.g. F6 and F8) splitPane.disableAccessibilityShortcuts(); // Split pane will be given any extra space insetsPane.add(splitPane, BorderLayout.CENTER); // Add a 2-pixel gap between the file table and status bar YBoxPanel southPanel = new YBoxPanel(); // southPanel.addSpace(2); // Add status bar this.statusBar = new StatusBar(this); southPanel.add(statusBar); // Show command bar only if it hasn't been disabled in the preferences this.commandBar = new CommandBar(this); // Note: CommandBar.setVisible() has to be called no matter if CommandBar is visible or not, in order for it to be properly initialized this.commandBar.setVisible(TcConfigurations.getPreferences().getVariable(TcPreference.COMMAND_BAR_VISIBLE, TcPreferences.DEFAULT_COMMAND_BAR_VISIBLE)); southPanel.add(commandBar); insetsPane.add(southPanel, BorderLayout.SOUTH); // Perform CloseAction when the user asked the window to close frameInstance.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); frameInstance.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { ActionManager.performAction(CloseWindowAction.Descriptor.ACTION_ID, MainFrame.this); } }); ActionKeymap.registerActions(this); // Fire table change events on registered ActivePanelListener instances, to notify of the initial active table. fireActivePanelChanged(activeTable.getFolderPanel()); // Set the custom FocusTraversalPolicy that manages focus for both FolderPanel and their subcomponents. frameInstance.setFocusTraversalPolicy(new CustomFocusTraversalPolicy()); } /** * Sets the window icon, using the best method (Java 1.6's Window#setIconImages when available, Window#setIconImage * otherwise) and icon resolution(s) (OS-dependent). */ private void setWindowIcon() { // TODO: this code should probably be moved to the desktop API // - Mac OS X completely ignores calls to #setIconImage/setIconImages, no need to waste time if (OsFamily.MAC_OS_X.isCurrent()) { return; } // Use Java 1.6 's new Window#setIconImages(List) when available List icons = new ArrayList<>(); // Start by adding a 16x16 image with 1-bit transparency, any OS should support that. icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon16_8.png").getImage()); // - Windows XP messes up 8-bit PNG transparency. // We would be better off with the .ico of the launch4j exe (which has 8-bit alpha transparency) but there // seems to be no way to keep it when in 'dontWrapJar' mode (separate exe and jar files). if (OsFamily.WINDOWS.isCurrent() && OsVersion.WINDOWS_XP.isCurrentOrLower()) { icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon48_8.png").getImage()); } // - Windows Vista supports 8-bit transparency and icon resolutions up to 256x256. // - GNOME and KDE support 8-bit transparency. else { // Add PNG 24 images (8-bit transparency) icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon16_24.png").getImage()); icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon32_24.png").getImage()); icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon48_24.png").getImage()); icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon128_24.png").getImage()); icons.add(IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, "icon256_24.png").getImage()); } frameInstance.setIconImages(icons); } /** * Registers the given ActivePanelListener to receive events when the active table changes. * * @param activePanelListener the ActivePanelListener to add */ public void addActivePanelListener(ActivePanelListener activePanelListener) { activePanelListeners.put(activePanelListener, null); } /** * Unregisters the given ActivePanelListener so that it no longer receives events when the active table changes. * * @param activePanelListener the ActivePanelListener to remove */ public void removeActivePanelListener(ActivePanelListener activePanelListener) { activePanelListeners.remove(activePanelListener); } /** * Fires table change events on all registered ActivePanelListener instances. * * @param folderPanel the new active panel */ private void fireActivePanelChanged(FolderPanel folderPanel) { for (ActivePanelListener listener : activePanelListeners.keySet()) { listener.activePanelChanged(folderPanel); } } /** * Returns true if 'no events mode' is currently enabled. * * @return true if 'no events mode' is currently enabled */ public boolean getNoEventsMode() { return this.noEventsMode; } /** * Enables/disables the 'no events mode' which prevents mouse and keyboard events from being received * by the application (MainFrame, its subcomponents and the menu bar). * * @param enabled true to enable 'no events mode', false to disable it */ public void setNoEventsMode(boolean enabled) { // Piece of code used in 0.8 beta1 and removed after because it's way too slow, kept here for the record // // Glass pane has empty mouse and key adapters (created in the constructor) // // which will catch all mouse and keyboard events // getGlassPane().setVisible(enabled); // getJMenuBar().setEnabled(!enabled); // // Remove focus from whatever component in FolderPanel which had focus // getGlassPane().requestFocus(); this.noEventsMode = enabled; } /** * Returns the {@link ToolBar} where shortcut buttons (go back, go forward, ...) are. * Note that a non-null instance of {@link ToolBar} is returned even if it is currently hidden. * * @return the toolbar component */ public ToolBar getToolBar() { return toolbar; } /** * Returns the panel where the {@link ToolBar} component is. * Note that a non-null instance of {@link ToolBar} is returned even if it is currently hidden. * * @return the toolbar component */ public JPanel getToolBarPanel() { return toolbarPanel; } /** * Returns the currently active panel. * *

    The returned panel doesn't necessarily have focus, for example if the MainFrame is currently not in the * foreground. * * @return the currently active panel */ public FolderPanel getActivePanel() { return activeTable.getFolderPanel(); } /** * Sets the currently active FileTable. This method is to be called by FolderPanel only. * * @param table the currently active FileTable */ void setActiveTable(FileTable table) { boolean activeTableChanged = activeTable != table; if (activeTableChanged) { this.activeTable = table; // Update window title to reflect new active table updateWindowTitle(); // Fire table change events on registered ActivePanelListener instances. fireActivePanelChanged(table.getFolderPanel()); } } /** * Returns the inactive table, i.e. the complement of {@link #getActiveTable()}. * * @return the inactive table */ public FileTable getInactiveTable() { return activeTable == leftTable ? rightTable : leftTable; } /** * Returns the inactive panel, i.e. the complement of {@link #getActivePanel()}. * * @return the inactive panel */ public FolderPanel getInactivePanel() { return getInactiveTable().getFolderPanel(); } /** * Returns the FolderPanel instance corresponding to the left panel. * * @return the FolderPanel instance corresponding to the left panel */ public FolderPanel getLeftPanel() { return leftFolderPanel; } /** * Returns the FolderPanel instance corresponding to the right panel. * * @return the FolderPanel instance corresponding to the right panel */ public FolderPanel getRightPanel() { return rightFolderPanel; } /** * Specifies how folder panels are split: if true is passed, the folder panels will be split vertically * (default), horizontally otherwise. * * @param vertical if true, the folder panels will be split horizontally (default), vertically otherwise. */ public void setSplitPaneOrientation(boolean vertical) { // Note: the vertical/horizontal terminology used in muCommander is just the opposite of the one used // in JSplitPane which is anti-natural / confusing splitPane.setOrientation(vertical ? JSplitPane.HORIZONTAL_SPLIT : JSplitPane.VERTICAL_SPLIT); } /** * Returns how folder panels are currently split: if true is returned, panels are split vertically * (default), horizontally otherwise. * * @return true if folder panels are split vertically */ public boolean getSplitPaneOrientation() { // Note: the vertical/horizontal terminology used in muCommander is just the opposite of the one used // in JSplitPane which is anti-natural / confusing return splitPane.getOrientation() == JSplitPane.HORIZONTAL_SPLIT; } /** * Swaps the two FolderPanel instances: after a call to this method, the left FolderPanel will be the right one and * vice-versa. */ public void swapFolders() { splitPane.remove(leftFolderPanel.getPanel()); splitPane.remove(rightFolderPanel.getPanel()); // Swaps the folder panels. FolderPanel tempPanel = leftFolderPanel; leftFolderPanel = rightFolderPanel; rightFolderPanel = tempPanel; // swaps folders trees int tempTreeWidth = leftFolderPanel.getTreeWidth(); leftFolderPanel.setTreeWidth(rightFolderPanel.getTreeWidth()); rightFolderPanel.setTreeWidth(tempTreeWidth); boolean tempTreeVisible = leftFolderPanel.isTreeVisible(); leftFolderPanel.setTreeVisible(rightFolderPanel.isTreeVisible()); rightFolderPanel.setTreeVisible(tempTreeVisible); // Resets the tables. FileTable tempTable = leftTable; leftTable = rightTable; rightTable = tempTable; // Preserve the sort order and columns visibility. TableColumnModel model = leftTable.getColumnModel(); leftTable.setColumnModel(rightTable.getColumnModel()); rightTable.setColumnModel(model); SortInfo sortInfo = leftTable.getSortInfo().clone(); leftTable.sortBy(rightTable.getSortInfo()); leftTable.updateColumnsVisibility(); rightTable.sortBy(sortInfo); rightTable.updateColumnsVisibility(); // Do the swap and update the split pane splitPane.setLeftComponent(leftFolderPanel.getPanel()); splitPane.setRightComponent(rightFolderPanel.getPanel()); splitPane.doLayout(); // Update split pane divider's location splitPane.updateDividerLocation(); activeTable.requestFocus(); } /** * Makes both folders the same, choosing the one which is currently active. */ public void setSameFolder() { (activeTable == leftTable ? rightTable : leftTable).getFolderPanel().tryChangeCurrentFolder(activeTable.getFolderPanel().getCurrentFolder()); } /** * Sets whether this MainFrame is currently active in the foreground. This method is to be called by WindowManager * only. * * @param foregroundActive true if this MainFrame is currently active in the foreground */ void setForegroundActive(boolean foregroundActive) { this.foregroundActive = foregroundActive; } /** * Forces a refresh of the frame's folder panel. */ public void tryRefreshCurrentFolders() { leftFolderPanel.tryRefreshCurrentFolder(); rightFolderPanel.tryRefreshCurrentFolder(); } /** * Returns true if this MainFrame is active, or is an ancestor of a Window that is currently active. * * @return true if this MainFrame is active, or is an ancestor of a Window that is currently active */ public boolean isAncestorOfActiveWindow() { if (frameInstance.isActive()) { return true; } for (Window w : frameInstance.getOwnedWindows()) { if (w.isActive()) { return true; } } return false; } /** * Updates this window's title to show currently active folder and window number. * This method is called by this class and WindowManager. */ public void updateWindowTitle() { // Update window title AbstractFile currentFolder = activeTable.getFolderPanel().getCurrentFolder(); String title = currentFolder != null ? currentFolder.getAbsolutePath() : ""; // Add the application name to window title on all OSs except MAC if (!OsFamily.MAC_OS_X.isCurrent()) { title += " - trolCommander"; } List mainFrames = WindowManager.getMainFrames(); if (mainFrames.size() > 1) { title += " [" + (mainFrames.indexOf(this) + 1) + "]"; } frameInstance.setTitle(title); // Use new Window decorations introduced in Mac OS X 10.5 (Leopard) if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher() && currentFolder != null) { // Displays the document icon in the window title bar, works only for local files Object javaIoFile; if (currentFolder.getURL().getScheme().equals(FileProtocols.FILE)) { // If the current folder is an archive entry, display the archive file, this is the closest we can get // with a java.io.File if (currentFolder.hasAncestor(AbstractArchiveEntryFile.class)) { AbstractArchiveFile parent = currentFolder.getParentArchive(); javaIoFile = parent != null ? parent.getUnderlyingFileObject() : null; } else { javaIoFile = currentFolder.getUnderlyingFileObject(); } } else { // If the current folder is not a local file, use the special /Network directory which is sort of // 'Network Neighborhood'. javaIoFile = new java.io.File("/Network"); } // Note that for some strange reason (looks like a bug), setting the property to null won't remove // the previous icon. frameInstance.getRootPane().putClientProperty("Window.documentFile", javaIoFile); } } /** * Toggles single panel view state and returns new one * * @return new state for singlePanel boolean */ public boolean toggleSinglePanel() { singlePanel = !singlePanel; return singlePanel; } /** * Overrides java.awt.Window#toFront to have the window return to a normal state if it is minimized. */ public void toFront() { if ( (frameInstance.getExtendedState()&Frame.ICONIFIED) != 0) { frameInstance.setExtendedState(Frame.NORMAL); } frameInstance.toFront(); } /** * Manages focus for both FolderPanel and their subcomponents. * * @author Maxence Bernard */ protected class CustomFocusTraversalPolicy extends FocusTraversalPolicy { @Override public Component getComponentAfter(Container container, Component component) { if (component == leftFolderPanel.getFoldersTreePanel().getTree()) return leftTable; if (component == rightFolderPanel.getFoldersTreePanel().getTree()) return rightTable; if (component == leftFolderPanel.getLocationTextField()) return leftTable; if (component == leftTable) return rightTable; if (component == rightFolderPanel.getLocationTextField()) return rightTable; // otherwise (component==table2) return leftTable; } @Override public Component getComponentBefore(Container container, Component component) { // Completely symmetrical with getComponentAfter return getComponentAfter(container, component); } @Override public Component getFirstComponent(Container container) { return leftTable; } @Override public Component getLastComponent(Container container) { return rightTable; } @Override public Component getDefaultComponent(Container container) { return getActiveTable(); } } public boolean isAutoSizeColumnsEnabled() { return leftTable.isAutoSizeColumnsEnabled(); } public void setAutoSizeColumnsEnabled(boolean b) { leftTable.setAutoSizeColumnsEnabled(b); rightTable.setAutoSizeColumnsEnabled(b); } @Override public void locationChanged(LocationEvent e) { // Update window title to reflect the new current folder updateWindowTitle(); } @Override public void locationChanging(LocationEvent locationEvent) { } @Override public void locationCancelled(LocationEvent locationEvent) { } @Override public void locationFailed(LocationEvent locationEvent) { } public void showTerminalPanel(boolean show) { if (show) { if (tcTerminal == null) { tcTerminal = new TcTerminal(this); terminalSplitPane = new JSplitPane(); terminalSplitPane.setBorder(null); terminalSplitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); } terminalSplitPane.setTopComponent(splitPane); terminalSplitPane.setBottomComponent(tcTerminal.getComponent()); int height = tcTerminal.loadHeight(); if (height < 0) { height = frameInstance.getHeight()/2; } terminalSplitPane.setDividerLocation(height); tcTerminal.show(true); insetsPane.remove(splitPane); insetsPane.add(terminalSplitPane, BorderLayout.CENTER); tcTerminal.updateTitle(); frameInstance.revalidate(); tcTerminal.getComponent().requestFocusInWindow(); } else if (tcTerminal != null) { tcTerminal.storeHeight(terminalSplitPane.getDividerLocation()); insetsPane.remove(terminalSplitPane); insetsPane.add(splitPane, BorderLayout.CENTER); tcTerminal.show(false); frameInstance.revalidate(); activeTable.requestFocus(); updateWindowTitle(); } } public void toggleTerminalPanel() { JComponent term = tcTerminal != null ? tcTerminal.getComponent() : null; if (term != null && !term.hasFocus() && term.getParent().getParent() != null) { tcTerminal.getComponent().getComponent(0).requestFocus(); } else { showTerminalPanel(tcTerminal == null || !tcTerminal.getComponent().isVisible()); } } public void closeTerminalSession() { showTerminalPanel(false); tcTerminal = null; terminalSplitPane = null; } public void setTerminalPanelHeight(int height) { if (height > terminalSplitPane.getHeight()) { height = terminalSplitPane.getHeight(); } terminalSplitPane.setDividerLocation(terminalSplitPane.getHeight() - height); } public int getTerminalPanelHeight() { return terminalSplitPane.getHeight() - terminalSplitPane.getDividerLocation(); } public JFrame getJFrame() { return frameInstance; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/PreviewPanel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.UserCancelledException; import com.mucommander.ui.viewer.ViewerRegistrar; import com.mucommander.ui.viewer.text.TextArea; import com.mucommander.ui.viewer.text.TextViewer; import net.sf.jftp.gui.tasks.ImageViewer; import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; import java.awt.event.KeyEvent; import java.io.IOException; /** * @author Oleg Trifonov * * Created on 26/09/2016. */ class PreviewPanel extends JPanel { private TextArea textArea; private TextViewer textViewer; private ImageViewer imageViewer; PreviewPanel() { super(new BorderLayout()); // No decoration for this panel setBorder(null); } private void setupTextArea() { textArea.setCurrentLineHighlightColor(ThemeManager.getCurrentColor(Theme.EDITOR_CURRENT_BACKGROUND_COLOR)); textArea.setAntiAliasingEnabled(true); textArea.setEditable(false); //textArea.addKeyListener(textAreaKeyListener); try { ThemeManager.readEditorTheme(ThemeManager.getCurrentSyntaxThemeName()).apply(textArea); } catch (Exception e) { e.printStackTrace(); } //textArea.setCodeFoldingEnabled(true); // Use theme colors and font textArea.setForeground(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR)); textArea.setCaretColor(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR)); Color background = ThemeManager.getCurrentColor(Theme.EDITOR_BACKGROUND_COLOR); textArea.setBackground(background); for (int i = 1; i <= textArea.getSecondaryLanguageCount(); i++) { textArea.setSecondaryLanguageBackground(i, background); } textArea.setSelectedTextColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_FOREGROUND_COLOR)); textArea.setSelectionColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_BACKGROUND_COLOR)); textArea.setFont(ThemeManager.getCurrentFont(Theme.EDITOR_FONT)); textArea.setCodeFoldingEnabled(true); textArea.setWrapStyleWord(true); textArea.addMouseWheelListener(e -> { boolean isCtrlPressed = (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0; if (isCtrlPressed) { Font currentFont = textArea.getFont(); int currentFontSize = currentFont.getSize(); boolean rotationUp = e.getWheelRotation() < 0; if (rotationUp || currentFontSize > 1) { Font newFont = new Font(currentFont.getName(), currentFont.getStyle(), currentFontSize + (rotationUp ? 1 : -1)); textArea.setFont(newFont); } } else { textArea.getParent().dispatchEvent(e); } }); //textArea.addCaretListener(caretListener); } void loadFile(AbstractFile file) { if (file == null) { clearPreviewArea(); doLayout(); repaint(); } else if (file.isDirectory()) { clearPreviewArea(); doLayout(); repaint(); } else { previewFile(file); } } private void previewFile(AbstractFile file) { try { FileViewer viewer = ViewerRegistrar.createFileViewer(file, null, null); clearPreviewArea(); if (viewer != null) { viewer.open(file); add(viewer); } } catch (UserCancelledException | IOException e) { e.printStackTrace(); } } private void clearPreviewArea() { while (getComponents().length > 0) { remove(0); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/QuickLists.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; /** * Enum of quick lists implemented in muCommander * * @author Arik Hadas */ public enum QuickLists { PARENT_FOLDERS, RECENT_ACCESSED_LOCATIONS, RECENT_EXECUTED_FILES, BOOKMARKS, ROOTS, TABS, RECENT_VIEWED_FILES, RECENT_EDITED_FILES, EDITOR_BOOKMARKS } ================================================ FILE: src/main/java/com/mucommander/ui/main/SplashScreen.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.icon.IconManager; import javax.swing.*; import java.awt.*; import java.awt.font.FontRenderContext; import java.awt.geom.Rectangle2D; /** * Splash screen that gets displayed on muCommander startup. * *

    The splash screen is made of a logo image on top of which is displayed muCommander version number (in the top right corner) * and a loading message (in the lower left corner) which is updated by {@link com.mucommander.TrolCommander} to show startup progress. * It is then closed by {@link com.mucommander.TrolCommander} when muCommander is fully started and ready for use. * * @author Maxence Bernard */ public class SplashScreen extends JWindow { /** trolCommander version displayed on this splash screen */ private final String version; /** Current loading message displayed on this splash screen */ private String loadingMessage; /** Font used to display version and loading message on this splash screen */ private final Font customFont; /** Path to the splash screen wo image within the JAR file */ private final static String SPLASH_IMAGE_PATH = IconManager.IconSet.TROLCOMMANDER.getFolder() + "splash.png"; /** Color of the text displayed on this splash screen */ private final static Color TEXT_COLOR = new Color(192, 238, 241); private final static Color SHADOW_TEXT_COLOR = new Color(0, 86, 117); /** Number of pixels between the loading message and the left side of the splash image */ private final static int LOADING_MSG_MARGIN_X = 4; /** Number of pixels between the loading message and the bottom of the splash image */ private final static int LOADING_MSG_MARGIN_Y = 6; /** Number of pixels between the version information and the right side of the splash image */ private final static int VERSION_MARGIN_X = 5; /** Number of pixels between the version information and the top of the splash image */ private final static int VERSION_MARGIN_Y = 3; /** * Creates and displays a new SplashScreen, with the given version string and initial loading message. * * @param version trolCommander version string which will be displayed in the top right corner * @param loadingMessage initial loading message, displayed in the lower left corner */ public SplashScreen(String version, String loadingMessage) { this.version = version; this.loadingMessage = loadingMessage; // create a custom font int fontSize = OsFamily.getCurrent() == OsFamily.LINUX && RuntimeConstants.DISPLAY_4K ? 24 : 11; this.customFont = new Font("Courier", Font.BOLD, fontSize); ImageIcon imageIcon = loadImageIcon(); setContentPane(new JLabel(imageIcon)); setSizeFromImage(imageIcon); DialogToolkit.centerOnScreen(this); // Display the splash screen setVisible(true); } private ImageIcon loadImageIcon() { // Resolve the URL of the splash logo image within the JAR file and create an ImageIcon // Note: DO NOT use IconManager to load the icon as it would trigger ConfigurationManager's initialization // and we don't want that, we want SplashScreen to be displayed as soon as possible ImageIcon imageIcon = new ImageIcon(ResourceLoader.getResourceAsURL(SPLASH_IMAGE_PATH)); // Wait for the image to be fully loaded MediaTracker mediaTracker = new MediaTracker(this); mediaTracker.addImage(imageIcon.getImage(), 0); try { mediaTracker.waitForID(0); } catch(InterruptedException e) { e.printStackTrace(); } return imageIcon; } private void setSizeFromImage(ImageIcon imageIcon) { // Set size manually instead of using pack(), because of a bug under 1.3.1/Win32 which // eats a 1-pixel row of the image int width = imageIcon.getIconWidth(); int height = imageIcon.getIconHeight(); setSize(width, height); } /** * Repaints this SplashScreen to display the new given loading message, replacing the previous one. * * @param msg the new loading message to be displayed */ public void setLoadingMessage(String msg) { this.loadingMessage = msg; repaint(); } /** * Overridden paint method. */ @Override public void paint(Graphics g) { super.paint(g); g.setFont(customFont); // Display loading message in the lower left corner int textX = LOADING_MSG_MARGIN_X; int textY = getHeight() - LOADING_MSG_MARGIN_Y; g.setColor(SHADOW_TEXT_COLOR); g.drawString(loadingMessage, textX-1, textY-1); g.setColor(TEXT_COLOR); g.drawString(loadingMessage, textX, textY); // Display version in the top right corner // Get FontRenderContext instance to calculate text width and height FontRenderContext fontRenderContext = ((Graphics2D)g).getFontRenderContext(); Rectangle2D textBounds = new java.awt.font.TextLayout(version, customFont, fontRenderContext).getBounds(); textX = getWidth()-(int)textBounds.getWidth()-VERSION_MARGIN_X; textY = (int)textBounds.getHeight()+VERSION_MARGIN_Y; g.setColor(SHADOW_TEXT_COLOR); g.drawString(version, textX-1, textY-1); g.setColor(TEXT_COLOR); g.drawString(version, textX, textY); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/SystemFileFilter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.AbstractFileFilter; /** * Filter used to filter out system files and folders that should not be displayed to inexperienced users. * *

    At the moment, this filter only supports Mac OS X top-level system folders (those hidden by Finder) * and thus this filter should only be used under Mac OS X. * * @author Maxence Bernard */ public class SystemFileFilter extends AbstractFileFilter { /** * Top-level Mac OS X system folders hidden by Finder. For more info about those files: * http://www.westwind.com/reference/OS-X/invisibles.html */ private final static String[] SYSTEM_FOLDERS = { // Mac OS X system folders "/.Trashes", "/.vol", "/dev", "/automount", "/bin", "/cores", "/etc", "/lost+found", "/Network", "/private", "/sbin", "/tmp", "/usr", "/var", // "/Volumes", "/mach.sym", "/mach_kernel", "/mach", "/Desktop DB", "/Desktop DF", "/File Transfer Folder", "/.hotfiles.btree", "/.Spotlight-V100", "/.hidden", // Used by Mac OS X up to 10.3, not in 10.4 System.getProperty("user.home")+"/.Trash", // User trash folder // Mac OS 9 system folders "/AppleShare PDS", "/Cleanup At Startup", "/Desktop Folder", "/Network Trash Folder", "/Shutdown Check", "/Temporary Items", System.getProperty("user.home")+"/Temporary Items", // User trash folder "/TheFindByContentFolder", "/TheVolumeSettingsFolder", "/Trash", "/VM Storage" }; /////////////////////////////// // FileFilter implementation // /////////////////////////////// public boolean accept(AbstractFile file) { String path = file.getAbsolutePath(false); for (String SYSTEM_FOLDER : SYSTEM_FOLDERS) { if (path.equals(SYSTEM_FOLDER)) { return false; } } return true; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/WindowManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main; import java.awt.Frame; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.List; import java.util.Vector; import javax.swing.LookAndFeel; import javax.swing.MenuSelectionManager; import javax.swing.SwingUtilities; import javax.swing.UIManager; import com.mucommander.ui.PreloadedJFrame; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.viewer.FileFrame; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import com.mucommander.launcher.ShutdownHook; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.extension.ExtensionManager; import com.mucommander.ui.main.commandbar.CommandBar; import com.mucommander.ui.main.frame.MainFrameBuilder; import com.mucommander.ui.helper.FocusRequester; /** * Window Manager is responsible for creating, disposing, switching, * in other words managing :) muCommander windows. * * @author Maxence Bernard, Arik Hadas */ //public class WindowManager implements ActionListener, WindowListener, ActivePanelListener, LocationListener, ConfigurationListener { @Slf4j public class WindowManager implements WindowListener, ConfigurationListener { // The following constants are used to compute the proper position of a new MainFrame. /** MainFrame (main trolCommander window) instances */ private final List mainFrames = new Vector<>(); /** MainFrame currently being used (that has focus), * or last frame to have been used if muCommander doesn't have focus */ private MainFrame currentMainFrame; /** * -- GETTER -- * Returns the sole instance of WindowManager. */ @Getter private static final WindowManager instance = new WindowManager(); /** * Installs all custom look and feels. */ private static void installCustomLookAndFeels() { // Get all available custom look and feels List plafs = TcConfigurations.getPreferences().getListVariable(TcPreference.CUSTOM_LOOK_AND_FEELS, TcPreferences.CUSTOM_LOOK_AND_FEELS_SEPARATOR); // Tries to retrieve the custom look and feels list. if (plafs == null) { return; } // Goes through the list and install every custom look and feel we could find. // Look and feels that aren't supported under the current platform are ignored. for (String plaf : plafs) { try { installLookAndFeel(plaf); } catch(Throwable e) { log.info("Failed to install Look&Feel {}", plaf, e); } } } /** * Creates a new instance of WindowManager. */ private WindowManager() { // Notifies Swing that look&feels must be loaded as extensions. // This is necessary to ensure that look and feels placed in the extensions folder // are accessible. UIManager.getDefaults().put("ClassLoader", ExtensionManager.getClassLoader()); // Installs all custom look and feels. installCustomLookAndFeels(); TcConfigurations.addPreferencesListener(this); } public static void setupAfterCreation() { // Sets custom lookAndFeel if different from current lookAndFeel String lnfName = TcConfigurations.getPreferences().getVariable(TcPreference.LOOK_AND_FEEL); if (lnfName != null && !lnfName.equals(UIManager.getLookAndFeel().getName())) { setLookAndFeel(lnfName); } if (lnfName == null) { log.debug("Could load look'n feel from preferences"); } } /** * Returns the MainFrame instance that was last active. Note that the returned MainFrame * may or may not be currently active. * * @return the MainFrame instance that was last active */ public static MainFrame getCurrentMainFrame() { return instance.currentMainFrame; } /** * Returns a Vector of all MainFrame instances currently displaying. * * @return a Vector of all MainFrame instances currently displaying */ public static List getMainFrames() { return instance.mainFrames; } /** * Refreshes all panels in all frames in an asynchronous manner. */ public static void tryRefreshCurrentFolders() { // Starts with the main frame to make sure that results are immediately // visible to the user. instance.currentMainFrame.tryRefreshCurrentFolders(); for (MainFrame mainFrame : instance.mainFrames) { if (mainFrame != instance.currentMainFrame) { mainFrame.tryRefreshCurrentFolders(); } } } /** * Creates a new MainFrame and makes it visible on the screen, on top of any other frames. * * @param mainFrameBuilder MainFrameBuilder class */ public static synchronized void createNewMainFrame(MainFrameBuilder mainFrameBuilder) { MainFrame[] newMainFrames = mainFrameBuilder.build(); // To catch user window closing actions for (MainFrame frame : newMainFrames) { frame.getJFrame().addWindowListener(instance); } // Adds the new MainFrame to the vector instance.mainFrames.addAll(Arrays.asList(newMainFrames)); // Set new window's title. Window titles show window number only if there is more than one window. // So if a second window was just created, we update first window's title so that it shows window number (#1). for (MainFrame frame : instance.mainFrames) { frame.updateWindowTitle(); } // Make frames visible for (MainFrame frame : newMainFrames) { frame.getJFrame().setVisible(true); } if (!instance.mainFrames.isEmpty()) { int previouslySelectedMainFrame = mainFrameBuilder.getSelectedFrame(); instance.mainFrames.get(previouslySelectedMainFrame).toFront(); } } /** * Disposes all opened windows, ending with the one that is currently active if there is one, * or the last one which was activated. */ public static synchronized void quit() { // Dispose all MainFrames, ending with the currently active one. int nbFrames = instance.mainFrames.size(); if (nbFrames > 0) { // If an uncaught exception occurred in the startup sequence, there is no MainFrame to dispose // Retrieve current MainFrame's index int currentMainFrameIndex = getCurrentWindowIndex(); // Dispose all MainFrames but the current one for (int i = 0; i < nbFrames; i++) { if (i != currentMainFrameIndex) { instance.mainFrames.get(i).getJFrame().dispose(); } } // Dispose current MainFrame last so that its attributes (last folders, window position...) are saved last // in the preferences if (currentMainFrameIndex >= 0) { instance.mainFrames.get(currentMainFrameIndex).getJFrame().dispose(); } } // Dispose all other frames (viewers, editors...) for (Frame frame : Frame.getFrames()) { if (frame.isShowing()) { log.debug("disposing frame {}", frame.getTitle()); frame.dispose(); } } // Initiate shutdown sequence. // Important note: we cannot rely on windowClosed() triggering the shutdown sequence as // Quit under OS X shuts down the app as soon as this method returns and as a result, // windowClosed() events are never dispatched to the MainFrames ShutdownHook.initiateShutdown(); } /** * Returns the index of the currently selected window * @return index of currently selected window */ public static int getCurrentWindowIndex() { return instance.mainFrames.indexOf(instance.currentMainFrame); } /** * Switches to the next MainFrame, in the order of which they were created. */ public static void switchToNextWindow() { int frameIndex = getCurrentWindowIndex(); MainFrame mainFrame = instance.mainFrames.get((frameIndex+1) % instance.mainFrames.size()); mainFrame.toFront(); } /** * Switches to previous MainFrame, in the order of which they were created. */ public static void switchToPreviousWindow() { int frameIndex = getCurrentWindowIndex(); int nbFrames = instance.mainFrames.size(); MainFrame mainFrame = instance.mainFrames.get((frameIndex-1+nbFrames) % nbFrames); mainFrame.toFront(); } public static void installLookAndFeel(String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException { LookAndFeel plaf = (LookAndFeel)Class.forName(className, true, ExtensionManager.getClassLoader()).getEnclosingConstructor().newInstance(); if (plaf.isSupportedLookAndFeel()) { UIManager.installLookAndFeel(plaf.getName(), plaf.getClass().getName()); } } /** * Changes LooknFeel to the given one, updating the UI of each MainFrame. * * @param lnfName name of the new LooknFeel to use */ private static void setLookAndFeel(String lnfName) { try { // Initializes class loading. // This is necessary due to Swing's UIDefaults.LazyProxyValue behavior that just // won't use the right ClassLoader instance to load resources. Thread currentThread = Thread.currentThread(); ClassLoader oldLoader = currentThread.getContextClassLoader(); currentThread.setContextClassLoader(ExtensionManager.getClassLoader()); UIManager.setLookAndFeel((LookAndFeel)Class.forName(lnfName, true, ExtensionManager.getClassLoader()).getDeclaredConstructor().newInstance()); // Restores the contextual ClassLoader. currentThread.setContextClassLoader(oldLoader); for (MainFrame mainFrame : instance.mainFrames) { SwingUtilities.updateComponentTreeUI(mainFrame.getJFrame()); } } catch(Throwable e) { log.debug("Exception caught", e); } } @Override public void windowActivated(WindowEvent e) { Object source = e.getSource(); // Return if event doesn't originate from a MainFrame/PreloadedJFrame (e.g. ViewerFrame or EditorFrame) if(!(source instanceof PreloadedJFrame)) return; currentMainFrame = (MainFrame) ((PreloadedJFrame)source).getMainFrameObject(); // Let MainFrame know that it is active in the foreground currentMainFrame.setForegroundActive(true); // Resets shift mode to false, since keyReleased events may have been lost during window switching CommandBar commandBar = currentMainFrame.getCommandBar(); if (commandBar != null) { commandBar.setAlternateActionsMode(false); } } @Override public void windowDeactivated(WindowEvent e) { Object source = e.getSource(); // Workaround for JRE bug #4841881 (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4841881) / // which causes Alt+Tab to focus the menu bar under certain L&F. // This bug has also been reported as trolCommmander bug #89. MenuSelectionManager.defaultManager().clearSelectedPath(); // Return if event doesn't originate from a MainFrame (e.g. ViewerFrame or EditorFrame) if (source instanceof PreloadedJFrame pf) { // Let MainFrame know that it is not active anymore ((MainFrame)(pf).getMainFrameObject()).setForegroundActive(false); } } @Override public void windowClosing(WindowEvent e) { } /** * windowClosed is synchronized so that it doesn't get called while quit() is executing. */ @Override public synchronized void windowClosed(WindowEvent e) { log.trace("called"); Object source = e.getSource(); if (source instanceof PreloadedJFrame pfSource) { var mainFrame = (MainFrame) (pfSource).getMainFrameObject(); closeMainFrame(mainFrame); } else if (source instanceof FileFrame fileFrame) { if (fileFrame.getReturnFocusTo() != null) { return; } } else if (source instanceof FocusDialog focusDialog) { if (focusDialog.getReturnFocusTo() != null) { return; } } // Test if there is at least one MainFrame still showing if (!mainFrames.isEmpty()) { FocusRequester.requestFocus(currentMainFrame.getJFrame()); } else if (!hasMoreActiveFrames()) { ShutdownHook.initiateShutdown(); } } /** * Remove disposed MainFrame from the MainFrame list */ private void closeMainFrame(MainFrame mainFrame) { int frameIndex = mainFrames.indexOf(mainFrame); mainFrames.remove(mainFrame); // Update following windows titles to reflect the MainFrame's disposal. // Window titles show window number only if there is more than one window. // So if there is only one window left, we update first window's title so that it removes window number (#1). int nbFrames = mainFrames.size(); if (nbFrames == 1) { mainFrames.getFirst().updateWindowTitle(); } else { if (frameIndex >= 0) { for (int i = frameIndex; i < nbFrames; i++) { mainFrames.get(i).updateWindowTitle(); } } } } /** * Test if there is at least one window (viewer, editor...) still showing */ private boolean hasMoreActiveFrames() { Frame[] frames = Frame.getFrames(); for (Frame frame : frames) { if (frame.isShowing()) { log.debug("found active frame"); return true; } } return false; } @Override public void windowIconified(WindowEvent e) { } @Override public void windowDeiconified(WindowEvent e) { } @Override public void windowOpened(WindowEvent e) { } /** * Listens to certain configuration variables. */ @Override public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); // /!\ font.size is set after font.family in AppearancePrefPanel // that's why we only listen to this one in order not to change Font twice if (TcPreferences.LOOK_AND_FEEL.equals(var)) { String lnfName = event.getValue(); if (!UIManager.getLookAndFeel().getClass().getName().equals(lnfName)) { setLookAndFeel(lnfName); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/commandbar/CommandBar.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.commandbar; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * CommandBar is the button bar that sits at the bottom of the main window and provides access to * main commander actions (view, edit, copy, move...). * * @author Maxence Bernard, Arik Hadas */ public class CommandBar extends JPanel implements KeyListener, MouseListener, CommandBarAttributesListener { /** Parent MainFrame instance */ private final MainFrame mainFrame; /** True when modifier key is pressed */ private boolean modifierDown; /** Command bar buttons */ private CommandBarButton[] buttons; /** Command bar actions */ private static String[] actionIds; /** Command bar alternate actions */ private static String[] alternateActionIds; /** Modifier key that triggers the display of alternate actions when pressed */ private static KeyStroke modifier; /** * Creates a new CommandBar instance associated with the given MainFrame. */ public CommandBar(MainFrame mainFrame) { this.mainFrame = mainFrame; // Listen to modifier key events to display alternate actions mainFrame.getLeftPanel().getFileTable().addKeyListener(this); mainFrame.getRightPanel().getFileTable().addKeyListener(this); // Listen to mouse events to popup a menu when command bar is right clicked addMouseListener(this); actionIds = CommandBarAttributes.getActions(); alternateActionIds = CommandBarAttributes.getAlternateActions(); modifier = CommandBarAttributes.getModifier(); addButtons(); CommandBarAttributes.addCommandBarAttributesListener(this); } /** * Add buttons and separators to the command-bar panel according to the actions array. * * actions array must be initialized before this function is called. */ private void addButtons() { setLayout(new GridLayout(0, actionIds.length)); // create buttons and add them to this command bar int nbButtons = actionIds.length; buttons = new CommandBarButton[nbButtons]; for (int i = 0; i < nbButtons; i++) { buttons[i] = CommandBarButton.create(actionIds[i], mainFrame); if (buttons[i] != null) { buttons[i].addMouseListener(this); add(buttons[i]); } } } /** * Displays/hides alternate actions: buttons that have an alternate action show it when the command bar's * modifier is pressed (Shift by default). */ public void setAlternateActionsMode(boolean on) { // Do nothing if command bar is not currently visible if (!isVisible()) { return; } if (this.modifierDown != on) { this.modifierDown = on; int nbButtons = buttons.length; for (int i=0; i. */ package com.mucommander.ui.main.commandbar; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.impl.*; import javax.swing.*; import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.WeakHashMap; /** * This class is responsible to handle the attributes of CommandBars - their actions, alternate actions and modifier. * Every CommandBar should get its attributes from this class, and register in it for receiving attributes modifications. * * @author Arik Hadas */ public class CommandBarAttributes { /** Command bar actions */ private static String[] actionIds; /** Command bar alternate actions */ private static String[] alternateActionIds; /** Modifier key that triggers the display of alternate actions when pressed */ private static KeyStroke modifier; /** Command bar default actions */ private static final String[] DEFAULT_ACTION_IDS = { TerminalAction.Descriptor.ACTION_ID, ViewAction.Descriptor.ACTION_ID, EditAction.Descriptor.ACTION_ID, CopyAction.Descriptor.ACTION_ID, MoveAction.Descriptor.ACTION_ID, MkdirAction.Descriptor.ACTION_ID, DeleteAction.Descriptor.ACTION_ID, RefreshAction.Descriptor.ACTION_ID, CloseWindowAction.Descriptor.ACTION_ID }; /** Command bar default alternate actions */ private static final String[] DEFAULT_ALTERNATE_ACTION_IDS = { TerminalPanelAction.Descriptor.ACTION_ID, ViewAsAction.Descriptor.ACTION_ID, EditAsAction.Descriptor.ACTION_ID, LocalCopyAction.Descriptor.ACTION_ID, RenameAction.Descriptor.ACTION_ID, MkfileAction.Descriptor.ACTION_ID, PermanentDeleteAction.Descriptor.ACTION_ID, null, null }; /** Default modifier key that triggers the display of alternate actions when pressed */ private static final KeyStroke DEFAULT_MODIFIER = KeyStroke.getKeyStroke(KeyEvent.VK_SHIFT, 0); /** Contains all registered command-bar attributes listeners, stored as weak references */ private static final WeakHashMap listeners = new WeakHashMap<>(); /** * This method restore the default command-bar attributes. * The attributes are updated only if they are not already equal to the default attributes. */ static void restoreDefault() { String[] defaultActions = null; String[] alternateActions = null; for (int i = 0; i < DEFAULT_ACTION_IDS.length; i++) { String id1 = DEFAULT_ACTION_IDS[i]; KeyStroke key1 = ActionKeymap.getAccelerator(id1); if (key1 != null && key1.getModifiers() != 0) { String id2 = DEFAULT_ALTERNATE_ACTION_IDS[i]; KeyStroke key2 = ActionKeymap.getAccelerator(id2); if (key2 == null || key2.getModifiers() == 0) { // swap keys if (defaultActions == null) { defaultActions = Arrays.copyOf(DEFAULT_ACTION_IDS, DEFAULT_ACTION_IDS.length); alternateActions = Arrays.copyOf(DEFAULT_ALTERNATE_ACTION_IDS, DEFAULT_ALTERNATE_ACTION_IDS.length); } defaultActions[i] = id2; alternateActions[i] = id1; } } } if (defaultActions == null) { defaultActions = DEFAULT_ACTION_IDS; } if (alternateActions == null) { alternateActions = DEFAULT_ALTERNATE_ACTION_IDS; } setAttributes(defaultActions, alternateActions, DEFAULT_MODIFIER); } /** * @return true if command-bar attributes equal to the default attributes. */ static boolean areDefaultAttributes() { if (actionIds != DEFAULT_ACTION_IDS) { int nbActions = actionIds.length; if (nbActions != DEFAULT_ACTION_IDS.length) return false; for (int i=0; i. */ package com.mucommander.ui.main.commandbar; /** * This is an interface that each class that should listen to CommandBar's attributes modifications need to implement. * * @author Arik Hadas */ public interface CommandBarAttributesListener { /** * This method is invoked when command-bar's actions\alternate actions\modifier have been modified. */ void commandBarAttributeChanged(); } ================================================ FILE: src/main/java/com/mucommander/ui/main/commandbar/CommandBarButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.commandbar; import java.awt.Dimension; import java.awt.Insets; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.button.NonFocusableButton; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import ru.trolsoft.macosx.RetinaImageIcon; /** * Button that located in command-bar. * Button in command-bar are associated with an MuAction and could have an alternative action, * i.e an action that would replace the regular button's action when a modifier key is pressed. * * @author Arik Hadas */ public class CommandBarButton extends NonFocusableButton implements ConfigurationListener { /** ID of the button's action */ private final String actionId; /** Current icon scale factor */ // The math.max(1.0f, ...) part is to workaround a bug which cause(d) this value to be set to 0.0 in the configuration file. static float scaleFactor = Math.max(1.0f, TcConfigurations.getPreferences().getVariable(TcPreference.COMMAND_BAR_ICON_SCALE, TcPreferences.DEFAULT_COMMAND_BAR_ICON_SCALE)); public static CommandBarButton create(String actionId, MainFrame mainFrame) { return actionId == null ? null : new CommandBarButton(actionId, mainFrame); } CommandBarButton(String actionId, MainFrame mainFrame) { // Use new JButton decorations introduced in Mac OS X 10.5 (Leopard) if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { //putClientProperty("JComponent.sizeVariant", "small"); //putClientProperty("JButton.buttonType", "textured"); } else { setMargin(new Insets(3,4,3,4)); } this.actionId = actionId; setButtonAction(actionId, mainFrame); // For Mac OS X whose default minimum width for buttons is enormous setMinimumSize(new Dimension(40, (int) getPreferredSize().getHeight())); // Listen to configuration changes to reload command bar buttons when icon size has changed TcConfigurations.addPreferencesListener(this); } /** * Sets the given button's action, custom label showing the accelerator and icon taking into account the scale factor. */ public void setButtonAction(String actionId, MainFrame mainFrame) { TcAction action = ActionManager.getActionInstance(actionId, mainFrame); setAction(action); // Append the action's shortcut to the button's label String label; label = action.getLabel(); if (action.getAcceleratorText() != null) { label += " [" + action.getAcceleratorText() + ']'; } setText(label); // Scale icon if scale factor is different from 1.0 if (scaleFactor != 1.0f) { setIcon(IconManager.getScaledIcon(action.getIcon(), scaleFactor)); } if (RetinaImageIcon.IS_RETINA && getIcon() instanceof RetinaImageIcon) { setDisabledIcon(((RetinaImageIcon) getIcon()).buildDisabledIcon()); setPressedIcon(getIcon()); } } /** * Return the id of the button's action * * @return id of the button's action */ public String getActionId() { return actionId; } /////////////////////////////////// // ConfigurationListener methods // /////////////////////////////////// /** * Listens to certain configuration variables. */ public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); // Reload butons icon if the icon scale factor has changed if (var.equals(TcPreferences.COMMAND_BAR_ICON_SCALE)) { scaleFactor = event.getFloatValue(); // Change the button's icon but NOT the action's icon which has to remain in its original non-scaled size setIcon(IconManager.getScaledIcon(((TcAction) getAction()).getIcon(), scaleFactor)); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/commandbar/CommandBarButtonForDisplay.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.commandbar; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import java.awt.*; /** * CommandBarButton that used for display purpose only * * @author Arik Hadas */ public class CommandBarButtonForDisplay extends CommandBarButton { /** The preferred size of display button */ public static final Dimension PREFERRED_SIZE = new Dimension(130, 30); public static CommandBarButtonForDisplay create(String actionId) { return actionId == null ? null : new CommandBarButtonForDisplay(actionId); } private CommandBarButtonForDisplay(String actionId) { super(actionId, null); setEnabled(true); setPreferredSize(PREFERRED_SIZE); } @Override public void setButtonAction(String actionId, MainFrame mainFrame) { // Use the action's label as the button's label String label = ActionProperties.getActionLabel(actionId); setText(label); // Set the button's tooltip to the action's tooltip if it has one, // to the action's label otherwise (the label may be too long for being displayed fully) String tooltipText = ActionProperties.getActionTooltip(actionId); setToolTipText(tooltipText==null?label:tooltipText); setIcon(IconManager.getScaledIcon(ActionProperties.getActionIcon(actionId), scaleFactor)); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/commandbar/CommandBarIO.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.commandbar; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.helpers.DefaultHandler; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; /** * This class contains the common things for reading and writing the command-bar actions and modifier. * * @author Arik Hadas */ public abstract class CommandBarIO extends DefaultHandler { private static Logger logger; /* Variables used for XML parsing */ /** Root element */ protected static final String ROOT_ELEMENT = "command_bar"; /** Attribute containing the last muCommander version that was used to create the file */ protected static final String VERSION_ATTRIBUTE = "version"; protected static final String MODIFIER_ATTRIBUTE = "modifier"; /** Element describing one of the button in the list */ protected static final String BUTTON_ELEMENT = "button"; /** Attribute containing the action class associated with the button */ protected static final String ACTION_ATTRIBUTE = "action"; /** Attribute containing the alternative action class associated with the button */ protected static final String ALT_ACTION_ATTRIBUTE = "alt_action"; /** Attribute containing the action id associated with the button */ protected static final String ACTION_ID_ATTRIBUTE = "action_id"; /** Attribute containing the alternative action id associated with the button */ protected static final String ALT_ACTION_ID_ATTRIBUTE = "alt_action_id"; /** Default command bar descriptor filename */ protected final static String DEFAULT_COMMAND_BAR_FILE_NAME = "command_bar.xml"; /** Path to the command bar descriptor resource file within the application JAR file */ protected final static String COMMAND_BAR_RESOURCE_PATH = "/" + DEFAULT_COMMAND_BAR_FILE_NAME; /** Command bar descriptor file used when calling {@link #loadCommandBar()} */ protected static AbstractFile commandBarFile; /** CommandBarWriter instance */ private static CommandBarWriter commandBarWriter; /** Whether the command-bar has been modified and should be saved */ protected static boolean wasCommandBarModified; /** * Parses the XML file describing the command bar's buttons and associated actions. * If the file doesn't exist yet, it is copied from the default resource file within the JAR. * * This method must be called before instantiating CommandBar for the first time. */ public static void loadCommandBar() throws Exception { // Load user's file if exist AbstractFile commandBarFile = getDescriptionFile(); if (commandBarFile != null && commandBarFile.exists()) { CommandBarReader reader = new CommandBarReader(commandBarFile); CommandBarAttributes.setAttributes(reader.getActionsRead(), reader.getAlternateActionsRead(), reader.getModifierRead()); } else { CommandBarAttributes.restoreDefault(); getLogger().debug(DEFAULT_COMMAND_BAR_FILE_NAME + " was not found, using defaults"); } // initialize the writer after setting the command-bar initial attributes: commandBarWriter = CommandBarWriter.create(); } /** * Mark that actions were modified and therefore should be saved. */ public static void setModified() { wasCommandBarModified = true; } /** * Writes the current command bar to the user's command bar file. * @throws IOException * @throws IOException */ public static void saveCommandBar() throws IOException { if (CommandBarAttributes.areDefaultAttributes()) { AbstractFile commandBarFile = getDescriptionFile(); if(commandBarFile != null && commandBarFile.exists()) { getLogger().info("Command bar use default settings, removing descriptor file"); commandBarFile.delete(); } else { getLogger().debug("Command bar not modified, not saving"); } } else if (commandBarWriter != null) { if (wasCommandBarModified) { commandBarWriter.write(); } else { getLogger().debug("Command bar not modified, not saving"); } } else { getLogger().warn("Could not save command bar. writer is null"); } } /** * Sets the path to the command bar description file to be loaded when calling {@link #loadCommandBar()}. * By default, this file is {@link #DEFAULT_COMMAND_BAR_FILE_NAME} within the preferences folder. * @param path path to the command bar descriptor file * @throws FileNotFoundException if the specified file is not accessible. */ public static void setDescriptionFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setDescriptionFile(new File(path)); } else { setDescriptionFile(file); } } /** * Sets the path to the command bar description file to be loaded when calling {@link #loadCommandBar()}. * By default, this file is {@link #DEFAULT_COMMAND_BAR_FILE_NAME} within the preferences folder. * @param file path to the command bar descriptor file * @throws FileNotFoundException if the specified file is not accessible. */ public static void setDescriptionFile(File file) throws FileNotFoundException { setDescriptionFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the command bar description file to be loaded when calling {@link #loadCommandBar()}. * By default, this file is {@link #DEFAULT_COMMAND_BAR_FILE_NAME} within the preferences folder. * @param file path to the command bar descriptor file * @throws FileNotFoundException if the specified file is not accessible. */ public static void setDescriptionFile(AbstractFile file) throws FileNotFoundException { // Makes sure file can be used as a commandbar description file. if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } commandBarFile = file; } public static AbstractFile getDescriptionFile() throws IOException { if (commandBarFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_COMMAND_BAR_FILE_NAME); } return commandBarFile; } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(CommandBarIO.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/commandbar/CommandBarReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.commandbar; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.AbstractFile; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.ui.action.ActionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import javax.swing.KeyStroke; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * This class parses the XML file describing the command bar's buttons and associated actions. * * @author Maxence Bernard, Arik Hadas */ class CommandBarReader extends CommandBarIO { private static Logger logger; /** Temporarily used for XML parsing */ private List actionsIdsV; /** Temporarily used for XML parsing */ private List alternateActionsIdsV; /** Temporarily used for XML parsing */ private KeyStroke modifier; /** Parsed file */ private final AbstractFile file; /** * Starts parsing the XML description file. * * @throws SAXException * @throws IOException * @throws ParserConfigurationException */ CommandBarReader(AbstractFile file) throws SAXException, IOException, ParserConfigurationException { this.file = file; try (InputStream in = new BackupInputStream(file)) { SAXParserFactory.newInstance().newSAXParser().parse(in, this); } } String[] getActionsRead() { int nbActions = actionsIdsV.size(); String[] actionIds = new String[nbActions]; actionsIdsV.toArray(actionIds); return actionIds; } String[] getAlternateActionsRead() { int nbActions = alternateActionsIdsV.size(); String[] alternateActionIds = new String[nbActions]; alternateActionsIdsV.toArray(alternateActionIds); return alternateActionIds; } KeyStroke getModifierRead() { return modifier; } //////////////////////////// // ContentHandler methods // //////////////////////////// @Override public void startDocument() { getLogger().trace(file.getAbsolutePath()+" parsing started"); actionsIdsV = new ArrayList<>(); alternateActionsIdsV = new ArrayList<>(); modifier = null; } @Override public void endDocument() { getLogger().trace(file.getAbsolutePath()+" parsing finished"); } @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { if(qName.equals(BUTTON_ELEMENT)) { // Resolve action id String actionIdAttribute = attributes.getValue(ACTION_ID_ATTRIBUTE); if (actionIdAttribute != null) { if (ActionManager.isActionExist(actionIdAttribute)) { actionsIdsV.add(actionIdAttribute); // Resolve alternate action id (if any) actionIdAttribute = attributes.getValue(ALT_ACTION_ID_ATTRIBUTE); alternateActionsIdsV.add(ActionManager.isActionExist(actionIdAttribute) ? actionIdAttribute : null); } } else { // Resolve action class String actionClassAttribute = attributes.getValue(ACTION_ATTRIBUTE); if (actionClassAttribute != null) { String actionId = ActionManager.extrapolateId(actionClassAttribute); if (ActionManager.isActionExist(actionId)) { actionsIdsV.add(actionId); // Resolve alternate action class (if any) actionClassAttribute = attributes.getValue(ALT_ACTION_ATTRIBUTE); if(actionClassAttribute == null) alternateActionsIdsV.add(null); else { actionId = ActionManager.extrapolateId(actionClassAttribute); if (ActionManager.isActionExist(actionId)) alternateActionsIdsV.add(actionId); else { getLogger().warn("Error in "+DEFAULT_COMMAND_BAR_FILE_NAME+": action id for " + actionClassAttribute + " not found"); alternateActionsIdsV.add(null); } } } else getLogger().warn("Error in "+DEFAULT_COMMAND_BAR_FILE_NAME+": action id for " + actionClassAttribute + " not found"); } } } else if(qName.equals(ROOT_ELEMENT)) { // Retrieve modifier key (shift by default) modifier = KeyStroke.getKeyStroke(attributes.getValue(MODIFIER_ATTRIBUTE)); // Note: early 0.8 beta3 nightly builds did not have version attribute, so the attribute may be null String fileVersion = attributes.getValue(VERSION_ATTRIBUTE); // if the file's version is not up-to-date, update the file to the current version at quitting. if (!RuntimeConstants.VERSION.equals(fileVersion)) setModified(); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(CommandBarReader.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/commandbar/CommandBarWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.commandbar; import java.io.IOException; import java.io.OutputStream; import javax.swing.KeyStroke; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.RuntimeConstants; import com.mucommander.io.backup.BackupOutputStream; import com.mucommander.ui.text.KeyStrokeUtils; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; /** * This class is responsible for writing the command-bar attributes (actions and modifier). * * @author Arik Hadas */ class CommandBarWriter extends CommandBarIO { private static Logger logger; // - Singleton ------------------------------------------------------- // ------------------------------------------------------------------- private static CommandBarWriter instance; public static CommandBarWriter create() { if (instance == null) instance = new CommandBarWriter(); return instance; } private CommandBarWriter() {} void write() { String[] commandBarActionIds = CommandBarAttributes.getActions(); String[] commandBarAlterativeActionIds = CommandBarAttributes.getAlternateActions(); KeyStroke commandBarModifier = CommandBarAttributes.getModifier(); try (BackupOutputStream bos = new BackupOutputStream(getDescriptionFile())) { new Writer(bos).write(commandBarActionIds, commandBarAlterativeActionIds, commandBarModifier); wasCommandBarModified = false; } catch (IOException e) { getLogger().debug("Caught exception", e); } } private static class Writer { private XmlWriter writer; private Writer(OutputStream stream) throws IOException { this.writer = new XmlWriter(stream); } private void write(String[] actionIds, String[] alternativeActionIds, KeyStroke modifier) throws IOException { try { writer.writeCommentLine("See http://trac.mucommander.com/wiki/CommandBar for information on how to customize this file"); XmlAttributes rootElementAttributes = new XmlAttributes(); rootElementAttributes.add(MODIFIER_ATTRIBUTE, KeyStrokeUtils.getKeyStrokeRepresentation(modifier)); rootElementAttributes.add(VERSION_ATTRIBUTE, RuntimeConstants.VERSION); writer.startElement(ROOT_ELEMENT, rootElementAttributes, true); int nbCommandBarActions = actionIds.length; for (int i=0; i initialFolders = new LinkedList<>(); // Initial folders AbstractFile folder; for (String folderPath : folderPaths) { // TODO: consider whether to search for workable path in case the folder doesn't exist if (folderPath != null && (folder = FileFactory.getFile(folderPath)) != null && folder.exists()) initialFolders.add(folder); } // If the initial path is not legal or does not exist, defaults to the user's home. AbstractFile[] results = initialFolders.isEmpty() ? new AbstractFile[]{FileFactory.getFile(System.getProperty("user.home"))} : initialFolders.toArray(new AbstractFile[0]); getLogger().debug("initial folders:"); for (AbstractFile result : results) { getLogger().debug("\t{}", result); } return results; } /** * Retrieves the user's initial path for the specified frame. *

    * If the path found in preferences is either illegal or does not exist, this method will * return the user's home directory - we assume this will always exist, which might be a bit * of a leap of faith. * * @param folderPanelType panel for which the initial path should be returned (either {@link com.mucommander.ui.main.FolderPanel.FolderPanelType#LEFT} or * {@link #@link com.mucommander.ui.main.FolderPanel.FolderPanelType.RIGHT}). * @return the user's initial path for the specified frame. */ FileURL getInitialPath(FolderPanelType folderPanelType) { // Preferences configuration TcPreferencesAPI preferences = TcConfigurations.getPreferences(); // Checks which kind of initial path we're dealing with. boolean isCustom = preferences.getVariable(TcPreference.STARTUP_FOLDERS, TcPreferences.DEFAULT_STARTUP_FOLDERS).equals(TcPreferences.STARTUP_FOLDERS_CUSTOM); String customPath = null; // Handles custom initial paths. if (isCustom) { customPath = (folderPanelType == FolderPanelType.LEFT ? preferences.getVariable(TcPreference.LEFT_CUSTOM_FOLDER) : preferences.getVariable(TcPreference.RIGHT_CUSTOM_FOLDER)); } AbstractFile result; if (customPath == null || (result = FileFactory.getFile(customPath)) == null || !result.exists()) { result = getHomeFolder(); } getLogger().debug("initial folder: {}", result); return result.getURL(); } FileTableConfiguration getFileTableConfiguration(FolderPanelType folderPanelType, int window) { FileTableConfiguration conf = new FileTableConfiguration(); // Loop on columns for (Column c : Column.values()) { if (c != Column.NAME) { // Skip the special name column (always visible, width automatically calculated) // Sets the column's initial visibility. conf.setEnabled(c, TcConfigurations.getSnapshot().getVariable( TcSnapshot.getShowColumnVariable(window, c, folderPanelType == FolderPanelType.LEFT), c.showByDefault() ) ); // Sets the column's initial width. conf.setWidth(c, TcConfigurations.getSnapshot().getIntegerVariable(TcSnapshot.getColumnWidthVariable(window, c, folderPanelType == FolderPanelType.LEFT))); } // Sets the column's initial order conf.setPosition(c, TcConfigurations.getSnapshot().getVariable( TcSnapshot.getColumnPositionVariable(window, c, folderPanelType == FolderPanelType.LEFT), c.ordinal()) ); } return conf; } AbstractFile getHomeFolder() { return FileFactory.getFile(System.getProperty("user.home")); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(MainFrameBuilder.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/MainMenuBar.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.menu; import com.mucommander.bonjour.BonjourMenu; import com.mucommander.bonjour.BonjourService; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionParameters; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.*; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.pref.theme.ThemeEditorDialog; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import com.mucommander.ui.viewer.FileFrame; import com.mucommander.utils.text.Translator; import ru.trolsoft.ui.TCheckBoxMenuItem; import ru.trolsoft.ui.TMenuSeparator; import javax.swing.*; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.*; import java.util.List; /** * This class is the main menu bar. It takes care of displaying menu and menu items and triggering * the proper actions. * *

    Implementation note: for performance reasons, some menu items are created/enabled/disabled when corresponding menus * are selected, instead of monitoring the MainFrame's state and unnecessarily creating/enabling/disabling menu items * when they are not visible. However, this prevents keyboard shortcuts from being managed by the menu bar for those * dynamic items. * * @author Maxence Bernard */ public class MainMenuBar extends JMenuBar implements ActionListener, MenuListener { private final MainFrame mainFrame; // View menu private final JMenu viewMenu; private final JMenu themesMenu; private final JCheckBoxMenuItem[] cbSortByItems = new TCheckBoxMenuItem[Column.values().length]; private final JMenu tableModeMenu; private final JCheckBoxMenuItem[] cbTableModeItems = new TCheckBoxMenuItem[3]; private final JMenu columnsMenu; private final JCheckBoxMenuItem[] cbToggleColumnItems = new TCheckBoxMenuItem[Column.values().length]; private final JCheckBoxMenuItem cbToggleToggleAutoSizeItem; private final JCheckBoxMenuItem cbToggleShowFoldersFirstItem; private final JCheckBoxMenuItem cbToggleFoldersAlwaysAlphabeticalItem; private final JCheckBoxMenuItem cbToggleShowHiddenFilesItem; private final JCheckBoxMenuItem cbToggleTreeItem; private final JCheckBoxMenuItem cbToggleSinglePanel; private final OpenWithMenu openWithMenu; private final OpenAsMenu openAsMenu; /* TODO branch private JCheckBoxMenuItem toggleBranchView; */ // Go menu private final JMenu goMenu; private final int volumeOffset; // Bookmark menu private final JMenu bookmarksMenu; private final int bookmarksOffset; // Index of the first bookmark menu item private JMenu ejectDrivesMenu; // Window menu private final JMenu windowMenu; private final int windowOffset; // Index of the first window menu item private final JCheckBoxMenuItem splitHorizontallyItem; private final JCheckBoxMenuItem splitVerticallyItem; /** Maps window menu items onto weakly-referenced frames */ private WeakHashMap windowMenuFrames; private final static String[] RECALL_WINDOW_ACTION_IDS = { RecallWindow1Action.Descriptor.ACTION_ID, RecallWindow2Action.Descriptor.ACTION_ID, RecallWindow3Action.Descriptor.ACTION_ID, RecallWindow4Action.Descriptor.ACTION_ID, RecallWindow5Action.Descriptor.ACTION_ID, RecallWindow6Action.Descriptor.ACTION_ID, RecallWindow7Action.Descriptor.ACTION_ID, RecallWindow8Action.Descriptor.ACTION_ID, RecallWindow9Action.Descriptor.ACTION_ID, RecallWindow10Action.Descriptor.ACTION_ID }; /** * Creates a new MenuBar for the given MainFrame. */ public MainMenuBar(MainFrame mainFrame) { this.mainFrame = mainFrame; // Disable menu bar (NOT menu item) mnemonics under Mac OS X because of a bug: when screen menu bar is enabled // and a menu is triggered by a mnemonic, the menu pops up where it would appear with a regular menu bar // (i.e. with screen menu bar disabled). MnemonicHelper menuMnemonicHelper = OsFamily.MAC_OS_X.isCurrent() ? null : new MnemonicHelper(); MnemonicHelper menuItemMnemonicHelper = new MnemonicHelper(); MnemonicHelper menuItemMnemonicHelper2 = new MnemonicHelper(); // File menu JMenu fileMenu = MenuToolkit.addMenu(Translator.get("file_menu"), menuMnemonicHelper, this); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(NewWindowAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(AddTabAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); fileMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(OpenAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(OpenNativelyAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); openWithMenu = new OpenWithMenu(mainFrame, null); fileMenu.add(openWithMenu); openAsMenu = new OpenAsMenu(mainFrame); fileMenu.add(openAsMenu); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(OpenInNewTabAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(OpenInOtherPanelAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(OpenInBothPanelsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(RevealInDesktopAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); fileMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(PackAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(UnpackAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(EmailAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(BatchRenameAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(SplitFileAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(CombineFilesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(CreateSymlinkAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); fileMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(ShowFilePropertiesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(CalculateChecksumAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(ChangePermissionsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(ChangeDateAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(ChangeReplicationAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); // Under Mac OS X, 'Preferences' already appears in the application (muCommander) menu, do not display it again if (!OsFamily.MAC_OS_X.isCurrent()) { fileMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(ShowPreferencesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); } fileMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(CloseWindowAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); // Under Mac OS X, 'Quit' already appears in the application (muCommander) menu, do not display it again if (!OsFamily.MAC_OS_X.isCurrent()) { MenuToolkit.addMenuItem(fileMenu, ActionManager.getActionInstance(QuitAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); } add(fileMenu); // Mark menu menuItemMnemonicHelper.clear(); JMenu markMenu = MenuToolkit.addMenu(Translator.get("mark_menu"), menuMnemonicHelper, this); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(MarkSelectedFileAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(MarkGroupAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(UnmarkGroupAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(MarkAllAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(UnmarkAllAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(MarkExtensionAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(MarkEmptyFilesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(InvertSelectionAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); markMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(CopyFilesToClipboardAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(CopyFileNamesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(CopyFileBaseNamesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(CopyFilePathsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(PasteClipboardFilesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); markMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(CompareFoldersAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(markMenu, ActionManager.getActionInstance(CompareFolderFilesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); add(markMenu); // View menu menuItemMnemonicHelper.clear(); viewMenu = MenuToolkit.addMenu(Translator.get("view_menu"), menuMnemonicHelper, this); MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(SwapFoldersAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(SetSameFolderAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); viewMenu.add(new TMenuSeparator()); tableModeMenu = MenuToolkit.addMenu(Translator.get("view_menu.table_mode"), null, this); cbTableModeItems[0] = MenuToolkit.addCheckBoxMenuItem(tableModeMenu, ActionManager.getActionInstance(ToggleTableViewModeFullAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); cbTableModeItems[1] = MenuToolkit.addCheckBoxMenuItem(tableModeMenu, ActionManager.getActionInstance(ToggleTableViewModeCompactAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); cbTableModeItems[2] = MenuToolkit.addCheckBoxMenuItem(tableModeMenu, ActionManager.getActionInstance(ToggleTableViewModeShortAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); ButtonGroup groupViewMode = new ButtonGroup(); for (JCheckBoxMenuItem checkBoxMenuItem : cbTableModeItems) { groupViewMode.add(checkBoxMenuItem); } // tableModeMenu.addMenuListener(new MenuListener() { // @Override // public void menuSelected(MenuEvent e) { // int mode = mainFrame.getActiveTable().getViewMode().ordinal(); // cbTableModeItems[mode].setSelected(true); // } // // @Override // public void menuDeselected(MenuEvent e) { } // // @Override // public void menuCanceled(MenuEvent e) { } // }); tableModeMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(tableModeMenu, ActionManager.getActionInstance(TogglePanelPreviewModeAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); viewMenu.add(tableModeMenu); viewMenu.add(new TMenuSeparator()); cbToggleShowFoldersFirstItem = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleShowFoldersFirstAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); cbToggleFoldersAlwaysAlphabeticalItem = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleFoldersAlwaysAlphabeticalAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); cbToggleShowHiddenFilesItem = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleHiddenFilesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); cbToggleTreeItem = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleTreeAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); cbToggleSinglePanel = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleSinglePanelAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); /* TODO branch toggleBranchView = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleBranchViewAction.class, mainFrame), menuItemMnemonicHelper); */ MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(ShowFoldersSizeAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); viewMenu.add(new TMenuSeparator()); ButtonGroup buttonGroup = new ButtonGroup(); for (Column c : Column.values()) { buttonGroup.add(cbSortByItems[c.ordinal()] = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(c.getSortByColumnActionId(), mainFrame), menuItemMnemonicHelper)); } MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(ReverseSortOrderAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); viewMenu.add(new TMenuSeparator()); // Toggle columns submenu columnsMenu = MenuToolkit.addMenu(Translator.get("view_menu.show_hide_columns"), null, this); menuItemMnemonicHelper2.clear(); for (Column c : Column.values()) { if (c == Column.NAME) { continue; } cbToggleColumnItems[c.ordinal()] = MenuToolkit.addCheckBoxMenuItem(columnsMenu, ActionManager.getActionInstance(c.getToggleColumnActionId(), mainFrame), menuItemMnemonicHelper2); } viewMenu.add(columnsMenu); cbToggleToggleAutoSizeItem = MenuToolkit.addCheckBoxMenuItem(viewMenu, ActionManager.getActionInstance(ToggleAutoSizeAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); viewMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(ToggleToolBarAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(ToggleStatusBarAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(ToggleCommandBarAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(viewMenu, ActionManager.getActionInstance(CustomizeCommandBarAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); add(viewMenu); // Go menu menuItemMnemonicHelper.clear(); goMenu = MenuToolkit.addMenu(Translator.get("go_menu"), menuMnemonicHelper, this); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(GoBackAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(GoForwardAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); goMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(GoToParentAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(GoToParentInOtherPanelAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(GoToParentInBothPanelsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(GoToRootAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); goMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(ChangeLocationAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(ConnectToServerAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(goMenu, ActionManager.getActionInstance(ShowServerConnectionsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); // Quick lists goMenu.add(new TMenuSeparator()); JMenu quickListMenu = MenuToolkit.addMenu(Translator.get("quick_lists_menu"), menuMnemonicHelper, this); menuItemMnemonicHelper2.clear(); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowParentFoldersQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowRecentLocationsQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowRecentExecutedFilesQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowBookmarksQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowRootFoldersQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowTabsQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowRecentViewedFilesQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowRecentEditedFilesQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); MenuToolkit.addMenuItem(quickListMenu, ActionManager.getActionInstance(ShowEditorBookmarksQLAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper2); goMenu.add(quickListMenu); // Add Bonjour services menu goMenu.add(new TMenuSeparator()); BonjourMenu bonjourMenu = new BonjourMenu() { @Override public TcAction getMenuItemAction(BonjourService bs) { return new OpenLocationAction(MainMenuBar.this.mainFrame, new HashMap<>(), bs); } }; char mnemonic = menuItemMnemonicHelper.getMnemonic(bonjourMenu.getName()); if (mnemonic != 0) { bonjourMenu.setMnemonic(mnemonic); } bonjourMenu.setIcon(null); goMenu.add(bonjourMenu); // Volumes will be added when the menu is selected goMenu.add(new TMenuSeparator()); volumeOffset = goMenu.getItemCount(); add(goMenu); // Tools menu menuItemMnemonicHelper.clear(); JMenu toolsMenu = MenuToolkit.addMenu(Translator.get("tools_menu"), menuMnemonicHelper, this); MenuToolkit.addMenuItem(toolsMenu, ActionManager.getActionInstance(UserMenuAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(toolsMenu, ActionManager.getActionInstance(FindFileAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(toolsMenu, ActionManager.getActionInstance(CalculatorAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(toolsMenu, ActionManager.getActionInstance(RunCommandAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); if (OsFamily.MAC_OS_X.isCurrent()) { ejectDrivesMenu = MenuToolkit.addMenu(Translator.get("eject_menu"), menuMnemonicHelper, this); toolsMenu.add(ejectDrivesMenu); } if (CompareFilesAction.supported()) { MenuToolkit.addMenuItem(toolsMenu, ActionManager.getActionInstance(CompareFilesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); } toolsMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(toolsMenu, ActionManager.getActionInstance(EditCommandsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); add(toolsMenu); //toolsOffset = toolsMenu.getItemCount(); // Bookmark menu, menu items will be added when the menu gets selected menuItemMnemonicHelper.clear(); bookmarksMenu = MenuToolkit.addMenu(Translator.get("bookmarks_menu"), menuMnemonicHelper, this); //bookmarksMenu = MenuToolkit.addScrollableMenu(Translator.get("bookmarks_menu"), menuMnemonicHelper, this); MenuToolkit.addMenuItem(bookmarksMenu, ActionManager.getActionInstance(AddBookmarkAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(bookmarksMenu, ActionManager.getActionInstance(EditBookmarksAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(bookmarksMenu, ActionManager.getActionInstance(ExploreBookmarksAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); bookmarksMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(bookmarksMenu, ActionManager.getActionInstance(EditCredentialsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); bookmarksMenu.add(new TMenuSeparator()); // Save the first bookmark menu item's offset for later (bookmarks will be added when menu becomes visible) this.bookmarksOffset = bookmarksMenu.getItemCount(); add(bookmarksMenu); // Window menu menuItemMnemonicHelper.clear(); windowMenu = MenuToolkit.addMenu(Translator.get("window_menu"), menuMnemonicHelper, this); // If running Mac OS X, add 'Minimize' and 'Zoom' items if (OsFamily.MAC_OS_X.isCurrent()) { MenuToolkit.addMenuItem(windowMenu, ActionManager.getActionInstance(MinimizeWindowAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(windowMenu, ActionManager.getActionInstance(MaximizeWindowAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); windowMenu.add(new TMenuSeparator()); } MenuToolkit.addMenuItem(windowMenu, ActionManager.getActionInstance(SplitEquallyAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); buttonGroup = new ButtonGroup(); buttonGroup.add(splitVerticallyItem = MenuToolkit.addCheckBoxMenuItem(windowMenu, ActionManager.getActionInstance(SplitVerticallyAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper)); buttonGroup.add(splitHorizontallyItem = MenuToolkit.addCheckBoxMenuItem(windowMenu, ActionManager.getActionInstance(SplitHorizontallyAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper)); windowMenu.add(new TMenuSeparator()); themesMenu = MenuToolkit.addMenu(Translator.get("prefs_dialog.themes"), null, this); // Theme menu items will be added when the themes menu is selected windowMenu.add(themesMenu); windowMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(windowMenu, ActionManager.getActionInstance(RecallPreviousWindowAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(windowMenu, ActionManager.getActionInstance(RecallNextWindowAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(windowMenu, ActionManager.getActionInstance(BringAllToFrontAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); // All other window menu items will be added when the menu gets selected windowMenu.add(new TMenuSeparator()); // Save the first window menu item's offset for later this.windowOffset = windowMenu.getItemCount(); add(windowMenu); // Help menu menuItemMnemonicHelper.clear(); JMenu helpMenu = MenuToolkit.addMenu(Translator.get("help_menu"), menuMnemonicHelper, null); MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(GoToDocumentationAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(ShowKeyboardShortcutsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(ShowDebugConsoleAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); // Links to website, only shows for OS/Window manager that can launch the default browser to open URLs if (DesktopManager.canBrowse()) { helpMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(GoToWebsiteAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); //MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(GoToForumsAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); //MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(ReportBugAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); //MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(DonateAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); helpMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(CheckForUpdatesAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); } // Under Mac OS X, 'About' already appears in the application (muCommander) menu, do not display it again if (!OsFamily.MAC_OS_X.isCurrent()) { helpMenu.add(new TMenuSeparator()); MenuToolkit.addMenuItem(helpMenu, ActionManager.getActionInstance(ShowAboutAction.Descriptor.ACTION_ID, mainFrame), menuItemMnemonicHelper); } add(helpMenu); } @Override public void actionPerformed(ActionEvent e) { // Discard action events while in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } // Bring the frame corresponding to the clicked menu item to the front JMenuItem source = (JMenuItem) e.getSource(); windowMenuFrames.get(source).toFront(); } @Override public void menuSelected(MenuEvent e) { Object source = e.getSource(); if (source == viewMenu) { updateViewMenu(); } else if (source == columnsMenu) { updateShowHideColumnsMenu(); } else if (source == goMenu) { updateGoMenu(); } else if (source == ejectDrivesMenu) { updateEjectDriveMenu(); } else if (source == bookmarksMenu) { updateBookmarksMenu(); } else if (source == windowMenu) { updateWindowsMenu(); } else if (source == themesMenu) { updateThemesMenu(); } else if (source == tableModeMenu) { int mode = mainFrame.getActiveTable().getViewMode().ordinal(); cbTableModeItems[mode].setSelected(true); } else if (source == openWithMenu) { openWithMenu.populate(mainFrame.getActiveTable().getSelectedFile()); } else if (source == openAsMenu) { final AbstractFile selectedFile = mainFrame.getActiveTable().getSelectedFile(); openAsMenu.setEnabled(selectedFile != null && !selectedFile.isDirectory()); } } private void updateViewMenu() { FileTable activeTable = mainFrame.getActiveTable(); // Select the 'sort by' criterion currently in use in the active table cbSortByItems[activeTable.getSortInfo().getCriterion().ordinal()].setSelected(true); boolean foldersFirst = activeTable.getSortInfo().getFoldersFirst(); cbToggleShowFoldersFirstItem.setSelected(foldersFirst); cbToggleFoldersAlwaysAlphabeticalItem.setEnabled(foldersFirst); cbToggleFoldersAlwaysAlphabeticalItem.setSelected(foldersFirst && activeTable.getSortInfo().getFoldersAlwaysAlphabetical()); cbToggleShowHiddenFilesItem.setSelected(TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_HIDDEN_FILES, TcPreferences.DEFAULT_SHOW_HIDDEN_FILES)); cbToggleTreeItem.setSelected(activeTable.getFolderPanel().isTreeVisible()); cbToggleToggleAutoSizeItem.setSelected(mainFrame.isAutoSizeColumnsEnabled()); cbToggleSinglePanel.setSelected(mainFrame.isSinglePanel()); // TODO branch toggleBranchView.setSelected(activeTable.getFolderPanel().isBranchView()); } private void updateShowHideColumnsMenu() { // Update the selected and enabled state of each column menu item. FileTable activeTable = mainFrame.getActiveTable(); for (Column c : Column.values()) { if (c == Column.NAME) { // Name column doesn't have a menu item as it cannot be disabled continue; } JCheckBoxMenuItem item = cbToggleColumnItems[c.ordinal()]; item.setSelected(activeTable.isColumnEnabled(c)); item.setEnabled(activeTable.isColumnDisplayable(c)); // Override the action's label to a shorter one item.setText(c.getLabel()); } } private void updateGoMenu() { // Remove any previous volumes from the Go menu // as they might have changed since menu was last selected for (int i = goMenu.getItemCount(); i > volumeOffset; i--) { goMenu.remove(volumeOffset); } AbstractFile[] volumes = LocalFile.getVolumes(); for (AbstractFile volume : volumes) { goMenu.add(new OpenLocationAction(mainFrame, new Hashtable<>(), volume)); } } private void updateEjectDriveMenu() { // Remove any previous drives menu items from menu // as there might have changed since menu was last selected ejectDrivesMenu.removeAll(); AbstractFile[] volumes = LocalFile.getVolumes(); boolean empty = true; for (AbstractFile volume : volumes) { if (volume != null && !volume.isSymlink() && !volume.getPath().toLowerCase().startsWith("/users/")) { MenuToolkit.addMenuItem(ejectDrivesMenu, volume.getName(), null, null, event -> { EjectDriveAction.eject(mainFrame, volume); mainFrame.tryRefreshCurrentFolders(); }); empty = false; } } if (empty) { JMenuItem menuItem = new JMenuItem(Translator.get("eject.no_mounted_devices")); menuItem.setEnabled(false); ejectDrivesMenu.add(menuItem); } } private void updateBookmarksMenu() { // Remove any previous bookmarks menu items from menu // as bookmarks might have changed since menu was last selected for (int i = bookmarksMenu.getItemCount(); i > bookmarksOffset; i--) { bookmarksMenu.remove(bookmarksOffset); } // Add bookmarks menu items List bookmarks = BookmarkManager.getBookmarks(); if (!bookmarks.isEmpty()) { addBookmarksForGroup(bookmarksMenu, bookmarks, null); } else { // Show 'No bookmark' as a disabled menu item instead showing nothing JMenuItem noBookmarkItem = MenuToolkit.addMenuItem(bookmarksMenu, Translator.get("bookmarks_menu.no_bookmark"), null, null, null); noBookmarkItem.setEnabled(false); } } private void addBookmarksForGroup(JMenu menu, List bookmarks, String parent) { for (Bookmark bookmark : bookmarks) { if ((bookmark.getParent() == null && parent == null) || (parent != null && parent.equals(bookmark.getParent()))) { if (bookmark.getLocation().isEmpty() && !bookmark.getName().equals(BookmarkManager.BOOKMARKS_SEPARATOR)) { JMenu groupMenu = MenuToolkit.addMenu(bookmark.getName(), null, null); menu.add(groupMenu); addBookmarksForGroup(groupMenu, bookmarks, bookmark.getName()); } else { MenuToolkit.addMenuItem(menu, new OpenLocationAction(mainFrame, new HashMap<>(), bookmark), null); } } } } private void updateWindowsMenu() { // Select the split orientation currently in use if (mainFrame.getSplitPaneOrientation()) { splitVerticallyItem.setSelected(true); } else { splitHorizontallyItem.setSelected(true); } // Removing any window menu item previously added // Note: menu item cannot be removed by menuDeselected() as actionPerformed() will be called after // menu has been deselected. for (int i = windowMenu.getItemCount(); i > windowOffset; i--) { windowMenu.remove(windowOffset); } // This WeakHashMap maps menu items to frame instances. It has to be a weakly referenced hash map // and not a regular hash map, since it will not (and cannot) be emptied when the menu has been deselected // and we really do not want this hash map to prevent the frames to be GCed windowMenuFrames = new WeakHashMap<>(); // create a menu item for each of the MainFrame instances, that displays the MainFrame's path // and a keyboard accelerator to recall the frame (for the first 10 frames only). List mainFrames = WindowManager.getMainFrames(); int nbFrames = mainFrames.size(); for (int i = 0; i < nbFrames; i++) { MainFrame mainFrame = mainFrames.get(i); JCheckBoxMenuItem checkBoxMenuItem = new TCheckBoxMenuItem(); // If frame number is less than 10, use the corresponding action class (accelerator will be displayed in the menu item) TcAction recallWindowAction; if (i < 10) { recallWindowAction = ActionManager.getActionInstance(RECALL_WINDOW_ACTION_IDS[i], this.mainFrame); } else { // Else use the generic RecallWindowAction Map actionProps = new HashMap<>(); // Specify the window number using the dedicated property actionProps.put(RecallWindowAction.WINDOW_NUMBER_PROPERTY_KEY, ""+(i+1)); recallWindowAction = ActionManager.getActionInstance(new ActionParameters(RecallWindowAction.Descriptor.ACTION_ID, actionProps), this.mainFrame); } checkBoxMenuItem.setAction(recallWindowAction); // Replace the action's label and use the MainFrame's current folder path instead checkBoxMenuItem.setText((i+1)+" "+mainFrame.getActiveTable().getFolderPanel().getCurrentFolder().getAbsolutePath()); // Use the action's label as a tooltip checkBoxMenuItem.setToolTipText(recallWindowAction.getLabel()); // Check current MainFrame (the one this menu bar belongs to) checkBoxMenuItem.setSelected(mainFrame == this.mainFrame); windowMenu.add(checkBoxMenuItem); } // Add 'other' (non-MainFrame) windows : viewer and editor frames, no associated accelerator Frame[] frames = Frame.getFrames(); nbFrames = frames.length; boolean firstFrame = true; for (int i = 0; i < nbFrames; i++) { Frame frame = frames[i]; // Test if Frame is not hidden (disposed), Frame.getFrames() returns both active and disposed frames if (frame.isShowing() && (frame instanceof FileFrame)) { // Add a separator before the first non-MainFrame frame to mark a separation between MainFrames // and other frames if (firstFrame) { windowMenu.add(new TMenuSeparator()); firstFrame = false; } // Use frame's window title JMenuItem menuItem = new JMenuItem(frame.getTitle()); menuItem.addActionListener(this); windowMenu.add(menuItem); windowMenuFrames.put(menuItem, frame); } } } private void updateThemesMenu() { // Remove all previous theme items, create new ones for each available theme and select the current theme themesMenu.removeAll(); ButtonGroup buttonGroup = new ButtonGroup(); Iterator themes = ThemeManager.availableThemes(); themesMenu.add(new JMenuItem(new EditCurrentThemeAction())); themesMenu.add(new TMenuSeparator()); while (themes.hasNext()) { Theme theme = themes.next(); JCheckBoxMenuItem item = new TCheckBoxMenuItem(new ChangeCurrentThemeAction(theme)); buttonGroup.add(item); if (ThemeManager.isCurrentTheme(theme)) { item.setSelected(true); } themesMenu.add(item); } } public void menuDeselected(MenuEvent e) { } public void menuCanceled(MenuEvent e) { } /** * Action that changes the current theme to the specified in the constructor. */ private class ChangeCurrentThemeAction extends AbstractAction { private final Theme theme; ChangeCurrentThemeAction(Theme theme) { super(theme.getName()); this.theme = theme; } public void actionPerformed(ActionEvent actionEvent) { try { ThemeManager.setCurrentTheme(theme); } catch(IllegalArgumentException e) { InformationDialog.showErrorDialog(mainFrame.getJFrame(), Translator.get("theme_could_not_be_loaded")); } } } /** * Actions that edits the current theme. */ private class EditCurrentThemeAction extends AbstractAction { EditCurrentThemeAction() { super(Translator.get("prefs_dialog.edit_current_theme")); } public void actionPerformed(ActionEvent actionEvent) { new ThemeEditorDialog(mainFrame.getJFrame(), ThemeManager.getCurrentTheme()).editTheme(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/OpenAsMenu.java ================================================ package com.mucommander.ui.main.menu; import com.mucommander.commons.file.ArchiveFormatProvider; import com.mucommander.commons.file.FileFactory; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.ActionParameters; import com.mucommander.ui.action.impl.OpenAsAction; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.main.MainFrame; import com.mucommander.utils.text.Translator; import javax.swing.*; import java.util.*; public class OpenAsMenu extends JMenu { private final static List EXTENSIONS = new ArrayList<>(); private final MainFrame mainFrame; static { for (Iterator it = FileFactory.archiveFormats(); it.hasNext(); ) { ArchiveFormatProvider provider = it.next(); EXTENSIONS.addAll(Arrays.asList(provider.getFileExtensions())); } Collections.sort(EXTENSIONS); } OpenAsMenu(MainFrame mainFrame) { super(Translator.get("file_menu.open_as") + "..."); this.mainFrame = mainFrame; populate(); } /** * Refreshes the content of the menu. */ private void populate() { for (String extension : EXTENSIONS) { Map params = Collections.singletonMap("extension", extension); Action action = ActionManager.getActionInstance(new ActionParameters(OpenAsAction.Descriptor.ACTION_ID, params), mainFrame); action.putValue(Action.NAME, extension.substring(1)); add(action); } } @Override public final JMenuItem add(Action a) { JMenuItem item = super.add(a); MenuToolkit.configureActionMenuItem(item); return item; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/OpenWithMenu.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.menu; import javax.swing.Action; import javax.swing.JMenu; import javax.swing.JMenuItem; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.command.CommandType; import com.mucommander.commons.file.AbstractFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.main.MainFrame; /** * Open with menu. *

    * Note that this class doesn't yet monitor modifications to the command list. * * @author Nicolas Rinaudo */ public class OpenWithMenu extends JMenu { private final MainFrame mainFrame; /** * Creates a new Open With menu. */ OpenWithMenu(MainFrame frame, AbstractFile clickedFile) { super(Translator.get("file_menu.open_with") + "..."); this.mainFrame = frame; populate(clickedFile); } /** * Refreshes the content of the menu. */ public void populate(AbstractFile clickedFile) { for (Command command : CommandManager.commands()) { if (command.getType() != CommandType.NORMAL_COMMAND) { continue; } if (clickedFile != null && !CommandManager.checkFileMask(command, clickedFile)) { continue; } add(ActionManager.getActionInstance(command, mainFrame)); } if (getItemCount() == 0) { setEnabled(false); } } @Override public final JMenuItem add(Action a) { JMenuItem item = super.add(a); MenuToolkit.configureActionMenuItem(item); return item; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/TablePopupMenu.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.menu; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.MountedDriveFilter; import com.mucommander.commons.file.util.FileSet; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.impl.CompareFilesAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.popup.TcActionsPopupMenu; import com.mucommander.utils.text.Translator; /** * Contextual popup menu invoked by FileTable when right-clicking on a file or a group of files. *

    * The following items are displayed (see constructor code for conditions) : *

    * Open * Open in new tab * Open natively * Open with... * Open as... * Rename * Reveal in Finder * ---- * Copy name(s) * Copy path(s) * ---- * Mark all * Unmark all * Mark / Unmark * ---- * Delete * ---- * Properties * Change permission... * Change date... * ---- * Eject [Mac OS only] * Compare files [Mac OS only] * * @author Maxence Bernard, Nicolas Rinaudo */ public class TablePopupMenu extends TcActionsPopupMenu { /** * Creates a new TablePopupMenu. * * @param mainFrame parent MainFrame instance * @param currentFolder current folder in table * @param clickedFile right-clicked file, can be null if user clicked on the folder table background * @param parentFolderClicked true if user right-clicked on the parent '..' folder * @param markedFiles list of marked files, can be empty but never null */ public TablePopupMenu(MainFrame mainFrame, AbstractFile currentFolder, AbstractFile clickedFile, boolean parentFolderClicked, FileSet markedFiles) { super(mainFrame); // 'Open ...' actions displayed if a single file was clicked addOpenActions(mainFrame, clickedFile, parentFolderClicked); // 'Reveal in desktop' displayed only if clicked file is a local file and the OS is capable of doing this addRevealInDesktopAction(currentFolder); addSeparator(); // 'Copy name(s)' and 'Copy path(s)' are displayed only if a single file was clicked or files are marked addCopyActions(clickedFile, markedFiles); // Those following items are displayed in all cases addMarkActions(); addSeparator(); // 'Rename' displayed if a single file was clicked if (clickedFile != null) { addAction(com.mucommander.ui.action.impl.RenameAction.Descriptor.ACTION_ID); } addAction(com.mucommander.ui.action.impl.DeleteAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.CreateSymlinkAction.Descriptor.ACTION_ID); if (clickedFile != null && clickedFile.isSymlink()) { addAction(com.mucommander.ui.action.impl.LocateSymlinkAction.Descriptor.ACTION_ID); } addSeparator(); addAction(com.mucommander.ui.action.impl.ShowFilePropertiesAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.ChangePermissionsAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.ChangeDateAction.Descriptor.ACTION_ID); if (new MountedDriveFilter().accept(clickedFile)) { addSeparator(); addAction(com.mucommander.ui.action.impl.EjectDriveAction.Descriptor.ACTION_ID); } addCompareSelectedFilesAction(markedFiles); } private void addMarkActions() { addAction(com.mucommander.ui.action.impl.MarkAllAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.UnmarkAllAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.MarkSelectedFileAction.Descriptor.ACTION_ID); } private void addCopyActions(AbstractFile clickedFile, FileSet markedFiles) { if (clickedFile != null || !markedFiles.isEmpty()) { addAction(com.mucommander.ui.action.impl.CopyFilesToClipboardAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.CopyFileNamesAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.CopyFileBaseNamesAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.CopyFilePathsAction.Descriptor.ACTION_ID); addSeparator(); } } private void addRevealInDesktopAction(AbstractFile currentFolder) { if (DesktopManager.canOpenInFileManager(currentFolder)) { addAction(com.mucommander.ui.action.impl.RevealInDesktopAction.Descriptor.ACTION_ID); } } private void addOpenActions(MainFrame mainFrame, AbstractFile clickedFile, boolean parentFolderClicked) { if (clickedFile != null || parentFolderClicked) { addAction(com.mucommander.ui.action.impl.OpenAction.Descriptor.ACTION_ID); addAction(com.mucommander.ui.action.impl.OpenNativelyAction.Descriptor.ACTION_ID); add(new OpenWithMenu(mainFrame, clickedFile)); if (clickedFile != null && !clickedFile.isDirectory()) { add(new OpenAsMenu(mainFrame)); } addAction(com.mucommander.ui.action.impl.OpenInNewTabAction.Descriptor.ACTION_ID); } } private void addCompareSelectedFilesAction(FileSet markedFiles) { if (markedFiles.size() != 2 || !CompareFilesAction.supported()) { return; } AbstractFile f1 = markedFiles.get(0); AbstractFile f2 = markedFiles.get(1); if (f1.isDirectory() || f2.isDirectory() || !f1.isLocalFile() || !f2.isLocalFile()) { return; } add(Translator.get("CompareFiles.label")).addActionListener( e -> CompareFilesAction.compareTwoFiles(f1.getAbsolutePath(), f2.getAbsolutePath()) ); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/UserPopupMenu.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.menu; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.macos.OSXTerminal; import com.mucommander.process.ExecutorUtils; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.usermenu.UserMenuItem; import com.mucommander.ui.viewer.EditorRegistrar; import com.mucommander.utils.text.Translator; import lombok.extern.slf4j.Slf4j; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j public class UserPopupMenu extends JPopupMenu implements ActionListener, PopupMenuListener { private final MainFrame mainFrame; private final AbstractFile menuFile; private final Map propertiesMap = new HashMap<>(); private JMenuItem firstItem; public UserPopupMenu(MainFrame mainFrame, AbstractFile file) { this.mainFrame = mainFrame; this.menuFile = file; addPopupMenuListener(this); } public JMenuItem add(JMenu parent, String name, UserMenuItem properties) { JMenuItem item = new JMenuItem(name); if (parent == null) { add(item); } else { parent.add(item); } propertiesMap.put(item, properties); item.addActionListener(this); if (firstItem == null) { firstItem = item; } return item; } private void selectFirstItem() { if (firstItem != null) { SwingUtilities.invokeLater(() -> MenuSelectionManager.defaultManager().setSelectedPath( new MenuElement[] {this, firstItem}) ); } } @Override public void actionPerformed(ActionEvent e) { Object menuItem = e.getSource(); if (menuItem instanceof JMenuItem) { performCommand(propertiesMap.get(menuItem)); } } private void performCommand(UserMenuItem properties) { if (properties.command == null) { mainFrame.getStatusBar().setStatusInfo(Translator.get("UserMenu.command_not_defined")); return; } switch (properties.console) { case NONE: executeInBackground(properties); break; case SHOW: case HIDE: executeInTerminal(properties); break; case APPEND: executeInNewTerminalTabs(properties); break; } } private void executeInNewTerminalTabs(UserMenuItem properties) { if (properties.command.isSingle()) { OSXTerminal.addNewTabWithCommands(menuFile.getParent(), properties.command.singleCommand); } else { for (List group : properties.command.commandsList) { String[] list = new String[group.size()]; String[] commands = group.toArray(list); executeInNewTerminalTabs(menuFile.getParent(), commands); } } } private static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException ignore) { } } private void executeInBackground(UserMenuItem properties) { final AbstractFile folder = mainFrame.getActivePanel().getCurrentFolder(); UserMenuItem.Command cmd = properties.command; if (cmd.isSingle()) { new Thread(() -> { try { ExecutorUtils.execute(cmd.singleCommand, folder); } catch (IOException | InterruptedException e) { log.error("Single command error", e); } }).start(); } else { for (List group : cmd.commandsList) { new Thread(() -> { try { for (String command : group) { ExecutorUtils.execute(command, folder); } } catch (IOException | InterruptedException e) { log.error("Multiple commands error", e); } }).start(); } } } private void executeInTerminal(UserMenuItem properties) { AbstractFile home = menuFile.getParent(); if (properties.command.isSingle()) { executeInNewTerminalWindow(home, properties.command.singleCommand); } else { for (List group : properties.command.commandsList) { String[] list = new String[group.size()]; String[] commands = group.toArray(list); executeInNewTerminalWindow(home, commands); } } } private static String getDefaultTerminalCommand() { return switch (OsFamily.getCurrent()) { case WINDOWS -> "cmd /c start cmd.exe /K \"cd /d $p\""; case LINUX -> ""; case MAC_OS_X -> "open -a Terminal ."; default -> ""; }; } private static void executeInNewTerminalTabs(AbstractFile home, String[] commands) { if (OsFamily.MAC_OS_X.isCurrent()) { OSXTerminal.addNewTabWithCommands(home, commands); } else { // TODO log.error("Operation not supported for {}", OsFamily.getCurrent()); } sleep(200); } private static void executeInNewTerminalWindow(AbstractFile home, String... commands) { if (OsFamily.MAC_OS_X.isCurrent()) { OSXTerminal.openNewWindowAndRun(home, commands); } else { // TODO log.error("Operation not supported for {}", OsFamily.getCurrent()); } sleep(200); } // private static String getConsoleCommand(AbstractFile folder) { // String cmd = getTerminalCommand(); // return cmd.replace("$p", folder.getAbsolutePath()); // } // // private static String getTerminalCommand() { // switch (getTerminalType()) { // case TERMINAL_DEFAULT: // return getDefaultTerminalCommand(); // case TERMINAL_CUSTOM: // return getCustomExternalTerminal(); // case TERMINAL_ITERM: // return "open -a iTerm ." // } // } private static String getCustomExternalTerminal() { return TcConfigurations.getPreferences().getVariable(TcPreference.CUSTOM_EXTERNAL_TERMINAL); } private static int getTerminalType() { return TcConfigurations.getPreferences().getVariable(TcPreference.EXTERNAL_TERMINAL_TYPE, TcPreferences.DEFAULT_TERMINAL_TYPE); } @Override public void processKeyEvent(KeyEvent e, MenuElement[] path, MenuSelectionManager manager) { if (e.getKeyCode() == KeyEvent.VK_F4 && e.getModifiersEx() == 0 && e.getID() == KeyEvent.KEY_PRESSED) { openEditor(); e.consume(); return; } super.processKeyEvent(e, path, manager); } public void show(Component invoker) { Dimension size = getPreferredSize(); int x = (invoker.getWidth() - size.width)/2; int y = (invoker.getHeight() - size.height)/2; show(invoker, x, y); selectFirstItem(); requestFocus(); } private void openEditor() { EditorRegistrar.createEditorFrame(mainFrame, menuFile, ActionProperties.getActionIcon(EditAction.Descriptor.ACTION_ID).getImage()); } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { mainFrame.getStatusBar().setStatusInfo(Translator.get("UserMenu.press_f4_to_edit_menu")); } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { mainFrame.getStatusBar().activePanelChanged(mainFrame.getActivePanel()); } @Override public void popupMenuCanceled(PopupMenuEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/package.html ================================================ Various menus used by muCommander. ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/usermenu/LoadUserMenuException.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.menu.usermenu; import lombok.Getter; public class LoadUserMenuException extends Exception { @Getter private final int line; @Getter private final int column; public LoadUserMenuException(String message) { super(message); line = parseLine(message); column = parseColumn(message); } private static int parseLine(String message) { return parseInt(message, ", line "); } private static int parseColumn(String message) { return parseInt(message, ", column "); } private static int parseInt(String message, String attrName) { int indexStart = message.indexOf(attrName); if (indexStart < 0) { return -1; } indexStart += attrName.length(); int indexEnd1 = message.indexOf(',', indexStart); int indexEnd2 = message.indexOf(':', indexStart); int indexEnd = indexEnd1 > 0 && indexEnd1 < indexEnd2 ? indexEnd1 : indexEnd2; if (indexEnd < 0) { indexEnd = message.length() - 1; } try { return Integer.parseInt(message.substring(indexStart, indexEnd)); } catch (Exception e) { return -1; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/usermenu/UserMenuItem.java ================================================ package com.mucommander.ui.main.menu.usermenu; import java.util.List; public class UserMenuItem { public enum ConsoleType { SHOW, NONE, HIDE, APPEND; public static ConsoleType fromStr(String s) { for (ConsoleType c : values()) { if (c.name().equalsIgnoreCase(s)) { return c; } } return HIDE; } } public static class Command { public final List> commandsList; public final String singleCommand; public Command(String singleCommand) { this.singleCommand = singleCommand; this.commandsList = null; } public Command(List> commandsList) { this.singleCommand = null; this.commandsList = commandsList; } public boolean isSingle() { return singleCommand != null; } } public final Command command; public final ConsoleType console; UserMenuItem(Command command, ConsoleType console) { this.command = command; this.console = console; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/menu/usermenu/UserPopupMenuLoader.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.menu.usermenu; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.UserPopupMenu; import org.jetbrains.annotations.Nullable; import org.yaml.snakeyaml.Yaml; import ru.trolsoft.ui.TMenuSeparator; import javax.swing.*; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; public class UserPopupMenuLoader { public static UserPopupMenu loadMenu(MainFrame mainFrame, AbstractFile file) throws IOException, LoadUserMenuException { try (InputStream is = file.getInputStream()) { Yaml yaml = new Yaml(); Map root = yaml.load(is); if (root == null || !root.containsKey("menu")) { throw new LoadUserMenuException("Invalid YAML structure: 'menu' key not found"); } @SuppressWarnings("unchecked") List items = (List) root.get("menu"); MnemonicHelper mnemonicHelper = new MnemonicHelper(); UserPopupMenu menu = new UserPopupMenu(mainFrame, file); loadMenu(menu, null, items, mnemonicHelper); return menu; } catch (LoadUserMenuException e) { throw e; } catch (Exception e) { throw new LoadUserMenuException(e.getMessage()); } } private static void loadMenu(UserPopupMenu menu, JMenu parent, List items, MnemonicHelper mnemonicHelper) throws LoadUserMenuException { if (items == null) { return; } for (Object obj : items) { if (obj instanceof String objs && objs.equalsIgnoreCase("separator")) { menu.add(new TMenuSeparator()); } else if (obj instanceof Map) { @SuppressWarnings("unchecked") Map item = (Map) obj; String name = getItemProp(item, "name"); Object subItems = item.get("items"); String key = getItemProp(item, "key"); if (name != null && subItems != null) { // Submenu JMenu submenu = new JMenu(name); if (key != null && !key.isEmpty()) { submenu.setMnemonic(KeyStroke.getKeyStroke(key).getKeyCode()); } else { submenu.setMnemonic(mnemonicHelper.getMnemonic(name)); } if (parent == null) { menu.add(submenu); } else { parent.add(submenu); } @SuppressWarnings("unchecked") List subItemsList = (List) subItems; loadMenu(menu, submenu, subItemsList, new MnemonicHelper()); } else if (name != null) { UserMenuItem.Command command = getItemCommand(item); String console = getItemProp(item, "console"); UserMenuItem properties = new UserMenuItem(command, UserMenuItem.ConsoleType.fromStr(console)); JMenuItem mi = menu.add(parent, name, properties); mi.setMnemonic(mnemonicHelper.getMnemonic(name)); if (key != null) { KeyStroke keyStroke = KeyStroke.getKeyStroke(key); mi.setAccelerator(keyStroke); } } else { throw new LoadUserMenuException("Invalid item type at index: " + items.indexOf(obj) + ", '" + obj + "'"); } } else { throw new LoadUserMenuException("Invalid item type at index: " + items.indexOf(obj) + ", '" + obj + "'"); } } } private static UserMenuItem.Command getItemCommand(Map item) { if (!item.containsKey("command")) { return null; } Object cmd = item.get("command"); if (cmd instanceof String) { return new UserMenuItem.Command((String) cmd); } else if (cmd instanceof List) { List> result = new ArrayList<>(); @SuppressWarnings("unchecked") List cmdList = (List) cmd; boolean containsArrays = listContainsLists(cmdList); if (containsArrays) { for (Object o : cmdList) { List group = new ArrayList<>(); result.add(group); if (o instanceof List) { @SuppressWarnings("unchecked") List groupList = (List) o; for (Object c : groupList) { if (c instanceof String) { group.add((String) c); } else { throw new RuntimeException("invalid command type: " + item); } } } } } else { List group = new ArrayList<>(); result.add(group); for (Object c : cmdList) { if (c instanceof String) { group.add((String) c); } else { throw new RuntimeException("invalid command type: " + item); } } } return new UserMenuItem.Command(result); } throw new RuntimeException("invalid command type: " + item); } private static boolean listContainsLists(List list) { for (Object o : list) { if (o instanceof List) { return true; } } return false; } @Nullable private static String getItemProp(Map item, String name) { Object value = item.get(name); return value != null ? value.toString() : null; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/package.html ================================================ Main muCommander UI components. ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/BookmarksQL.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import javax.swing.Icon; import com.mucommander.bookmark.Bookmark; import com.mucommander.bookmark.BookmarkListener; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.collections.AlteredVector; import com.mucommander.commons.file.FileFactory; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowBookmarksQLAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.quicklist.QuickListWithIcons; import java.util.ArrayList; import java.util.List; /** * This quick list shows existing bookmarks. * * @author Arik Hadas */ public class BookmarksQL extends QuickListWithIcons implements BookmarkListener { private Bookmark[] cachedBookmarks; private final FolderPanel folderPanel; public BookmarksQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowBookmarksQLAction.Descriptor.ACTION_ID), Translator.get("bookmarks_menu.no_bookmark")); this.folderPanel = folderPanel; bookmarksChanged(); BookmarkManager.addBookmarkListener(this); } @Override protected void acceptListItem(Bookmark item) { folderPanel.tryChangeCurrentFolder(item.getLocation()); //change with text validate } @Override protected Bookmark[] getData() { return cachedBookmarks; } @Override protected Icon itemToIcon(Bookmark item) { return getIconOfFile(FileFactory.getFile(item.getLocation())); } public void bookmarksChanged() { cachedBookmarks = prepareBookmarks(); } private static Bookmark[] prepareBookmarks() { List outList = new ArrayList<>(); AlteredVector bookmarks = BookmarkManager.getBookmarks(); for (Bookmark bookmark : bookmarks) { if (!bookmark.getLocation().isEmpty() && !bookmark.getName().equals(BookmarkManager.BOOKMARKS_SEPARATOR)) { outList.add(bookmark); } } Bookmark[] result = new Bookmark[outList.size()]; outList.toArray(result); return result; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/EditAsQL.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2019 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.process.ProcessRunner; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.action.impl.EditAsAction; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.quicklist.QuickListWithDataList; import com.mucommander.ui.quicklist.item.QuickListDataList; import com.mucommander.ui.viewer.EditorFactory; import com.mucommander.ui.viewer.EditorRegistrar; import com.mucommander.ui.viewer.FileEditor; import com.mucommander.ui.viewer.WarnUserException; import java.awt.*; import java.util.ArrayList; import java.util.List; public class EditAsQL extends QuickListWithDataList { private class CommandEditFactory implements EditorFactory { private final Command cmd; CommandEditFactory(Command cmd) { this.cmd = cmd; } @Override public boolean canEditFile(AbstractFile file) { return CommandManager.checkFileMask(cmd, file); } @Override public FileEditor createFileEditor() { return null; } @Override public String getName() { return cmd.getDisplayName();// + " (" + cmd.getCommand() + ")"; } @Override public String toString() { return getName(); } private void editFile(AbstractFile file) { try { ProcessRunner.execute(cmd.getTokens(file), file); } catch(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } } private final AbstractFile file; private final MainFrame mainFrame; public EditAsQL(MainFrame mainFame, AbstractFile file) { super(mainFame.getActivePanel(), ActionProperties.getActionLabel(EditAsAction.Descriptor.ACTION_ID), ""); this.file = file; this.mainFrame = mainFame; } @Override protected EditorFactory[] getData() { if (file == null) { return new EditorFactory[0]; } // Builtin viewers List factories = EditorRegistrar.getAllEditors(file); List result = new ArrayList<>(factories.size()); for (final EditorFactory factory : factories) { result.add(new EditorFactory() { @Override public boolean canEditFile(AbstractFile file) throws WarnUserException { return factory.canEditFile(file); } @Override public FileEditor createFileEditor() { return factory.createFileEditor(); } @Override public String getName() { return factory.getName(); } @Override public String toString() { return getName(); } }); } // View commands for (Command cmd : CommandManager.getCommands(CommandManager.EDITOR_ALIAS)) { if (CommandManager.checkFileMask(cmd, file)) { result.add(new EditAsQL.CommandEditFactory(cmd)); } } EditorFactory[] resultArray = new EditorFactory[result.size()]; resultArray = result.toArray(resultArray); return resultArray; } @Override protected void acceptListItem(EditorFactory item) { if (item instanceof EditAsQL.CommandEditFactory) { ((EditAsQL.CommandEditFactory) item).editFile(file); return; } Image icon = ActionProperties.getActionIcon(EditAction.Descriptor.ACTION_ID).getImage(); EditorRegistrar.createEditorFrame(mainFrame, file, icon);//, item, null); } @Override protected QuickListDataList getList() { return new QuickListDataList<>(getData()); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/EditorBookmarksQL.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import com.mucommander.cache.TextHistory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.action.impl.ShowEditorBookmarksQLAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.quicklist.QuickListWithIcons; import com.mucommander.ui.viewer.EditorRegistrar; import org.jetbrains.annotations.NotNull; import javax.swing.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.LinkedList; import java.util.List; public class EditorBookmarksQL extends QuickListWithIcons { private static final LinkedList LIST = new LinkedList<>(); private static final int MAX_FILES_IN_LIST = 500; private final MainFrame mainFrame; public EditorBookmarksQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowEditorBookmarksQLAction.Descriptor.ACTION_ID), i18n("editor_bookmarks_quick_list.empty_message")); mainFrame = folderPanel.getMainFrame(); } @Override protected Icon itemToIcon(AbstractFile item) { return TcAction.getStandardIcon(EditAction.class); } @Override protected AbstractFile[] getData() { List list = TextHistory.getInstance().getList(TextHistory.Type.EDITOR_BOOKMARKS); return buildFilesArray(list); } @NotNull private static AbstractFile[] buildFilesArray(List list) { AbstractFile[] result = new AbstractFile[list.size()]; for (int i = 0; i < list.size(); i++) { result[i] = FileFactory.getFile(list.get(i)); } return result; } @Override protected void acceptListItem(AbstractFile item) { if (item != null && item.exists()) { openFileInEditor(item); } else { mainFrame.getStatusBar().setStatusInfo(i18n("editor_bookmarks_quick_list.file_not_found")); } } @Override protected void onShow() { super.onShow(); mainFrame.getStatusBar().setStatusInfo(i18n("editor_bookmarks_quick_list.press_f4_to_edit_list")); } protected void onHide() { super.onHide(); mainFrame.getStatusBar().activePanelChanged(mainFrame.getActivePanel()); } private void openFileInEditor(AbstractFile file) { EditorRegistrar.createEditorFrame(mainFrame, file, ActionProperties.getActionIcon(EditAction.Descriptor.ACTION_ID).getImage()); } public static void addFile(AbstractFile file) { if (!LIST.remove(file) && LIST.size() > MAX_FILES_IN_LIST) { LIST.removeLast(); } LIST.addFirst(file); } @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_F4 && e.getModifiersEx() == 0) { e.consume(); openBookmarkFileInEditor(); } super.keyPressed(e); } private void openBookmarkFileInEditor() { try { AbstractFile file = TextHistory.getHistoryFile(TextHistory.Type.EDITOR_BOOKMARKS); setVisible(false); SwingUtilities.invokeLater(() -> openFileInEditor(file)); } catch (IOException e) { e.printStackTrace(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/ParentFoldersQL.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2014 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import java.util.List; import java.util.LinkedList; import javax.swing.Icon; import com.mucommander.commons.file.AbstractFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowParentFoldersQLAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.quicklist.QuickListWithIcons; /** * This quick list shows the parent folders of the current location in the FileTable. * * @author Arik Hadas */ public class ParentFoldersQL extends QuickListWithIcons { private final FolderPanel folderPanel; public ParentFoldersQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowParentFoldersQLAction.Descriptor.ACTION_ID), Translator.get("parent_folders_quick_list.empty_message")); this.folderPanel = folderPanel; } @Override protected void acceptListItem(AbstractFile item) { folderPanel.tryChangeCurrentFolder(item); } @Override public AbstractFile[] getData() { List abstractFiles = populateParentFolders(folderPanel.getCurrentFolder()); return abstractFiles.toArray(new AbstractFile[0]); } @Override protected Icon itemToIcon(AbstractFile item) { return getIconOfFile(item); } protected List populateParentFolders(AbstractFile folder) { List parents = new LinkedList<>(); while ((folder = folder.getParent()) != null) { parents.add(folder); } return parents; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/RecentEditedQL.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.DummyFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.action.impl.ShowRecentEditedFilesQLAction; import com.mucommander.ui.action.impl.ViewAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.quicklist.QuickListWithIcons; import com.mucommander.ui.viewer.EditorRegistrar; import com.mucommander.ui.viewer.text.TextFilesHistory; import javax.swing.Icon; import java.util.LinkedList; import java.util.List; /** * @author Oleg Trifonov * Created on 01/07/14. */ public class RecentEditedQL extends QuickListWithIcons { private static final LinkedList list = new LinkedList<>(); private static final int MAX_FILES_IN_LIST = 100; private final MainFrame mainFrame; public RecentEditedQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowRecentEditedFilesQLAction.Descriptor.ACTION_ID), Translator.get("recent_edited_files_quick_list.empty_message")); mainFrame = folderPanel.getMainFrame(); } @Override protected Icon itemToIcon(AbstractFile item) { return TcAction.getStandardIcon(EditAction.class); } @Override protected AbstractFile[] getData() { List list = TextFilesHistory.getInstance().getLastList(MAX_FILES_IN_LIST); return list.toArray(new AbstractFile[0]); } @Override protected void acceptListItem(AbstractFile item) { if (item instanceof DummyFile) { item = FileFactory.getFile(item.getURL()); } if (item != null && item.exists()) { EditorRegistrar.createEditorFrame(mainFrame, item, ActionProperties.getActionIcon(ViewAction.Descriptor.ACTION_ID).getImage()); } else { // TODO error message } } public static void addFile(AbstractFile file) { if (!list.remove(file) && list.size() > MAX_FILES_IN_LIST) { list.removeLast(); } list.addFirst(file); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/RecentExecutedFilesQL.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import java.io.IOException; import java.util.LinkedList; import javax.swing.Icon; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.TempExecJob; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowRecentExecutedFilesQLAction; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.quicklist.QuickListWithIcons; /** * This quick list shows recently executed files. * * @author Arik Hadas */ public class RecentExecutedFilesQL extends QuickListWithIcons { private static final LinkedList list = new LinkedList<>(); private static final int MAX_NUM_OF_ELEMENTS = 10; // private FolderPanel folderPanel; public RecentExecutedFilesQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowRecentExecutedFilesQLAction.Descriptor.ACTION_ID), Translator.get("recent_executed_files_quick_list.empty_message")); // this.folderPanel = folderPanel; } @Override protected void acceptListItem(AbstractFile item) { MainFrame mainFrame = WindowManager.getCurrentMainFrame(); if (item.getURL().getScheme().equals(FileProtocols.FILE) && (item.hasAncestor(LocalFile.class))) { try { DesktopManager.open(item); } catch(IOException e) { e.printStackTrace(); } } else { // Copies non-local file in a temporary local file and opens them using their native association. ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("copy_dialog.copying")); TempExecJob job = new TempExecJob(progressDialog, mainFrame, item); progressDialog.start(job); } } public static void addFile(AbstractFile file) { if (!list.remove(file) && list.size() > MAX_NUM_OF_ELEMENTS) { list.removeLast(); } list.addFirst(file); } @Override protected AbstractFile[] getData() { return list.toArray(new AbstractFile[0]); } @Override protected Icon itemToIcon(AbstractFile item) { return getIconOfFile(item); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/RecentLocationsQL.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import java.util.LinkedList; import java.util.List; import javax.swing.Icon; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.core.GlobalLocationHistory; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowRecentLocationsQLAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.quicklist.QuickListWithIcons; /** * This quick list shows recently accessed locations. * * @author Arik Hadas */ public class RecentLocationsQL extends QuickListWithIcons { private final FolderPanel folderPanel; public RecentLocationsQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowRecentLocationsQLAction.Descriptor.ACTION_ID), Translator.get("recent_locations_quick_list.empty_message")); this.folderPanel = folderPanel; } @Override protected void acceptListItem(RecentLocation item) { folderPanel.tryChangeCurrentFolder(item.url); } @Override public RecentLocation[] getData() { List list = new LinkedList<>(); for (FileURL url : GlobalLocationHistory.getInstance().getHistory()) { // Don't include the currently presented location in the list if (url.equals(folderPanel.getCurrentFolder().getURL())) { continue; } list.add(0, new RecentLocation(url)); } return list.toArray(new RecentLocation[0]); } @Override protected Icon itemToIcon(RecentLocation item) { return getIconOfFile(FileFactory.getFile(item.url)); } class RecentLocation { private final FileURL url; RecentLocation(FileURL url) { this.url = url; } @Override public String toString() { if (!FileProtocols.FILE.equals(url.getScheme())) { return url.toString(); } String path = url.getPath(); if (LocalFile.USES_ROOT_DRIVES && !path.isEmpty()) { path = path.substring(1); } return path; } @Override public boolean equals(Object obj) { if (obj instanceof RecentLocation) { return url.equals(((RecentLocation) obj).url); } return false; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/RecentViewedQL.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.DummyFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.ShowRecentViewedFilesQLAction; import com.mucommander.ui.action.impl.ViewAction; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.quicklist.QuickListWithIcons; import com.mucommander.ui.viewer.ViewerRegistrar; import com.mucommander.ui.viewer.text.TextFilesHistory; import javax.swing.Icon; import java.util.List; /** * @author Oleg Trifonv * Created on 01/07/14. */ public class RecentViewedQL extends QuickListWithIcons { private static final int MAX_FILES_IN_LIST = 50; private final MainFrame mainFrame; public RecentViewedQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowRecentViewedFilesQLAction.Descriptor.ACTION_ID), Translator.get("recent_viewed_files_quick_list.empty_message")); this.mainFrame = folderPanel.getMainFrame(); } @Override protected Icon itemToIcon(AbstractFile item) { return TcAction.getStandardIcon(ViewAction.class); } @Override protected AbstractFile[] getData() { List list = TextFilesHistory.getInstance().getLastList(MAX_FILES_IN_LIST); return list.toArray(new AbstractFile[0]); } @Override protected void acceptListItem(AbstractFile item) { if (item instanceof DummyFile) { item = FileFactory.getFile(item.getURL()); } if (item.exists()) { ViewerRegistrar.createViewerFrame(mainFrame, item, ActionProperties.getActionIcon(ViewAction.Descriptor.ACTION_ID).getImage()); } else { // TODO error message } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/RootFoldersQL.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import javax.swing.Icon; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowRootFoldersQLAction; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.quicklist.QuickListWithIcons; /** * This quick list shows roots of partitions. * * @author Arik Hadas */ public class RootFoldersQL extends QuickListWithIcons { private final FolderPanel folderPanel; public RootFoldersQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowRootFoldersQLAction.Descriptor.ACTION_ID), Translator.get("roots_quick_list.empty_message")); this.folderPanel = folderPanel; } @Override protected Icon itemToIcon(AbstractFile item) { return FileIcons.hasProperSystemIcons()?FileIcons.getSystemFileIcon(item):null; } @Override protected AbstractFile[] getData() { return LocalFile.getVolumes(); } @Override protected void acceptListItem(AbstractFile item) { folderPanel.tryChangeCurrentFolder(item); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/TabsQL.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import java.awt.Dimension; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.swing.Icon; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ShowTabsQLAction; import com.mucommander.ui.icon.EmptyIcon; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.tabs.FileTableTab; import com.mucommander.ui.main.tabs.FileTableTabHeader; import com.mucommander.ui.main.tabs.PrintableFileTableTabFactory; import com.mucommander.ui.quicklist.QuickListWithIcons; import com.mucommander.ui.tabs.TabFactory; /** * This quick list shows the tabs contained in the FolderPanel. * * @author Arik Hadas */ public class TabsQL extends QuickListWithIcons { /** The FolderPanel that contains the tabs */ private final FolderPanel folderPanel; private final TabFactory tabsFactory = new PrintableFileTableTabFactory(); private final Icon lockedTabIcon = IconManager.getIcon(IconManager.IconSet.COMMON, FileTableTabHeader.LOCKED_ICON_NAME); private final Icon unlockedTabIcon = new EmptyIcon(8, 9); public TabsQL(FolderPanel folderPanel) { super(folderPanel, ActionProperties.getActionLabel(ShowTabsQLAction.Descriptor.ACTION_ID), Translator.get("tabs_quick_list.empty_message")); this.folderPanel = folderPanel; } @Override protected Icon getImageIconOfItemImp(final FileTableTab item, final Dimension preferredSize) { return itemToIcon(item); } @Override protected Icon itemToIcon(FileTableTab item) { return item.isLocked() ? lockedTabIcon : unlockedTabIcon; } protected FileTableTab[] getData() { List tabsList = new ArrayList<>(); Iterator tabsIterator = folderPanel.getTabs().iterator(); while (tabsIterator.hasNext()) { tabsList.add(tabsFactory.createTab(tabsIterator.next())); } // Remove the selected tab from the list tabsList.remove(folderPanel.getTabs().getSelectedIndex()); return tabsList.toArray(new FileTableTab[0]); } @Override protected void acceptListItem(FileTableTab item) { folderPanel.getTabs().selectTab(item); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/ViewAsQL.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2019 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import com.mucommander.command.Command; import com.mucommander.command.CommandManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.process.ProcessRunner; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.ViewAction; import com.mucommander.ui.action.impl.ViewAsAction; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.quicklist.QuickListWithDataList; import com.mucommander.ui.quicklist.item.QuickListDataList; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.ViewerFactory; import com.mucommander.ui.viewer.ViewerRegistrar; import com.mucommander.ui.viewer.WarnUserException; import java.awt.Image; import java.util.ArrayList; import java.util.List; /** * @author Oleg Trifonov * Created on 02/07/14. */ public class ViewAsQL extends QuickListWithDataList { private class CommandViewFactory implements ViewerFactory { private final Command cmd; CommandViewFactory(Command cmd) { this.cmd = cmd; } @Override public boolean canViewFile(AbstractFile file) { return CommandManager.checkFileMask(cmd, file); } @Override public FileViewer createFileViewer() { return null; } @Override public String getName() { return cmd.getDisplayName();// + " (" + cmd.getCommand() + ")"; } @Override public String toString() { return getName(); } private void viewFile(AbstractFile file) { try { ProcessRunner.execute(cmd.getTokens(file), file); } catch(Exception e) { InformationDialog.showErrorDialog(mainFrame.getJFrame()); } } } private final AbstractFile file; private final MainFrame mainFrame; public ViewAsQL(MainFrame mainFame, AbstractFile file) { super(mainFame.getActivePanel(), ActionProperties.getActionLabel(ViewAsAction.Descriptor.ACTION_ID), ""); this.file = file; this.mainFrame = mainFame; } @Override protected ViewerFactory[] getData() { if (file == null) { return new ViewerFactory[0]; } // Builtin viewers List factories = ViewerRegistrar.getAllViewers(file); List result = new ArrayList<>(factories.size()); for (final ViewerFactory factory : factories) { result.add(new ViewerFactory() { @Override public boolean canViewFile(AbstractFile file) throws WarnUserException { return factory.canViewFile(file); } @Override public FileViewer createFileViewer() { return factory.createFileViewer(); } @Override public String getName() { return factory.getName(); } @Override public String toString() { return getName(); } }); } // View commands for (Command cmd : CommandManager.getCommands(CommandManager.VIEWER_ALIAS)) { if (CommandManager.checkFileMask(cmd, file)) { result.add(new CommandViewFactory(cmd)); } } ViewerFactory[] resultArray = new ViewerFactory[result.size()]; resultArray = result.toArray(resultArray); return resultArray; } @Override protected void acceptListItem(ViewerFactory item) { if (item instanceof CommandViewFactory) { ((CommandViewFactory) item).viewFile(file); return; } Image icon = ActionProperties.getActionIcon(ViewAction.Descriptor.ACTION_ID).getImage(); ViewerRegistrar.createViewerFrame(mainFrame, file, icon, item, null); } @Override protected QuickListDataList getList() { return new QuickListDataList<>(getData()); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/quicklist/ViewedAndEditedFilesQL.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.quicklist; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.quicklist.QuickListContainer; import com.mucommander.ui.quicklist.QuickListWithIcons; import com.mucommander.ui.viewer.FileFrame; import com.mucommander.ui.viewer.FileViewersList; import javax.swing.Icon; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.List; /** * @author Oleg Trifonov * Created on 28/07/16. */ public class ViewedAndEditedFilesQL extends QuickListWithIcons { private final List files = FileViewersList.getFiles(); private final AbstractFile currentFile; private int currentFileIndex = -1; public ViewedAndEditedFilesQL(QuickListContainer container, AbstractFile currentFile) { super(container, Translator.get("file_editor.files.list"), ""); this.currentFile = currentFile; dataList.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_TAB) { selectNext(); e.consume(); } } @Override public void keyReleased(KeyEvent e) { int mask = OsFamily.MAC_OS_X.isCurrent() ? KeyEvent.VK_ALT : KeyEvent.VK_CONTROL; if (e.getKeyCode() == mask) { setVisible(false); acceptListItem(dataList.getSelectedValue()); } } }); } @Override protected Icon itemToIcon(AbstractFile item) { for (FileViewersList.FileRecord rec : files) { if (rec.fileName.equals(item.getAbsolutePath())) { return rec.getIcon(); } } return null; } @Override protected AbstractFile[] getData() { AbstractFile[] result = new AbstractFile[files.size()]; for (int i = 0 ; i < files.size(); i++) { result[i] = FileFactory.getFile(files.get(i).fileName); if (result[i].equals(currentFile)) { currentFileIndex = i; } } return result; } @Override public void show() { super.show(); if (currentFileIndex >= 0) { dataList.setSelectedIndex(currentFileIndex); selectNext(); } } @Override protected void acceptListItem(AbstractFile item) { if (item == dataList.getSelectedValue()) { FileFrame frame = files.get(dataList.getSelectedIndex()).fileFrameRef.get(); if (frame != null) { frame.toFront(); return; } } // theoretically these next code will execute newer for (FileViewersList.FileRecord rec : files) { if (rec.fileName.equals(item.getAbsolutePath())) { FileFrame frame = rec.fileFrameRef.get(); if (frame != null) { frame.toFront(); } return; } } } private void selectNext() { int selected = dataList.getSelectedIndex(); selected++; if (selected >= dataList.getModel().getSize()) { selected = 0; } dataList.setSelectedIndex(selected); } public boolean isEmpty() { return files.isEmpty(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/FileWindowsListButton.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import com.jidesoft.swing.JideSplitButton; import com.mucommander.RuntimeConstants; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.text.FontUtils; import com.mucommander.ui.viewer.FileViewersList; import com.mucommander.utils.FileIconsCache; import javax.swing.Icon; import javax.swing.JFrame; import javax.swing.JMenuItem; import javax.swing.SwingUtilities; import java.awt.Window; import java.util.ArrayList; import java.util.List; import static com.mucommander.ui.viewer.FileViewersList.FileRecord; /** * Created on 09/06/16. * @author Oleg Trifonov */ public class FileWindowsListButton extends JideSplitButton { private long lastUpdateTime; private FileRecord selectedRecord; private MainFrame selectedMainFrame; private final boolean includeMainFrames; public FileWindowsListButton(boolean includeMainFrames) { super(); this.includeMainFrames = includeMainFrames; if (OsFamily.getCurrent() == OsFamily.LINUX && RuntimeConstants.DISPLAY_4K) { FontUtils.scaleFont(this, 18, 22); } updateList(); addActionListener(e -> showSelectedFile()); } FileWindowsListButton() { this(false); } private void showSelectedFile() { if (selectedRecord == null) { if (selectedMainFrame != null) { setText(mainFrameName(selectedMainFrame)); setIcon(getIconFrom(selectedMainFrame)); selectedMainFrame.toFront(); } return; } JFrame fileFrame = selectedRecord.fileFrameRef.get(); if (fileFrame != null) { fileFrame.toFront(); } } private void selectFile(FileRecord fileRecord) { setText(fileRecord.shortName); setIcon(getIconFrom(fileRecord)); selectedRecord = fileRecord; showSelectedFile(); } private void updateList() { removeAll(); if (includeMainFrames) { List mainFrames = WindowManager.getMainFrames(); for (MainFrame mainFrame : mainFrames) { String name = mainFrameName(mainFrame); JMenuItem menuItem = new JMenuItem(name, getIconFrom(mainFrame)); menuItem.addActionListener(e -> mainFrame.toFront()); add(menuItem); } } boolean containsSelected = false; List list = new ArrayList<>(FileViewersList.getFiles()); for (FileRecord fr : list) { Window excludedFrame = SwingUtilities.getWindowAncestor(this); if (fr.fileFrameRef.get() == excludedFrame) { continue; } JMenuItem menuItem = new JMenuItem(fr.fileName, getIconFrom(fr)); menuItem.addActionListener(e -> selectFile(fr)); add(menuItem); if (fr == selectedRecord) { containsSelected = true; } } if (!containsSelected) { selectedRecord = null; } if (getMenuComponentCount() > 0) { if (selectedRecord == null) { if (includeMainFrames) { selectedRecord = null; selectedMainFrame = WindowManager.getCurrentMainFrame(); setText(mainFrameName(selectedMainFrame)); setIcon(getIconFrom(selectedMainFrame)); } else { selectedRecord = FileViewersList.getFiles().get(0); setText(selectedRecord.shortName); setIcon(getIconFrom(selectedRecord)); } } setVisible(true); } else { setVisible(false); selectedRecord = null; } lastUpdateTime = System.currentTimeMillis(); } private static String mainFrameName(MainFrame mainFrame) { return mainFrame.getLeftPanel().getCurrentFolder().getName() + " : " + mainFrame.getRightPanel().getCurrentFolder().getName(); } private Icon getIconFrom(MainFrame mainFrame) { return FileIconsCache.getInstance().getIcon(mainFrame.getActivePanel().getCurrentFolder()); } private Icon getIconFrom(FileRecord fileRecord) { return fileRecord == null ? null : fileRecord.getIcon(); } @Override public boolean isVisible() { if (lastUpdateTime < FileViewersList.getLastUpdateTime()) { SwingUtilities.invokeLater(this::updateList); } return super.isVisible(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/HeapIndicator.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import com.mucommander.cache.TextHistory; import com.mucommander.cache.WindowsStorage; import com.mucommander.ui.border.MutableLineBorder; import com.mucommander.ui.theme.*; import com.mucommander.utils.FileIconsCache; import javax.swing.JLabel; import javax.swing.Timer; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * @author Oleg Trifonov * Created on 13/11/14. */ public class HeapIndicator extends JLabel implements ActionListener, ThemeListener, MouseListener { private Timer timer; private int refreshInterval; private long usedMem; private long totalMem; private static final Color COLOR_BORDER = new Color(0x555555); private static final Color COLOR_FOREGROUND = new Color(0x8888ff); HeapIndicator() { super(""); setHorizontalAlignment(CENTER); setRefreshInterval(1000*10); update(); // setMinimumSize(new Dimension(80, 0)); // setMaximumSize(new Dimension(80, 100)); addMouseListener(this); setBorder(new MutableLineBorder(ThemeManager.getCurrentColor(Theme.STATUS_BAR_BORDER_COLOR))); } @Override public Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); return new Dimension(d.width+4, d.height+2); } @Override public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); g.setColor(COLOR_BORDER); g.drawRect(0, 0, width-1, height-1); g.setColor(COLOR_FOREGROUND); int x2 = (int)(width*((float)usedMem/(float)totalMem)); g.fillRect(1, 1, x2-1, height-2); super.paint(g); } private static long bytesToKb(long bytes) { return bytes / 1024L; } private static long bytesToMb(long bytes) { return bytes / 1024L / 1024L; } /** * Updates heap memory information. */ private void update() { totalMem = Runtime.getRuntime().totalMemory(); usedMem = totalMem - Runtime.getRuntime().freeMemory(); int percent = (int)(100*usedMem/totalMem); //setText(" " + bytesToMb(usedMem) + " MB "); setText(" " + bytesToMb(totalMem) + " MB "); setToolTipText("Memory used " + bytesToMb(usedMem) + "MB from " + bytesToMb(totalMem) + "MB " + percent + "%"); } private void installTimer(int interval) { if (timer == null) { timer = new Timer(interval, this); } else { timer.stop(); timer.setDelay(interval); } timer.start(); } private void uninstallTimer() { if (timer != null) { timer.stop(); timer.removeActionListener(this); timer = null; } } private void setRefreshInterval(int interval) { this.refreshInterval = interval; installTimer(interval); } @Override public void actionPerformed(ActionEvent e) { update(); repaint(); } @Override public void setVisible(boolean visible) { if (visible) { installTimer(refreshInterval); } else { uninstallTimer(); } super.setVisible(visible); } @Override public void colorChanged(ColorChangedEvent event) { } @Override public void fontChanged(FontChangedEvent event) { } @Override public void mouseClicked(MouseEvent e) { TextHistory.getInstance().clear(); WindowsStorage.getInstance().clear(); FileIconsCache.getInstance().clear(); System.gc(); update(); repaint(); } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/StatusBar.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import com.mucommander.cache.FastLRUCache; import com.mucommander.cache.LRUCache; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.impl.CachedFile; import com.mucommander.commons.file.impl.ftp.FTPFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.impl.sftp.SFTPFile; import com.mucommander.commons.file.util.SymLinkUtils; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.utils.text.SizeFormat; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.event.TableSelectionListener; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; import com.mucommander.ui.theme.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.trolsoft.utils.ImageSizeDetector; import ru.trolsoft.utils.JavaClassVersionDetector; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; /** * StatusBar is the component that sits at the bottom of each MainFrame, between the folder panels and command bar. * There is one and only one StatusBar per MainFrame, created by the associated MainFrame. It can be hidden, * but the instance will always remain, until the MainFrame is disposed. * *

    StatusBar is used to display info about the total/selected number of files in the current folder and current volume's * free/total space. When a folder is being changed, a waiting message is displayed. When quick search is being used, * the current quick search string is displayed. * *

    StatusBar receives LocationListener events when the folder has or is being changed, and automatically updates * selected files and volume info, and display the waiting message when the folder is changing. Quick search info * is set by FileTable.QuickSearch. * *

    When StatusBar is visible, a Thread runs in the background to periodically update free/total space volume info. * This thread stops when the StatusBar is hidden. * * @author Maxence Bernard */ public class StatusBar extends JPanel implements Runnable, MouseListener, ActivePanelListener, TableSelectionListener, LocationListener, ComponentListener, ThemeListener { private static final Logger LOGGER = LoggerFactory.getLogger(StatusBar.class); private final MainFrame mainFrame; /** * Label that displays info about current selected file(s) */ private final JLabel selectedFilesLabel; /** * Icon used while loading is in progress. */ private final SpinningDial dial; /** * Label that displays info about current volume (free/total space) */ private final VolumeSpaceLabel volumeSpaceLabel; private final TaskPanel taskPanel; private final JProgressBar progressBar; private final Component progressGlue; /** * Thread which auto updates volume info */ private Thread autoUpdateThread; /** * Number of volume info strings that can be temporarily cached */ private final static int VOLUME_INFO_CACHE_CAPACITY = 50; /** * Number of milliseconds before cached volume info strings expire */ private final static int VOLUME_INFO_TIME_TO_LIVE = 60000; /** * Number of milliseconds between each volume info update by auto-update thread */ private final static int AUTO_UPDATE_PERIOD = 60_000; /** * Caches volume info strings (free/total space) for a while, since this information is expensive to retrieve * (I/O bound). This map uses folders' volume path as its key. */ private static final LRUCache volumeInfoCache = new FastLRUCache<>(VOLUME_INFO_CACHE_CAPACITY); /** * Icon that is displayed when folder is changing */ public final static String WAITING_ICON = "waiting.png"; /** * Listens to configuration changes and updates static fields accordingly */ private final static ConfigurationListener CONFIGURATION_ADAPTER; /** * SizeFormat format used to create the selected file(s) size string */ private static int selectedFileSizeFormat; private final static ExtensionFilenameFilter SUPPORTED_IMAGE_FILTER = new ExtensionFilenameFilter(new String[]{ ".png", ".gif", ".jpg", ".jpeg", ".bmp", ".tga", ".tiff", ".tif"}); private final static ExtensionFilenameFilter JAVA_CLASS_FILTER = new ExtensionFilenameFilter(".class"); static { // Initialize the size column format based on the configuration setSelectedFileSizeFormat(TcConfigurations.getPreferences().getVariable(TcPreference.DISPLAY_COMPACT_FILE_SIZE, TcPreferences.DEFAULT_DISPLAY_COMPACT_FILE_SIZE)); // Listens to configuration changes and updates static fields accordingly. // Note: a reference to the listener must be kept to prevent it from being garbage-collected. CONFIGURATION_ADAPTER = new ConfigurationListener() { public synchronized void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); if (var.equals(TcPreferences.DISPLAY_COMPACT_FILE_SIZE)) { setSelectedFileSizeFormat(event.getBooleanValue()); } } }; TcConfigurations.addPreferencesListener(CONFIGURATION_ADAPTER); } /** * Sets the SizeFormat format used to create the selected file(s) size string. * * @param compactSize true to use a compact size format, false for full size in bytes */ private static void setSelectedFileSizeFormat(boolean compactSize) { if (compactSize) { selectedFileSizeFormat = SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_SHORT | SizeFormat.ROUND_TO_KB; } else { selectedFileSizeFormat = SizeFormat.DIGITS_FULL | SizeFormat.UNIT_LONG; } selectedFileSizeFormat |= SizeFormat.INCLUDE_SPACE; } /** * Creates a new StatusBar instance. */ public StatusBar(MainFrame mainFrame) { // create and add status bar setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); this.mainFrame = mainFrame; progressBar = new JProgressBar(); add(progressBar); //progressBar.setVisible(false); progressGlue = Box.createHorizontalGlue(); add(progressGlue); showProgress(-1); selectedFilesLabel = new JLabel(""); dial = new SpinningDial(); add(selectedFilesLabel); add(Box.createHorizontalGlue()); FileWindowsListButton fileWindowsListButton = new FileWindowsListButton(); add(fileWindowsListButton); taskPanel = new TaskPanel(); add(taskPanel); add(Box.createRigidArea(new Dimension(2, 0))); HeapIndicator heapIndicator = new HeapIndicator(); add(heapIndicator); add(Box.createRigidArea(new Dimension(2, 0))); // Add a button for interacting with the trash, only if the current platform has a trash implementation if (DesktopManager.getTrash() != null) { TrashPopupButton trashButton = new TrashPopupButton(mainFrame); trashButton.setPopupMenuLocation(SwingConstants.TOP); add(trashButton); add(Box.createRigidArea(new Dimension(2, 0))); } volumeSpaceLabel = new VolumeSpaceLabel(); add(volumeSpaceLabel); // Show/hide this status bar based on user preferences // Note: setVisible has to be called even with true for the auto-update thread to be initialized setVisible(shouldBeVisible()); // Catch location events to update status bar info when folder is changed FolderPanel leftPanel = mainFrame.getLeftPanel(); leftPanel.getLocationManager().addLocationListener(this); FolderPanel rightPanel = mainFrame.getRightPanel(); rightPanel.getLocationManager().addLocationListener(this); // Catch table selection change events to update the selected files info when the selected files have changed on // one of the file tables leftPanel.getFileTable().addTableSelectionListener(this); rightPanel.getFileTable().addTableSelectionListener(this); // Catch active panel change events to update status bar info when current table has changed mainFrame.addActivePanelListener(this); // Catch mouse events to pop up a menu on right-click selectedFilesLabel.addMouseListener(this); volumeSpaceLabel.addMouseListener(this); addMouseListener(this); // Catch component events to be notified when this component is made visible // and update status info addComponentListener(this); // Initializes theme. selectedFilesLabel.setFont(ThemeManager.getCurrentFont(Theme.STATUS_BAR_FONT)); selectedFilesLabel.setForeground(ThemeManager.getCurrentColor(Theme.STATUS_BAR_FOREGROUND_COLOR)); volumeSpaceLabel.setFont(ThemeManager.getCurrentFont(Theme.STATUS_BAR_FONT)); volumeSpaceLabel.setForeground(ThemeManager.getCurrentColor(Theme.STATUS_BAR_FOREGROUND_COLOR)); ThemeManager.addCurrentThemeListener(this); } private static boolean shouldBeVisible() { return TcConfigurations.getPreferences().getVariable(TcPreference.STATUS_BAR_VISIBLE, TcPreferences.DEFAULT_STATUS_BAR_VISIBLE); } /** * Updates info displayed on the status bar: currently selected files and volume info. */ private void updateStatusInfo() { // No need to waste precious cycles if status bar is not visible if (!isVisible()) { return; } updateSelectedFilesInfo(); updateVolumeInfo(); } /** * Updates info about currently selected files ((nb of selected files, combined size), displayed on the left-side of this status bar. */ // Making this method synchronized creates a deadlock with FileTable // public synchronized void updateSelectedFilesInfo() { public void updateSelectedFilesInfo() { // No need to waste precious cycles if status bar is not visible if (!isVisible()) { return; } FileTable currentFileTable = mainFrame.getActiveTable(); // Currently select file, can be null AbstractFile selectedFile = currentFileTable.getSelectedFile(false, true); BaseFileTableModel tableModel = currentFileTable.getFileTableModel(); // Number of marked files, can be 0 int nbMarkedFiles = tableModel.getNbMarkedFiles(); // Combined size of marked files, 0 if no file has been marked long markedTotalSize = tableModel.getTotalMarkedSize(); // number of files in folder int fileCount = tableModel.getFileCountWithoutParent(); // Update files info based on marked files if there are some, or currently selected file otherwise int nbSelectedFiles = nbMarkedFiles == 0 && selectedFile != null ? 1 : nbMarkedFiles; StringBuilder filesInfo = new StringBuilder(); if (fileCount == 0) { // Set status bar to a space character, not an empty string otherwise it will disappear filesInfo.append(' '); } else { filesInfo.append(Translator.get("status_bar.selected_files", String.valueOf(nbSelectedFiles), String.valueOf(fileCount))); if (nbMarkedFiles > 0) { filesInfo.append(" - "); filesInfo.append(SizeFormat.format(markedTotalSize, selectedFileSizeFormat)); } if (selectedFile != null) { appendSelectedFileInfo(filesInfo, selectedFile); } } // Update label setStatusInfo("" + filesInfo); } private void appendSelectedFileInfo(StringBuilder filesInfo, AbstractFile selectedFile) { filesInfo.append(" - "); filesInfo.append(""); filesInfo.append(selectedFile.getName()); filesInfo.append(""); if (selectedFile.isSymlink()) { String target = getFileLink(selectedFile); if (target != null) { filesInfo.append(" -> "); filesInfo.append(target); } } boolean local = selectedFile.getAncestor() instanceof LocalFile; if (selectedFile.isDirectory()) { if (local) { filesInfo.append(" ("); try { filesInfo.append(selectedFile.ls().length); } catch (IOException ignored) { } filesInfo.append(' '); filesInfo.append(Translator.get("files")); filesInfo.append(')'); } } else { filesInfo.append(" ("); filesInfo.append(SizeFormat.format(selectedFile.getSize(), SizeFormat.DIGITS_FULL | SizeFormat.UNIT_LONG | SizeFormat.INCLUDE_SPACE)); if (local && SUPPORTED_IMAGE_FILTER.accept(selectedFile)) { // Show image size try (InputStream is = selectedFile.getInputStream()) { ImageSizeDetector detector = new ImageSizeDetector(is); if (detector.getType() != null) { filesInfo.append(", "); filesInfo.append(detector.getWidth()); filesInfo.append(" x "); filesInfo.append(detector.getHeight()); } } catch (FileNotFoundException ignore) { // etc. if file was moved } catch (IOException e) { e.printStackTrace(); } } else if (JAVA_CLASS_FILTER.accept(selectedFile)) { try (InputStream is = selectedFile.getPushBackInputStream(16)) { JavaClassVersionDetector detector = new JavaClassVersionDetector(is); if (detector.getVersion() != JavaClassVersionDetector.Version.UNKNOWN) { filesInfo.append(", Java v").append(detector.getVersion().name); } else if (detector.getVersion() != JavaClassVersionDetector.Version.WRONG_FORMAT) { filesInfo.append(", Java major = ").append(detector.getMajor()).append(", minor = ").append(detector.getMinor()); } } catch (IOException e) { e.printStackTrace(); } } filesInfo.append(")"); } } private static String getFileLink(AbstractFile file) { AbstractFile f; if (file instanceof CachedFile) { f = ((CachedFile) file).getProxiedFile(); } else { f = file; } if (f instanceof LocalFile) { return SymLinkUtils.getTargetPath(file); } else if (f instanceof FTPFile) { return ((FTPFile) f).getLink(); } else if (f instanceof SFTPFile) { return ((SFTPFile) f).getLink(); } return null; } /** * Updates info about current volume (free space, total space), displayed on the right-side of this status bar. */ private synchronized void updateVolumeInfo() { // No need to waste precious cycles if status bar is not visible if (!isVisible()) { return; } final AbstractFile currentFolder = mainFrame.getActivePanel().getCurrentFolder(); // Resolve the current folder's volume and use its path as a key for the volume info cache final String volumePath = currentFolder != null && currentFolder.exists() ? currentFolder.getVolume().getAbsolutePath(true) : ""; Long[] cachedVolumeInfo = volumeInfoCache.get(volumePath); if (cachedVolumeInfo != null) { LOGGER.debug("Cache hit!"); volumeSpaceLabel.setVolumeSpace(cachedVolumeInfo[0], cachedVolumeInfo[1]); } else { // Retrieves free and total volume space. // Perform volume info retrieval in a separate thread as this method may be called // by the event thread and it can take a while, we want to return as soon as possible new Thread("StatusBar.updateVolumeInfo") { @Override public void run() { // Free space on current volume, -1 if this information is not available long volumeFree; // Total space on current volume, -1 if this information is not available long volumeTotal; // Folder is a local file and Java version is 1.5: call getVolumeInfo() instead of // separate calls to getFreeSpace() and getTotalSpace() as it is twice as fast. // if (currentFolder instanceof LocalFile && JavaVersion.JAVA_1_5.isCurrentOrLower()) { // try { // long volumeInfo[] = ((LocalFile)currentFolder).getVolumeInfo(); // volumeTotal = volumeInfo[0]; // volumeFree = volumeInfo[1]; // } catch (IOException e) { // volumeTotal = -1; // volumeFree = -1; // } // } // Java 1.6 and up or any other file type // else { try { volumeFree = currentFolder != null ? currentFolder.getFreeSpace() : -1; } catch (IOException e) { volumeFree = -1; } try { volumeTotal = currentFolder != null ? currentFolder.getTotalSpace() : -1; } catch (IOException e) { volumeTotal = -1; } // } // For testing the free space indicator //volumeFree = (long)(volumeTotal * Math.random()); volumeSpaceLabel.setVolumeSpace(volumeTotal, volumeFree); LOGGER.debug("Adding to cache"); volumeInfoCache.add(volumePath, new Long[]{volumeTotal, volumeFree}, VOLUME_INFO_TIME_TO_LIVE); } }.start(); } } /** * Displays the specified text and icon on the left-side of the status bar, * replacing any previous information. * * @param text the piece of text to display * @param icon the icon to display next to the text * @param iconBeforeText if true, icon will be placed on the left side of the text, if not on the right side */ public void setStatusInfo(String text, Icon icon, boolean iconBeforeText) { selectedFilesLabel.setText(text); if (icon == null) { // What we don't want here is the label's height to change depending on whether it has an icon or not. // This would result in having to revalidate the status bar and in turn the whole MainFrame. // A label's height is roughly the max of the text's font height and the icon (if any). So if there is no // icon for the label, we use a transparent image for padding in case the text's font height is smaller // than a 'standard' (16x16) icon. This ensures that the label's height remains constant. BufferedImage bi = new BufferedImage(1, 16, BufferedImage.TYPE_INT_ARGB); icon = new ImageIcon(bi); } selectedFilesLabel.setIcon(icon); selectedFilesLabel.setHorizontalTextPosition(iconBeforeText ? JLabel.TRAILING : JLabel.LEADING); } /** * Displays the specified text on the left-side of the status bar, * replacing any previous text and icon. * * @param infoMessage the piece of text to display */ public void setStatusInfo(String infoMessage) { setStatusInfo(infoMessage, null, false); } /** * Starts a volume info auto-update thread, only if there isn't already one running. */ private synchronized void startAutoUpdate() { if (autoUpdateThread == null) { // Start volume info auto-update thread autoUpdateThread = new Thread(this, "StatusBar autoUpdateThread"); // Set the thread as a daemon thread autoUpdateThread.setDaemon(true); autoUpdateThread.start(); } } /** * Overrides JComponent.setVisible(boolean) to start/stop volume info auto-update thread. */ @Override public void setVisible(boolean visible) { if (visible) { // Start auto-update thread startAutoUpdate(); super.setVisible(true); // Update status bar info updateStatusInfo(); } else { // Stop auto-update thread this.autoUpdateThread = null; super.setVisible(false); } } /** * Periodically updates volume info (free / total space). */ @Override public void run() { do { // Sleep for a while try { Thread.sleep(AUTO_UPDATE_PERIOD); } catch (InterruptedException ignore) { } // Update volume info if: // - status bar is visible // - MainFrame isn't changing folders // - MainFrame is active and in the foreground // Volume info update will potentially hit the LRU cache and not actually update volume info if (isVisible() && !mainFrame.getNoEventsMode() && mainFrame.isForegroundActive()) { updateVolumeInfo(); } } while (autoUpdateThread != null && mainFrame.getJFrame().isVisible()); // Stop when MainFrame is disposed } @Override public void activePanelChanged(FolderPanel folderPanel) { updateStatusInfo(); } @Override public void selectedFileChanged(FileTable source) { // No need to update if the originating FileTable is not the currently active one if (source == mainFrame.getActiveTable() && mainFrame.isForegroundActive()) { updateSelectedFilesInfo(); } } @Override public void markedFilesChanged(FileTable source) { // No need to update if the originating FileTable is not the currently active one if (source == mainFrame.getActiveTable() && mainFrame.isForegroundActive()) { updateSelectedFilesInfo(); } } @Override public void locationChanged(LocationEvent e) { dial.setAnimated(false); updateStatusInfo(); } @Override public void locationChanging(LocationEvent e) { // Show a message in the status bar saying that folder is being changed setStatusInfo(Translator.get("status_bar.connecting_to_folder"), dial, true); dial.setAnimated(true); } @Override public void locationCancelled(LocationEvent e) { dial.setAnimated(false); updateStatusInfo(); } @Override public void locationFailed(LocationEvent e) { dial.setAnimated(false); updateStatusInfo(); } @Override public void mouseClicked(MouseEvent e) { // Discard mouse events while in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } // Right-clicking on the toolbar brings up a popup menu that allows the user to hide this status bar if (DesktopManager.isRightMouseButton(e)) { // if (e.isPopupTrigger()) { // Doesn't work under Mac OS X (CTRL+click doesn't return true) JPopupMenu popupMenu = new JPopupMenu(); popupMenu.add(ActionManager.getActionInstance(com.mucommander.ui.action.impl.ToggleStatusBarAction.Descriptor.ACTION_ID, mainFrame)); popupMenu.show(this, e.getX(), e.getY()); popupMenu.setVisible(true); } if (e.getSource() == volumeSpaceLabel) { volumeInfoCache.clearAll(); updateVolumeInfo(); } } @Override public void mouseReleased(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void componentShown(ComponentEvent e) { // Invoked when the component has been made visible (apparently not called when just created) // Status bar needs to be updated since it is not updated when not visible updateStatusInfo(); } @Override public void componentHidden(ComponentEvent e) { } @Override public void componentMoved(ComponentEvent e) { } @Override public void componentResized(ComponentEvent e) { } @Override public void fontChanged(FontChangedEvent event) { if (event.getFontId() == Theme.STATUS_BAR_FONT) { selectedFilesLabel.setFont(event.getFont()); volumeSpaceLabel.setFont(event.getFont()); repaint(); } } @Override public void colorChanged(ColorChangedEvent event) { if (event.getColorId() == Theme.STATUS_BAR_FOREGROUND_COLOR) { selectedFilesLabel.setForeground(event.getColor()); volumeSpaceLabel.setForeground(event.getColor()); repaint(); } } public TaskPanel getTaskPanel() { return taskPanel; } private void showProgress(int progress) { if (progress >= 0) { progressBar.setVisible(true); progressBar.setValue(progress); progressGlue.setMaximumSize(new Dimension(Short.MAX_VALUE, 0)); progressGlue.revalidate(); } else { progressBar.setVisible(false); progressGlue.setMaximumSize(new Dimension(0, 0)); progressGlue.revalidate(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/TaskPanel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import javax.swing.BoxLayout; import javax.swing.JPanel; /** * @author Oleg Trifonov * Created on 08/12/14. */ public class TaskPanel extends JPanel { public TaskPanel() { super(); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); } public void addTask(TaskWidget taskWidget) { add(taskWidget); validate(); repaint(); getParent().revalidate(); getParent().repaint(); taskWidget.taskPanel = this; } public void removeWidget(TaskWidget taskWidget) { remove(taskWidget); validate(); repaint(); getParent().revalidate(); getParent().repaint(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/TaskWidget.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import com.mucommander.ui.dialog.FocusDialog; import org.apache.commons.lang.StringUtils; import ru.trolsoft.ui.TProgressBar; import javax.swing.*; import java.awt.*; import java.awt.event.*; /** * @author Oleg Trifonov * Created on 08/12/14. */ public class TaskWidget extends TProgressBar {//NonFocusableButton { private static final Insets INSETS = new Insets(2, 2, 2, 2); private static final int STRING_LENGTH = 25; private int progress; TaskPanel taskPanel; private FocusDialog progressDialog; public TaskWidget() { super(); init(); } public TaskWidget(String text) { super(); setString(text); init(); } public TaskWidget(Icon icon) { super(); init(); } public TaskWidget(String text, Icon icon) { super(); setText(text); init(); } private void init() { setStringPainted(true); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (progressDialog != null) { setVisible(false); progressDialog.showDialog(); //progressDialog.setVisible(true); } } }); } /** * Replace the default insets to be exactly (2,2,2,2). */ @Override public Insets getInsets() { return INSETS; } public void removeFromPanel() { if (taskPanel != null) { taskPanel.removeWidget(this); } } public void setProgress(int progress) { this.progress = progress; setValue(progress); } public FocusDialog getProgressDialog() { return progressDialog; } public void setProgressDialog(FocusDialog progressDialog) { this.progressDialog = progressDialog; } public void setText(String s) { if (s.length() > STRING_LENGTH) { s = s.substring(0, STRING_LENGTH-4) + ".."; } s = StringUtils.center(s, STRING_LENGTH); setString(s); Dimension dim = new JLabel(s).getPreferredSize(); setPreferredSize(new Dimension(dim.width + 20, getPreferredSize().height)); setMaximumSize(new Dimension(dim.width + 20, getMaximumSize().height)); if (getParent() != null) { getParent().revalidate(); } } public String getText() { return getString(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/TrashPopupButton.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import com.mucommander.desktop.AbstractTrash; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.impl.EmptyTrashAction; import com.mucommander.ui.action.impl.OpenTrashAction; import com.mucommander.ui.button.PopupButton; import com.mucommander.ui.button.RolloverButtonAdapter; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import javax.swing.*; import java.awt.*; /** * TrashPopupButton is a button that allows to interact with the current platform's trash, as returned by * {@link com.mucommander.desktop.DesktopManager#getTrash()}. * When the button is clicked, a popup menu is displayed, allowing to perform a choice of actions such as opening * the trash or emptying it. * Note that this button will only be functional if a trash is available on the current platform. * * @author Maxence Bernard */ public class TrashPopupButton extends PopupButton { private final MainFrame mainFrame; TrashPopupButton(MainFrame mainFrame) { this.mainFrame = mainFrame; setContentAreaFilled(false); setIcon(IconManager.getIcon(IconManager.IconSet.STATUS_BAR, "trash.png")); RolloverButtonAdapter.decorateButton(this); } @Override public JPopupMenu getPopupMenu() { JPopupMenu popupMenu = new JPopupMenu(); AbstractTrash trash = DesktopManager.getTrash(); if (trash != null) { if (trash.canOpen()) { popupMenu.add(ActionManager.getActionInstance(OpenTrashAction.Descriptor.ACTION_ID, mainFrame)); } if (trash.canEmpty()) { JMenuItem emptyTrashItem = new JMenuItem(ActionManager.getActionInstance(EmptyTrashAction.Descriptor.ACTION_ID, mainFrame)); // Retrieve the number of items that the trash contains, -1 if this information is not available. int itemCount = trash.getItemCount(); if (itemCount == 0) { // Disable the 'empty trash' action if the trash contains no item emptyTrashItem.setEnabled(false); } else if (itemCount > 0) { // Append the number of items to the menu item's label emptyTrashItem.setText(emptyTrashItem.getText()+" ("+itemCount+")"); } // Note: 'empty trash' is enabled if itemCount==-1 popupMenu.add(emptyTrashItem); } } return popupMenu; } //////////////////////// // Overridden methods // //////////////////////// /** * Replace the default insets to be exactly (2,2,2,2). */ @Override public Insets getInsets() { return new Insets(2, 2, 2, 2); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/statusbar/VolumeSpaceLabel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.statusbar; import com.mucommander.utils.text.SizeFormat; import com.mucommander.utils.text.Translator; import com.mucommander.ui.border.MutableLineBorder; import com.mucommander.ui.theme.*; import javax.swing.JLabel; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import static com.mucommander.ui.theme.ThemeManager.*; /** * This label displays the amount of free and/or total space on a volume. */ class VolumeSpaceLabel extends JLabel implements ThemeListener { /** SizeFormat's format used to display volume info in status bar */ private final static int VOLUME_INFO_SIZE_FORMAT = SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_SHORT | SizeFormat.INCLUDE_SPACE | SizeFormat.ROUND_TO_KB; private long freeSpace; private long totalSpace; private Color backgroundColor; private Color okColor; private Color warningColor; private Color criticalColor; private final static float SPACE_WARNING_THRESHOLD = 0.1f; private final static float SPACE_CRITICAL_THRESHOLD = 0.05f; VolumeSpaceLabel() { super(""); setHorizontalAlignment(CENTER); backgroundColor = getCurrentColor(Theme.STATUS_BAR_BACKGROUND_COLOR); //borderColor = getCurrentColor(Theme.STATUS_BAR_BORDER_COLOR); okColor = getCurrentColor(Theme.STATUS_BAR_OK_COLOR); warningColor = getCurrentColor(Theme.STATUS_BAR_WARNING_COLOR); criticalColor = getCurrentColor(Theme.STATUS_BAR_CRITICAL_COLOR); setBorder(new MutableLineBorder(getCurrentColor(Theme.STATUS_BAR_BORDER_COLOR))); ThemeManager.addCurrentThemeListener(this); } /** * Sets the new volume total and free space, and updates the label's text to show the new values and, * only if both total and free space are available (different from -1), paint a graphical representation * of the amount of free space available and set a tooltip showing the percentage of free space on the volume. * * @param totalSpace total volume space, -1 if not available * @param freeSpace free volume space, -1 if not available */ void setVolumeSpace(long totalSpace, long freeSpace) { this.freeSpace = freeSpace; this.totalSpace = totalSpace; // Set new label's text setText(getVolumeInfo()); // Set tooltip if (freeSpace < 0 || totalSpace < 0) { setToolTipText(null); // Removes any previous tooltip } else { setToolTipText((int) (100 * freeSpace / (float) totalSpace) + "%"); } repaint(); } private String getVolumeInfo() { String volumeInfo; if (freeSpace >= 0) { volumeInfo = SizeFormat.format(freeSpace, VOLUME_INFO_SIZE_FORMAT); if (totalSpace >= 0) volumeInfo += " / "+ SizeFormat.format(totalSpace, VOLUME_INFO_SIZE_FORMAT); volumeInfo = Translator.get("status_bar.volume_free", volumeInfo); } else if (totalSpace >= 0) { volumeInfo = SizeFormat.format(totalSpace, VOLUME_INFO_SIZE_FORMAT); volumeInfo = Translator.get("status_bar.volume_capacity", volumeInfo); } else { volumeInfo = ""; } return volumeInfo; } /** * Adds some empty space around the label. */ @Override public Dimension getPreferredSize() { Dimension d = super.getPreferredSize(); return new Dimension(d.width+4, d.height+2); } /** * Returns an interpolated color value, located at percent between c1 and c2 in the RGB space. * * @param c1 first color * @param c2 end color * @param percent distance between c1 and c2, comprised between 0 and 1. * @return an interpolated color value, located at percent between c1 and c2 in the RGB space. */ private static Color interpolateColor(Color c1, Color c2, float percent) { return new Color( interpolate(c1.getRed(), c2.getRed(), percent), interpolate(c1.getGreen(), c2.getGreen(), percent), interpolate(c1.getBlue(), c2.getBlue(), percent) ); } private static int interpolate(int v1, int v2, float percent) { return v1 + (int)((v2 - v1) * percent); } @Override public void paint(Graphics g) { // If free or total space is not available, this label will just be painted as a normal JLabel if (freeSpace >= 0 && totalSpace >= 0) { int width = getWidth(); int height = getHeight(); // Paint amount of free volume space if both free and total space are available float freeSpacePercentage = freeSpace/(float)totalSpace; Color c; if (freeSpacePercentage <= SPACE_CRITICAL_THRESHOLD) { c = criticalColor; } else if (freeSpacePercentage <= SPACE_WARNING_THRESHOLD) { c = interpolateColor(warningColor, criticalColor, (SPACE_WARNING_THRESHOLD-freeSpacePercentage)/SPACE_WARNING_THRESHOLD); } else { c = interpolateColor(okColor, warningColor, (1-freeSpacePercentage)/(1-SPACE_WARNING_THRESHOLD)); } g.setColor(c); int freeSpaceWidth = Math.max(Math.round(freeSpacePercentage*(float)(width-2)), 1); g.fillRect(1, 1, freeSpaceWidth + 1, height - 2); // Fill background g.setColor(backgroundColor); g.fillRect(freeSpaceWidth + 1, 1, width - freeSpaceWidth - 1, height - 2); } super.paint(g); } // Total/Free space reversed, doesn't look quite right // @Override // public void paint(Graphics g) { // // If free or total space is not available, this label will just be painted as a normal JLabel // if(freeSpace!=-1 && totalSpace!=-1) { // int width = getWidth(); // int height = getHeight(); // // // Paint amount of free volume space if both free and total space are available // float freeSpacePercentage = freeSpace/(float)totalSpace; // float usedSpacePercentage = (totalSpace-freeSpace)/(float)totalSpace; // // Color c; // if(freeSpacePercentage<=SPACE_CRITICAL_THRESHOLD) { // c = criticalColor; // } // else if(freeSpacePercentage<=SPACE_WARNING_THRESHOLD) { // c = interpolateColor(warningColor, criticalColor, (SPACE_WARNING_THRESHOLD-freeSpacePercentage)/SPACE_WARNING_THRESHOLD); // } // else { // c = interpolateColor(okColor, warningColor, (1-freeSpacePercentage)/(1-SPACE_WARNING_THRESHOLD)); // } // // g.setColor(c); // // int usedSpaceWidth = Math.max(Math.round(usedSpacePercentage*(float)(width-2)), 1); // g.fillRect(1, 1, usedSpaceWidth + 1, height - 2); // // // Fill background // g.setColor(backgroundColor); // g.fillRect(usedSpaceWidth + 1, 1, width - usedSpaceWidth - 1, height - 2); // } // // super.paint(g); // } public void fontChanged(FontChangedEvent event) {} public void colorChanged(ColorChangedEvent event) { switch (event.getColorId()) { case Theme.STATUS_BAR_BACKGROUND_COLOR: backgroundColor = event.getColor(); break; case Theme.STATUS_BAR_BORDER_COLOR: // Some (rather evil) look and feels will change borders outside of muCommander's control, // this check is necessary to ensure no exception is thrown. if (getBorder() instanceof MutableLineBorder) ((MutableLineBorder)getBorder()).setLineColor(event.getColor()); break; case Theme.STATUS_BAR_OK_COLOR: okColor = event.getColor(); break; case Theme.STATUS_BAR_WARNING_COLOR: warningColor = event.getColor(); break; case Theme.STATUS_BAR_CRITICAL_COLOR: criticalColor = event.getColor(); break; default: return; } repaint(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/CalculateDirectorySizeWorker.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.main.table.views.BaseFileTableModel; import javax.swing.SwingWorker; import java.io.IOException; import java.util.List; /** * @author Oleg Trifonov * Created on 09/01/14. */ public class CalculateDirectorySizeWorker extends SwingWorker { /** Refresh rate in milliseconds */ private static final long REFRESH_RATE_MS = 300; private final BaseFileTableModel fileTableModel; private final AbstractFile path; private final FileTable table; private long size; private long lastRefreshTime; public CalculateDirectorySizeWorker(BaseFileTableModel fileTableModel, FileTable table, AbstractFile path) { this.fileTableModel = fileTableModel; this.table = table; this.path = path; } @Override protected Long doInBackground() { size = 0; try { calcDirectorySize(path); } catch (Exception e) { e.printStackTrace(); size = -1; } return size; } @Override protected void done() { fileTableModel.addProcessedDirectory(path, table, size, true); fileTableModel.fillCellCache(table); table.repaint(); } @Override protected void process(List chunks) { fileTableModel.addProcessedDirectory(path, table, size, false); fileTableModel.fillCellCache(table); table.repaint(); table.updateSelectedFilesStatusBar(); } private void calcDirectorySize(AbstractFile path) throws IOException { if (isCancelled()) { return; } long tm = System.currentTimeMillis(); if (tm - lastRefreshTime > REFRESH_RATE_MS) { lastRefreshTime = tm; publish(size); } if (path.isSymlink() && path != this.path) { return; } AbstractFile[] childs; try { childs = path.ls(); } catch (IOException e) { return; } for (AbstractFile f : childs) { if (isCancelled()) { return; } if (f.isDirectory()) { calcDirectorySize(f); } else if (!f.isSymlink()) { size += f.getSize(); } } } public AbstractFile getFile() { return path; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/CellLabel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.ui.main.table.views.full.FileTableCellRenderer; import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import java.awt.*; /** * A custom JLabel component used by {@link FileTableCellRenderer FileTableCellRenderer} to render table cells. * *

    CellLabel is basically a faster dumbed-down JLabel which overrides some JLabel to be no-ops (see below) * and some other (setText, setIcon) to call JLabel's super methods only if value has changed since last call, * as very often values don't change from one cell to another. Some methods were borrowed from Sun's * DefaultTableCellRender implementation and are marked as much. * *

    Quote from Sun's Javadoc : The table class defines a single cell renderer and uses it as a * rubber-stamp for rendering all cells in the table; it renders the first cell, * changes the contents of that cell renderer, shifts the origin to the new location, re-draws it, and so on. *

    The standard JLabel component was not * designed to be used this way, and we want to avoid * triggering a revalidate each time the * cell is drawn. This would greatly decrease performance because the * revalidate message would be * passed up the hierarchy of the container to determine whether any other * components would be affected. So this class * overrides the validate, revalidate, * repaint, and firePropertyChange methods to be * no-ops. * * @author Maxence Bernard, Sun Microsystems */ public class CellLabel extends JLabel { /** Amount of border space on the left and right of the cell */ public static final int CELL_BORDER_WIDTH = 4; /** Amount of border space on the top and bottom of the cell */ public static final int CELL_BORDER_HEIGHT = 1; /** Empty border to give more space around cells */ private static final Border CELL_BORDER = new EmptyBorder(CELL_BORDER_HEIGHT, CELL_BORDER_WIDTH, CELL_BORDER_HEIGHT, CELL_BORDER_WIDTH); private static final Stroke DASHED_STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{1,1}, 0); private static final String DOTS = "..."; /** Last text set by the setText method */ private String lastText; /** Last icon set by the setIcon method */ private ImageIcon lastIcon; /** Last tooltip text set by the setToolTipText method */ private String lastTooltip; /** Last foreground color set by the setForeground method */ private Color lastForegroundColor; /** Last background color set by the setBackground method */ private Color lastBackgroundColor; /** Outline color (top and bottom). */ protected Color outlineColor; /** Gradient color for the background. */ private Color gradientColor; private boolean hasSeparatorLine; private int progressValue; private Color markerColor; /** * Creates a new blank CellLabel. */ public CellLabel() { setBorder(CELL_BORDER); } // - Color changing ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Overrides JComponent.setForeground to call * the super method only if the value has changed since last call. * * @param c the new foreground's color for this label */ @Override public void setForeground(Color c) { if ((c != null && !c.equals(lastForegroundColor)) || (lastForegroundColor != null && !lastForegroundColor.equals(c))) { super.setForeground(c); lastForegroundColor = c; } } /** * Overrides JComponent.setBackground to call * the super method only if the value has changed since last call. * * @param c the new background's color for this label */ @Override public void setBackground(Color c) { if ((c != null && !c.equals(lastBackgroundColor)) || (lastBackgroundColor != null && !lastBackgroundColor.equals(c))) { super.setBackground(c); lastBackgroundColor = c; gradientColor = null; } } /** * Sets the background to a gradient between the two specified colors. * @param c1 first component of the gradient. * @param c2 second component of the gradient. */ public void setBackground(Color c1, Color c2) { if (c1.equals(c2)) { setBackground(c1); } else { lastBackgroundColor = c1; gradientColor = c2; } } /** * Sets the label outline color. * @param c the new background's color for this label */ public void setOutline(Color c) { outlineColor = c; } /** * Overrides JLabel.setText to call * the super method only if the value has changed since last call. * * @param text the new text this label will display */ @Override public void setText(String text) { if ((text != null && !text.equals(lastText)) || (lastText != null && !lastText.equals(text))) { super.setText(text); lastText = text; } } /** * Overrides JLabel.setIcon to call * the super method only if the value has changed since last call. * * @param icon the new icon this label will display */ public void setIcon(ImageIcon icon) { if (icon != lastIcon) { super.setIcon(icon); lastIcon = icon; } } /** * Overrides JLabel.setToolTipText to call * the super method only if the value has changed since last call. * * @param tooltip the new tooltip this label will display */ @Override public void setToolTipText(String tooltip) { if ((tooltip != null && !tooltip.equals(lastTooltip)) || (lastTooltip != null && !lastTooltip.equals(tooltip))) { super.setToolTipText(tooltip); lastTooltip = tooltip; } } public void setupText(String text, int maxWidth) { setText(text); // If label's width is larger than the column width: // - truncate the text from the center and equally to the left and right sides, adding an ellipsis ('...') // where characters have been removed. This allows both the start and end of filename to be visible. // - set a tooltip text that will display the whole text when mouse is over the label //final TableColumn tableColumn = table.getColumnModel().getColumn(columnIndex); if (maxWidth < getPreferredSize().getWidth()) { final int tl = text.length(); final int tl2 = tl/2; String leftText = text.substring(0, tl2); String rightText = text.substring(tl2, tl); while (maxWidth < getPreferredSize().getWidth() && !leftText.isEmpty() && !rightText.isEmpty()) { // Prevents against going out of bounds final int ltl = leftText.length(); final int rtl = rightText.length(); if (ltl > rtl) { leftText = leftText.substring(0, ltl - 1); } else { rightText = rightText.substring(1, rtl); } setText(leftText + DOTS + rightText); } // Set the tool tip setToolTipText(text); } else { // Have to set it to null otherwise the defaultRender sets the tooltip text to the last one specified setToolTipText(null); } } /** * Paints the label. * @param g where to paint the label. */ @Override public void paint(Graphics g) { boolean doOutline = outlineColor != null && !outlineColor.equals(lastBackgroundColor); final int w = getWidth(); final int h = getHeight(); // Checks whether we need to paint a gradient background. if (gradientColor != null) { // Initialisation. Graphics2D g2 = (Graphics2D)g; // Allows us to use the setPaint and getPaint methods. Paint oldPaint = g2.getPaint(); // Previous Paint affected to g. // Paints the gradient background. // TODO avoid object creation in paint methods g2.setPaint(new GradientPaint(0, 0, lastBackgroundColor, 0, getHeight(), gradientColor, false)); if (doOutline) { g2.fillRect(0, 1, w, h - 2); } else { g2.fillRect(0, 0, w, h); } // Restores the graphics to its previous state. g2.setPaint(oldPaint); } if (markerColor != null) { drawMarker(g, w, h); } else { // Normal painting super.paint(g); } // If necessary, paints the outline color. if (doOutline) { paintOutline(g); } if (hasSeparatorLine) { Graphics2D g2d = (Graphics2D)g; g2d.setColor(Color.GRAY); g2d.setStroke(DASHED_STROKE); g2d.drawLine(w-1, 0, w-1, getHeight()); } // TODO improve it if (progressValue > 0) { drawProgress(g, w, h); } } private void drawMarker(Graphics g, int w, int h) { setSize(w - h, h); super.paint(g); setSize(w, h); if (gradientColor == null) { g.setColor(lastBackgroundColor); g.fillRect(w - h, 1, h, h - 2); } g.setColor(markerColor); g.fillArc(w - h, h/6, h*2/3, h*2/3, 0, 360); } private void drawProgress(Graphics g, int w, int h) { int a = -progressValue*20 % 360; int r = h - 4; if (r % 2 != 0) { r--; } g.setColor(lastForegroundColor); g.fillArc(w - r, 2, r, r, a+100, 200); g.setColor(lastBackgroundColor); int r2 = r/2; if (r2 % 2 != 0) { r2++; } int d = (r - r2) / 2; g.fillOval(w - r + d, 2 + d, r2, r2); //g.fillArc(w - r + d, 2 + d, r2, r2, a+100, 200); } protected void paintOutline(Graphics g) { g.setColor(outlineColor); g.drawLine(0, 0, getWidth(), 0); g.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1); } // - DefaultTableCellRenderer implementation ----------------------------------------- // ----------------------------------------------------------------------------------- /* * The following methods are overridden as a performance measure to * prune code-paths are often called in the case of renders * but which we know are unnecessary. Great care should be taken * when writing your own renderer to weigh the benefits and * drawbacks of overriding methods like these. */ /** * Overridden for performance reasons. */ @Override public boolean isOpaque() { // If we're not using a gradient background, the component's opaque // status is context dependant. if (gradientColor == null) { Color back = lastBackgroundColor; Component p = getParent(); if (p != null) { p = p.getParent(); } // The label does not need to be opaque if it has an opaque parent component // of the same background color. return !(back != null && p != null && back.equals(p.getBackground()) && p.isOpaque()); } // We must consider the label not to be opaque, otherwise the gradient would be overpainted by // the component's background color. return false; } /** * Overridden for performance reasons. */ @Override public void validate() {} /** * Overridden for performance reasons. */ @Override public void revalidate() {} /** * Overridden for performance reasons. */ @Override public void repaint(long tm, int x, int y, int width, int height) {} /** * Overridden for performance reasons. */ @Override public void repaint(Rectangle r) { } /** * Overridden for performance reasons. */ @Override protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { // Strings get interned... if ("text".equals(propertyName)) { super.firePropertyChange(propertyName, oldValue, newValue); } } /** * Overridden for performance reasons. */ @Override public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { } public void setHasSeparator(boolean hasSeparator) { this.hasSeparatorLine = hasSeparator; } public void setProgressValue(int progressValue) { this.progressValue = progressValue; } /** * Set label color (Mac OS X only) * @param markerColor color or null if file has no label */ public void setMarkerColor(Color markerColor) { this.markerColor = markerColor; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/Column.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.commons.file.util.FileComparator; import com.mucommander.utils.text.Translator; import lombok.Getter; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Enumerates and describes the different columns used in the {@link FileTable}. * * @author Maxence Bernard */ public enum Column { EXTENSION("extension", true, true, FileComparator.EXTENSION_CRITERION, "ToggleExtensionColumn", "SortByExtension"), NAME("name", false, true, FileComparator.NAME_CRITERION, null, "SortByName"), SIZE("size", true, true, FileComparator.SIZE_CRITERION, "ToggleSizeColumn", "SortBySize"), DATE("date", true, true, FileComparator.DATE_CRITERION, "ToggleDateColumn", "SortByDate"), PERMISSIONS("permissions", true, true, FileComparator.PERMISSIONS_CRITERION, "TogglePermissionsColumn", "SortByPermissions"), OWNER("owner", true, false, FileComparator.OWNER_CRITERION, "ToggleOwnerColumn", "SortByOwner"), GROUP("group", true, false, FileComparator.GROUP_CRITERION, "ToggleGroupColumn", "SortByGroup"); private static final Map ORDINAL_TO_ENUM_MAPPING; static { Map map = new HashMap<>(); for (Column column : Column.values()) { map.put(column.ordinal(), column); } ORDINAL_TO_ENUM_MAPPING = Collections.unmodifiableMap(map); } /** Standard minimum column width */ private final static int STANDARD_MINIMUM_WIDTH = 2 * CellLabel.CELL_BORDER_WIDTH; /** * -- GETTER -- * Returns this column's localized label. */ @Getter private final String label; private final int minimumWidth; private final boolean showByDefault; /** * -- GETTER -- * Returns the criterion used for sorting column values. */ @Getter private final int fileComparatorCriterion; private final String toggleActionId; private final String sortByActionId; Column(String labelId, boolean hasMinimumWidth, boolean showByDefault, int fileComparatorCriterion, String toggleActionId, String sortByActionId) { this.label = Translator.get(labelId); this.minimumWidth = hasMinimumWidth ? STANDARD_MINIMUM_WIDTH : 0; this.showByDefault = showByDefault; this.fileComparatorCriterion = fileComparatorCriterion; this.toggleActionId = toggleActionId; this.sortByActionId = sortByActionId; } /** * Returns this column's minimum width. * * @return this column's minimum width. */ public int getMinimumColumnWidth() { return minimumWidth; } /** * Returns true if this column should be displayed, unless configured otherwise. * * @return true if this column should be displayed, unless configured otherwise. */ public boolean showByDefault() { return showByDefault; } /** * Returns the column instance that has the specified {@link #ordinal()} value. * * @param ordinal the column's ordinal value * @return the column instance that has the specified {@link #ordinal()} value. */ public static Column valueOf(int ordinal) { return ORDINAL_TO_ENUM_MAPPING.get(ordinal); } /** * Returns the ID of the action that allows this column to be shown/hidden. * Caution: the {@link #NAME} column cannot be toggled, therefore the returned action ID is null. * * @return the ID of the action that allows this column to be shown/hidden. */ public String getToggleColumnActionId() { return toggleActionId; } /** * Returns the ID of the action that allows to sort the table by this column. * * @return the ID of the action that allows to sort the table by this column. */ public String getSortByColumnActionId() { return sortByActionId; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/FileGroupResolver.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.WildcardFileFilter; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferencesAPI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Oleg Trifonov */ public class FileGroupResolver { public static final int MAX_GROUPS = 10; private static class ResolverRecord { final int group; final WildcardFileFilter filter; ResolverRecord(int group, String mask) { this.group = group; this.filter = new WildcardFileFilter(mask); } } private final Map extensionsMap = new HashMap<>(); private final List filtersList = new ArrayList<>(); private static FileGroupResolver instance; private FileGroupResolver() { } public static FileGroupResolver getInstance() { if (instance == null) { instance = new FileGroupResolver(); instance.init(); } return instance; } /** * Reads and parse configuration */ public void init() { extensionsMap.clear(); filtersList.clear(); TcPreferencesAPI prefs = TcConfigurations.getPreferences(); for (int group = 0; group < MAX_GROUPS; group++) { String masks = prefs.getVariable(TcPreference.values()[TcPreference.FILE_GROUP_1_MASK.ordinal() + group]); if (masks == null) { continue; } String[] split = masks.split(","); for (String aSplit : split) { String mask = aSplit.trim().toLowerCase(); addMask(mask, group); } } } private void addMask(String mask, int group) { if (mask.startsWith("*.")) { String ext = mask.substring(2); if (ext.contains("*") || ext.contains("?")) { filtersList.add(new ResolverRecord(group, mask)); } else { extensionsMap.put(ext, group); } } else { filtersList.add(new ResolverRecord(group, mask)); } } /** * Returns the number of group for specified filename or -1s * @param file file * @return group number (0..9) or -1 */ public int resolve(AbstractFile file) { if (file.isDirectory() || file.isSymlink()) { return -1; } String ext = file.getExtension(); if (ext == null) { ext = ""; } else { ext = ext.toLowerCase(); } Integer group = extensionsMap.get(ext); if (group != null) { return group; } for (ResolverRecord rec : filtersList) { if (rec.filter.accept(file)) { return rec.group; } } return -1; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/FileTable.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import java.awt.*; import java.awt.event.*; import java.io.Serial; import java.util.Iterator; import java.util.WeakHashMap; import javax.swing.*; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.action.impl.*; import com.mucommander.ui.main.table.views.BaseCellRenderer; import com.mucommander.ui.main.table.views.BaseFileTableModel; import com.mucommander.ui.main.table.views.TableViewMode; import com.mucommander.ui.main.table.views.compact.CompactFileTableColumnModel; import com.mucommander.ui.main.table.views.compact.CompactFileTableModel; import com.mucommander.ui.main.table.views.full.FileTableCellRenderer; import com.mucommander.ui.main.table.views.full.FileTableColumnModel; import com.mucommander.ui.main.table.views.full.FileTableConfiguration; import com.mucommander.ui.main.table.views.full.FileTableModel; import com.mucommander.ui.text.FilePathFieldKeyListener; import com.mucommander.ui.theme.*; import com.mucommander.utils.FileIconsCache; import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jidesoft.swing.DefaultOverlayable; import com.jidesoft.swing.StyledLabelBuilder; import com.mucommander.commons.collections.Enumerator; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.desktop.DesktopManager; import com.mucommander.job.MoveJob; import com.mucommander.utils.text.CustomDateFormat; import com.mucommander.utils.text.Translator; import com.mucommander.ui.action.ActionKeymap; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.dialog.file.AbstractCopyDialog; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.dialog.file.ProgressDialog; import com.mucommander.ui.event.ActivePanelListener; import com.mucommander.ui.event.TableSelectionListener; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.TablePopupMenu; import com.mucommander.ui.quicksearch.QuickSearch; /** * A heavily modified JTable which displays a folder's contents and allows file mouse and keyboard selection, * marking and navigation. JTable provides the basics for file selection but its behavior has to be * extended to allow file marking. * * @author Maxence Bernard, Nicolas Rinaudo */ public class FileTable extends JTable implements MouseListener, MouseMotionListener, KeyListener, ActivePanelListener, ConfigurationListener, ThemeListener { private static final Logger logger = LoggerFactory.getLogger(FileTable.class); /** Minimum width for 'name' column when in automatic column sizing mode */ private final static int RESERVED_NAME_COLUMN_WIDTH = 40; /** Minimum column width when in automatic column sizing mode */ private final static int MIN_COLUMN_AUTO_WIDTH = 20; private static final int MAX_ROWS_FOR_AUTO_LAYOUT_CALCULATION = 50; private static final Dimension INTERCELL_SPACING = new Dimension(0, 0); /** Frame containing this file table. */ private final MainFrame mainFrame; /** Folder panel containing this frame. * -- GETTER -- * Returns the FolderPanel that contains this FileTable */ @Getter private final FolderPanel folderPanel; /** TableModel instance used by this JTable to get cells' values */ private BaseFileTableModel tableModel; /** TableCellRender instance used by this JTable to render cells */ private BaseCellRenderer cellRenderer; /** CellEditor used to edit filenames when clicked */ private final FilenameEditor filenameEditor; /** Contains sort-related variables * -- GETTER -- * Returns a SortInfo instance that holds information about how this table is currently sorted */ @Getter private final SortInfo sortInfo = new SortInfo(); /** Row currently selected */ private int currentRow; /** Column currently selected */ private int currentColumn; // Used when right button is pressed and mouse is dragged private boolean markOnRightClick; private int lastDraggedRow = -1; // Used by shift+Click private int lastRow; /** Allows to detect repeated key strokes of mark key (space/insert) */ private boolean markKeyRepeated; /** In case of repeated mark keystrokes, true if last row has already been marked/unmarked */ private boolean lastRowMarked; /** Timestamp of last row selection change */ private long selectionChangedTimestamp; /** Timestamp of last double click */ private long lastDoubleClickTimestamp; /** Is automatic columns sizing enabled ? * -- GETTER -- * Returns true if the auto-columns sizing is currently enabled. */ @Getter private boolean autoSizeColumnsEnabled; /** Instance of the inner class that handles quick search * -- GETTER -- * Returns the the QuickSearch inner class instance used by this FileTable */ @Getter private final QuickSearch quickSearch = new FileTableQuickSearch(); /** TableSelectionListener instances registered to receive selection change events */ private final WeakHashMap tableSelectionListeners = new WeakHashMap<>(); /** True when this table is the current or last active table in the MainFrame * -- GETTER -- * Returns true/ if this table is the active one in the MainFrame. * Being the active table doesn't necessarily mean that it currently has focus, the focus can be in some other component * of the active, or nowhere in the MainFrame if the window is not in the foreground. *

    Use * to test if the table currently has focus. */ @Getter private boolean isActiveTable; /** Timestamp of the last focus gain (in milliseconds) */ private long focusGainedTime; /** Delay in ms after which filename editor can be triggered when current row's filename cell is clicked */ private final static int EDIT_NAME_CLICK_DELAY = 500; /** Timestamp of last double click - workaround for MouseEvent.getClickCount() */ private long doubleClickTime; /** Counts the number of clicks within the double-click interval */ private int doubleClickCounter = 1; /** Interval to wait for the double-click */ private static final int DOUBLE_CLICK_INTERVAL = DesktopManager.getMultiClickInterval(); /** Wrapper of presentation adjustments for the file-table */ private final FileTableWrapperForDisplay scrollpaneWrapper; /** Table that shows the user to refresh if the location doesn't exist */ private final DefaultOverlayable overlayTable; @Getter private TableViewMode viewMode; private final FileTableConfiguration conf; /** * Number of visible rows on table. Calculated on layout and used to compact/short model */ private int pageSize; /** * Sometimes cursor gets "sticky". These variables used to detect this situation and fix it */ private int lastSelectedRow, lastSelectedCol, lastSelectedEqCnt; /** Whether to proceed with renaming the next file after renaming the selected file */ private boolean consecutiveRename; public FileTable(MainFrame mainFrame, FolderPanel folderPanel, FileTableConfiguration conf) { super(new FileTableModel(), new FileTableColumnModel(conf)); this.conf = conf; tableModel = (BaseFileTableModel)getModel(); tableModel.setSortInfo(sortInfo); tableModel.setQuickSearch(quickSearch); ThemeManager.addCurrentThemeListener(this); setAutoResizeMode(AUTO_RESIZE_NEXT_COLUMN); // Stores the mainframe and folderpanel. this.mainFrame = mainFrame; this.folderPanel = folderPanel; // Remove all default action mappings as they conflict with corresponding mu actions InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); inputMap.clear(); inputMap.setParent(null); // Initializes the table. setShowGrid(false); setIntercellSpacing(INTERCELL_SPACING); filenameEditor = new FilenameEditor(new JTextField()); setViewMode(TableViewMode.FULL); // TODO !!!!!! setTableHeader(new FileTableHeader(this)); // Initializes event listening. addMouseListener(this); folderPanel.getPanel().addMouseListener(this); addMouseMotionListener(this); addKeyListener(this); mainFrame.addActivePanelListener(this); TcConfigurations.addPreferencesListener(this); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers // instead of a custom header renderer. if (usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } // Initialize a wrapper of presentation adjustments for the file-table scrollpaneWrapper = new FileTableWrapperForDisplay(this, folderPanel, mainFrame); overlayTable = createOverlayableTable(); addFocusListener(new FocusAdapter() { @Override public void focusGained(FocusEvent e) { overlayTable.repaint(); } }); } /** * * @param mode - FULL, COMPACT or SHORT */ public synchronized void setViewMode(TableViewMode mode) { if (this.viewMode == mode) { return; } final boolean fromConstructor = this.viewMode == null; final int selectedFileIndex = fromConstructor ? 0 : getSelectedFileIndex(); final SortInfo sortInfo = getSortInfo(); this.viewMode = mode; getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); BaseFileTableModel oldModel = (BaseFileTableModel)getModel(); switch (mode) { case FULL: if (!fromConstructor) { setModel(new FileTableModel()); setColumnModel(new FileTableColumnModel(conf)); } getColumnModel().getColumn(convertColumnIndexToView(Column.NAME.ordinal())).setCellEditor(filenameEditor); setAutoSizeColumnsEnabled(TcConfigurations.getPreferences().getVariable(TcPreference.AUTO_SIZE_COLUMNS, TcPreferences.DEFAULT_AUTO_SIZE_COLUMNS)); break; case COMPACT: case SHORT: if (!fromConstructor) { CompactFileTableModel newModel = new CompactFileTableModel(mode.getColumnsCount(), pageSize > 0 ? pageSize : 10); newModel.setQuickSearch(quickSearch); setModel(newModel); setColumnModel(new CompactFileTableColumnModel(mode.getColumnsCount(), conf)); } int columnWidth = getWidth()/mode.getColumnsCount(); for (int col = 0; col < mode.getColumnsCount(); col++) { TableColumn column = getColumnModel().getColumn(col); column.setCellEditor(filenameEditor); column.setWidth(columnWidth); } break; } setRowHeight(); BaseFileTableModel newModel = (BaseFileTableModel)getModel(); newModel.setupFromModel(oldModel); tableModel = newModel; cellRenderer = mode.createCellRenderer(this); tableModel.setSortInfo(sortInfo); if (!fromConstructor) { doLayout(); try { selectFile(selectedFileIndex); } catch (Exception e) { logger.error("Could not select file: {}", selectedFileIndex, e); } } invalidate(); // sortBy(Column.NAME); //sortBy(sortInfo); sortBy(sortInfo.getCriterion()); sortBy(sortInfo.getCriterion(), sortInfo.getAscendingOrder()); // TODO restore header selection } private DefaultOverlayable createOverlayableTable() { return new DefaultOverlayable(scrollpaneWrapper) { @Serial private static final long serialVersionUID = 1L; { addOverlayComponent(createRefreshNonExistingLocationLabel()); } private JLabel createRefreshNonExistingLocationLabel() { JLabel label = StyledLabelBuilder.createStyledLabel("{Refresh to reconnect:f:darkGray}"); label.setIcon(TcAction.getStandardIcon(RefreshAction.class)); return label; } @Override public boolean requestFocusInWindow() { return scrollpaneWrapper.requestFocusInWindow(); } /** * Overridden to ensure that the table is always visible. */ @Override public void setVisible(boolean visible) { if (visible) { super.setVisible(true); } } }; } /** * Returns the FileTable as a UI component for display purpose. * The UI component is actually a JScrollPane that allows the FileTable to scroll and * responsible to set its viewing properties as needed. * * @return the FileTable as a UI component for display purpose */ public JComponent getAsUIComponent() { return overlayTable; } /** * Under Mac OS X 10.5 (Leopard) and up, sets client properties on this table's JTableHeader to indicate the current * sort criterion/column and sort order (ascending or descending). These properties allow Mac OS X/Java to render * the headers accordingly, instead of having to use a {@link FileTableHeaderRenderer custom header renderer}. * This method has no effect whatsoever on platforms other where {@link #usesTableHeaderRenderingProperties()} * returns false. */ private void setTableHeaderRenderingProperties() { if (!usesTableHeaderRenderingProperties()) { return; } JTableHeader tableHeader = getTableHeader(); if (tableHeader == null) { return; } boolean isActiveTable = isActiveTable(); // Highlights the selected column tableHeader.putClientProperty("JTableHeader.selectedColumn", isActiveTable ? convertColumnIndexToView(sortInfo.getCriterion().ordinal()) : null); // Displays an ascending/descending arrow tableHeader.putClientProperty("JTableHeader.sortDirection", isActiveTable ? sortInfo.getAscendingOrder() ? "ascending":"descending" // descending is misspelled but this is OK : null); // Note: if this table is not currently active, properties are cleared to remove the highlighting effect. // However, clearing the properties does not yield the desired behavior as it does not restore the table // header back to normal. This looks like a bug in Apple's implementation. } /** * Restores selection when focus is gained. * Note: this is not FocusListener implementation method */ private void focusGained() { focusGainedTime = System.currentTimeMillis(); if (isEditing()) { filenameEditor.filenameField.requestFocus(); } else { overlayTable.getOverlayComponents()[0].setEnabled(true); // Repaints the table to reflect the new focused state overlayTable.repaint(); } } /** * Hides selection when focus is lost. * Note: this is not FocusListener implementation method */ private void focusLost() { // Repaints the table to reflect the new focused state overlayTable.repaint(); } /** * Returns true if the current platform is capable of indicating the sort criterion and sort order * on the table headers by setting client properties, instead of using a {@link FileTableHeaderRenderer custom header renderer}. * At the moment this method returns true only under Mac OS X 10.5 (and up). * * @return true if the current platform is capable of indicating the sort criterion and sort order on the table * headers by setting client properties. */ public static boolean usesTableHeaderRenderingProperties() { return OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher(); } /** * Convenience method that returns this table's model (the one that {@link #getModel()} returns), * as a {@link FileTableModel}, to avoid having to cast it. * * @return this table's model cast as a FileTableModel */ public BaseFileTableModel getFileTableModel() { return tableModel; } /** * Returns the file that is currently selected (highlighted), null if the parent folder '..' is * currently selected. * * @return the file that is currently selected (highlighted), null if the parent folder '..' is currently selected */ public synchronized AbstractFile getSelectedFile() { return getSelectedFile(false, false); } /** * Returns the file that is currently selected (highlighted). If the currently selected file is the * parent folder '..', the parent folder is returned only if the corresponding parameter is true. * * @param includeParentFolder if true and parent folder '..' is currently selected, the parent folder * will be returned. * @return the file that is currently selected (highlighted) */ public synchronized AbstractFile getSelectedFile(boolean includeParentFolder) { return getSelectedFile(includeParentFolder, false); } /** * Returns the file that is currently selected (highlighted), wrapped in a {@link com.mucommander.commons.file.impl.CachedFile} * instance if the corresponding parameter is true. If the currently selected file is the * parent folder '..', the parent folder is returned only if the corresponding parameter is true. * * @param includeParentFolder if true and the parent folder '..' is currently selected, the parent folder file * will be returned. If false, null will be returned if the parent folder file is currently selected. * @param returnCachedFile if true, a CachedFile corresponding to the currently selected file will be returned * @return the file that is currently selected (highlighted) */ public synchronized AbstractFile getSelectedFile(boolean includeParentFolder, boolean returnCachedFile) { if (tableModel.getRowCount() == 0 || (!includeParentFolder && isParentFolderSelected())) { return null; } return returnCachedFile ? tableModel.getCachedFileAt(currentRow, currentColumn) : tableModel.getFileAt(currentRow, currentColumn); } /** * Returns selected files in a {@link FileSet}. Selected files are either the marked files or the currently selected * file if no file is currently marked. The parent folder '..' is never included in the returned set. * * @return selected files in a FileSet */ public FileSet getSelectedFiles() { FileSet selectedFiles = tableModel.getMarkedFiles(); // if no row is marked, then add selected row if there is one, and if it is not parent folder if (selectedFiles.isEmpty()) { AbstractFile selectedFile = getSelectedFile(); if (selectedFile != null) { selectedFiles.add(selectedFile); } } return selectedFiles; } /** * Returns true if the currently selected row/file is the parent folder '..' . * * @return true if the currently selected row/file is the parent folder '..' */ public boolean isParentFolderSelected() { int currentFileIndex = tableModel.getFileIndexAt(currentRow, currentColumn); return currentFileIndex == 0 && tableModel.hasParentFolder(); } /** * Returns true if the given file index is the parent folder '..' . * * @param index index of the row to test * @return true if the given row is the parent folder '..' */ public boolean isParentFolder(int index) { return index == 0 && tableModel.hasParentFolder(); } /** * Shorthand for {@link #setCurrentFolder(AbstractFile, AbstractFile[], AbstractFile)} called with no specific file * to select (default selection). * * @param folder the new current folder * @param children children of the specified folder */ public void setCurrentFolder(AbstractFile folder, AbstractFile[] children) { overlayTable.setOverlayVisible(!folder.exists()); setCurrentFolder(folder, children, null); } /** * Changes the current folder to the specified one and refreshes the table to reflect the folder's contents. * The current file selection is also updated, with the following behavior: *

      *
    • If filetoSelect is not null, the specified file becomes the currently selected * file, if it can be found in the new current folder. Previously marked files are cleared.
    • *
    • If it is null: *
        *
      • if the current folder is the same as the previous one, the currently selected file and marked files * remain the same, provided they still exist.
      • *
      • if the new current folder is the parent of the previous one, the previous current folder is selected.
      • *
      • in any other case, the first row is selected, whether it be the parent directory ('..') or the first * file of the current folder if it has no parent.
      • *
      *
    • *
    * *

    * This method returns only when the folder has actually been changed and the table refreshed.
    * Important: This method should only be called by {@link FolderPanel} and in any case MUST be synchronized * externally to ensure this method is never called concurrently by different threads. * * @param folder the new current folder * @param children children of the specified folder * @param fileToSelect the file to select, null for the default selection. */ public void setCurrentFolder(AbstractFile folder, AbstractFile[] children, AbstractFile fileToSelect) { // Stop quick search in case it was being used before folder change if (!isQuickSearchMatchesFirst() || !folder.equals(tableModel.getCurrentFolder())) { quickSearch.stop(); } AbstractFile currentFolder = folderPanel.getCurrentFolder(); // If we're refreshing the current folder, save the current selection and marked files // in order to restore them properly. FileSet markedFiles = null; if (currentFolder != null && folder.equalsCanonical(currentFolder)) { markedFiles = tableModel.getMarkedFiles(); if (fileToSelect == null) { fileToSelect = getSelectedFile(); } } // If we're navigating to the current folder's parent, we select the current folder. else if (fileToSelect == null) { if (tableModel.hasParentFolder() && folder.equals(tableModel.getParentFolder())) { fileToSelect = currentFolder; } } // Changes the current folder in the swing thread to make sure that repaints cannot // happen in the middle of the operation - this is used to prevent flickering, badly // refreshed frames and such unpleasant graphical artifacts. Runnable folderChangeThread = new FolderChangeThread(folder, children, markedFiles, fileToSelect); // Wait for the getTask to complete, so that we return only when the folder has actually been changed and the // table updated to reflect the new folder. // Note: we use a wait/notify scheme rather than calling SwingUtilities#invokeAndWait to avoid deadlocks // due to AWT thread synchronization issues. synchronized(folderChangeThread) { SwingUtilities.invokeLater(folderChangeThread); while(true) { try { // FolderChangeThread will call notify when done folderChangeThread.wait(); break; } catch (InterruptedException e) { // will keep looping } } } } /** * Sets row height based on current cell's font and border, revalidates and repaints this JTable. */ private void setRowHeight() { // JTable.setRowHeight() revalidates and repaints the JTable. // Note that it's important here to use the cell editor's font rather than the cell renderer's: if this method is called // as a result to a font changed event, we do not know which class' fontChanged event will be called first. setRowHeight(2*CellLabel.CELL_BORDER_HEIGHT + Math.max(getFontMetrics(filenameEditor.filenameField.getFont()).getHeight(), (int)FileIcons.getIconDimension().getHeight())); // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called :/ // setRowHeight(Math.max(getFontMetrics(cellRenderer.getCellFont()).getHeight()+cellRenderer.CELL_BORDER_HEIGHT, editorRowHeight)); } /** * Enables/disables auto-columns sizing, which automatically resizes columns to fit the table's width. * * @param enabled true to enable auto-columns sizing, false to disable it */ public void setAutoSizeColumnsEnabled(boolean enabled) { this.autoSizeColumnsEnabled = enabled; if (autoSizeColumnsEnabled) { getTableHeader().setResizingAllowed(false); // Will invoke doLayout() resizeAndRepaint(); } else { getTableHeader().setResizingAllowed(true); } } /** * Controls whether folders are displayed first in this FileTable or mixed with regular files. * After calling this method, the table is refreshed to reflect the change. * * @param enabled if true, folders are displayed before regular files. If false, files are mixed with directories. */ public void setFoldersFirst(boolean enabled) { if (sortInfo.getFoldersFirst() != enabled) { sortInfo.setFoldersFirst(enabled); sortTable(); } } /** * Controls whether folders are sorted always alphabetical (if displayed first in this FileTable) * After calling this method, the table is refreshed to reflect the change. * * @param enabled if true, folders are sorted alphabetical */ public void setFoldersAlwaysAlphabetical(boolean enabled) { if (sortInfo.getFoldersAlwaysAlphabetical() != enabled) { sortInfo.setFoldersAlwaysAlphabetical(enabled); sortTable(); } } /** * Controls whether quick search matches are displayed first in this FileTable or mixed with other files. */ public void setShowMatchesFirst(boolean enabled) { sortInfo.setQuickSearchMatchesFirst(enabled); } /** * Selects the given file, does nothing if this table does not contain the file. * * @param file the file to select * @return true if success */ public boolean selectFile(AbstractFile file) { int index = tableModel.getFileIndex(file); if (index >= 0) { selectFile(index); return true; } return false; } /** * Makes the given row the currently selected one. * * @param index index of the row to select for full model or index of file for compact model */ public void selectFile(int index) { if (index < 0) { index = 0; } if (viewMode == TableViewMode.FULL) { changeSelection(index, 0, false, false); } else { CompactFileTableModel compactModel = (CompactFileTableModel)getModel(); final int cols = compactModel.getColumnCount(); final int rows = compactModel.getRowCount(); if (index < rows*cols) { compactModel.setOffset(0); changeSelection(index % rows, index / rows, false, false); } else { compactModel.setOffset((index / (rows * cols)) * rows * cols); int index0 = index % (rows*cols); changeSelection(index0 % rows, index0 / rows, false, false); } } } /** * Equivalent to calling {@link #setFileMarked(int, boolean, boolean)} with repaint enabled. * * @param index index of the file to mark/unmark * @param marked true to mark the file, false to unmark it */ private void setFileMarked(int index, boolean marked) { setFileMarked(index, marked, true); } /** * Sets the given row as marked/unmarked in the table model, repaints the row to reflect the change, * and notifies registered {@link com.mucommander.ui.event.TableSelectionListener} that the files currently marked * on this FileTable have changed. * *

    This method has no effect if the row corresponds to the parent folder row '..' . * * @param index index of the file to mark/unmark * @param marked true to mark the file, false to unmark it * @param repaint true to repaint the row after it has been marked/unmarked */ private void setFileMarked(int index, boolean marked, boolean repaint) { if (isParentFolder(index)) { return; } tableModel.setFileMarked(index, marked); if (repaint) { int row = tableModel.getFileRow(index); repaintRow(row); } // Notify registered listeners that currently marked files have changed on this FileTable fireMarkedFilesChangedEvent(); } /** * Equivalent to calling {@link #setFileMarked(AbstractFile, boolean, boolean)} with repaint enabled. * * @param file file to mark/unmark * @param marked true to mark the file, false to unmark it */ public void setFileMarked(AbstractFile file, boolean marked) { setFileMarked(file, marked, true); } /** * Sets the given file as marked/unmarked in the table model, repaints the corresponding row to reflect the change, * and notifies registered {@link com.mucommander.ui.event.TableSelectionListener} that currently marked files * have changed on this FileTable. * * @param file file to mark/unmark * @param marked true to mark the file, false to unmark it * @param repaint true to repaint the file's row after it has been marked/unmarked */ public void setFileMarked(AbstractFile file, boolean marked, boolean repaint) { int index = tableModel.getFileIndex(file); if (index >= 0) { setFileMarked(index, marked, repaint); } } /** * Marks or unmarks the current selected file (current row) and advance current row to the next one, * with the following exceptions: *

      *
    • if quick search is active, this method does nothing *
    • if '..' file is selected, file is not marked but current row is still advanced to the next one *
    • if the {@link MarkSelectedFileAction} key event is repeated and the last file has already * been marked/unmarked since the key was last released, the file is not marked in order to avoid * marked/unmarked flaps when the mark key is kept pressed. *
    * * @see MarkSelectedFileAction */ public void markSelectedFile() { // Avoids repeated mark/unmark on last row: return if last row has already been marked/unmarked by repeated mark key strokes if (markKeyRepeated && lastRowMarked) { return; } final int fileIndex = getFileTableModel().getFileIndexAt(currentRow, currentColumn); // Don't mark '..' file but select next row if (!isParentFolderSelected()) { setFileMarked(fileIndex, !tableModel.isFileMarked(currentRow, currentColumn)); } // Changes selected item to the next one if (fileIndex < tableModel.getFilesCount()-1) { selectFile(fileIndex + 1); } else if (!lastRowMarked) { // Need an explicit repaint to repaint the last row since select row is not called repaintRow(currentRow); // Last row has been marked/unmarked, value will be reset by keyReleased() lastRowMarked = true; } // Any further mark key events will be considered as repeated until keyReleased() has been called markKeyRepeated = true; } /** * Marks or unmarks a range of rows, delimited by the provided start row index and end row index (inclusive). * End row index can be lower, greater or equals to the start row. * * @param start index of the first file to repaint * @param end index of the last file to mark, can be lower, greater or equals to startRow * @param marked if true, the rows will be marked, unmarked otherwise */ public void setRangeMarked(int start, int end, boolean marked) { tableModel.setRangeMarked(start, end, marked); int startRow = tableModel.getFileRow(start); int endRow = tableModel.getFileRow(end); if (viewMode == TableViewMode.FULL) { repaintRange(startRow, endRow); } else { repaintRange(0, tableModel.getRowCount()-1); } fireMarkedFilesChangedEvent(); } /** * Repaints the given row. * * @param row the row to repaint */ private void repaintRow(int row) { repaint(0, row * getRowHeight(), getWidth(), rowHeight); } /** * Repaints a range of rows, delimited by the provided start row index and end row index (inclusive). * End row index can be lower, greater or equals to the start row. * * @param startRow index of the first row to repaint * @param endRow index of the last row to repaint, can be lower, greater or equals to startRow */ private void repaintRange(int startRow, int endRow) { int rowHeight = getRowHeight(); repaint(0, Math.min(startRow, endRow)*rowHeight, getWidth(), (Math.abs(startRow-endRow)+1)*rowHeight); } /** * Returns the number of rows that a page down/page up action should jump, based on this FileTable's viewport size. * The returned number doesn't take into account the number of rows available in this FileTable. * * @return the number of rows that a page down/page up action should jump */ public int getPageRowIncrement() { return getScrollableBlockIncrement(getVisibleRect(), SwingConstants.VERTICAL, 1)/getRowHeight() - 1; } /** * Sorts this FileTable by the given sort criterion, order and 'folders first' value. The criterion and ascending * order will be ignored if the corresponding column is not currently visible, but the 'folders first' value will * still be taken into account. * * @param criterion the sort criterion, see {@link Column} for possible values * @param ascending true for ascending order, false for descending order * @param foldersFirst if true, folders are displayed before regular files. If false, files are mixed with directories. */ private void sortBy(Column criterion, boolean ascending, boolean foldersFirst, boolean foldersAlwaysAlphabetical) { // If we're not changing the current sort values, abort. if (criterion == sortInfo.getCriterion() && ascending == sortInfo.getAscendingOrder() && foldersFirst == sortInfo.getFoldersFirst() && foldersAlwaysAlphabetical == sortInfo.getFoldersAlwaysAlphabetical() ) { return; } sortInfo.setFoldersFirst(foldersFirst); sortInfo.setFoldersAlwaysAlphabetical(foldersAlwaysAlphabetical); // Ignore the sort criterion and order if the corresponding column is not visible if (isColumnVisible(criterion)) { sortInfo.setCriterion(criterion); sortInfo.setAscendingOrder(ascending); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if (usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } // Repaint header getTableHeader().repaint(); } // Sorts table while keeping the current file selection sortTable(); } /** * Calls {@link #sortBy(Column, boolean, boolean, boolean)} with the sort information contained in the given {@link SortInfo}. * * @param sortInfo the information to use to sort this table. */ public void sortBy(SortInfo sortInfo) { sortBy(sortInfo.getCriterion(), sortInfo.getAscendingOrder(), sortInfo.getFoldersFirst(), sortInfo.getFoldersAlwaysAlphabetical()); } /** * Sorts this FileTable by the given sort criterion and order. The column corresponding to the specified criterion * has to be visible when this method is called. If it isn't, this method won't have any effect. * * @param criterion the sort criterion, see {@link Column} for possible values * @param ascending true for ascending order, false for descending order */ public void sortBy(Column criterion, boolean ascending) { sortBy(criterion, ascending, sortInfo.getFoldersFirst(), sortInfo.getFoldersAlwaysAlphabetical()); } /** * Sorts this FileTable by the given sort criterion. If the criterion is already the current one, the sort order * (ascending or descending) will be reversed. * * @param criterion the sort criterion, see {@link Column} for possible values */ public void sortBy(Column criterion) { if (criterion == sortInfo.getCriterion()) { reverseSortOrder(); return; } sortBy(criterion, sortInfo.getAscendingOrder()); } /** * Convenience method that returns this table's javax.swing.table.TableColumnModel cast as a * {@link FileTableColumnModel}. * This method return not null for full table view mode * * @return this table's TableColumnModel cast as a FileTableColumnModel */ private FileTableColumnModel getFileTableColumnModel() { return getColumnModel() instanceof FileTableColumnModel ? (FileTableColumnModel)getColumnModel() : null; } /** * Convenience method that returns this table's javax.swing.table.TableColumnModel cast as a * {@link CompactFileTableColumnModel}. * This method return not null for compact table view mode * * @return this table's TableColumnModel cast as a CompactFileTableColumnModel */ private CompactFileTableColumnModel getCompactFileTableColumnModel() { return getColumnModel() instanceof CompactFileTableColumnModel ? (CompactFileTableColumnModel)getColumnModel() : null; } @Override public void setColumnModel(@NotNull TableColumnModel columnModel) { // super.setColumnModel() must be called BEFORE the methods below super.setColumnModel(columnModel); if (filenameEditor != null) { if (viewMode == TableViewMode.FULL) { columnModel.getColumn(convertColumnIndexToView(Column.NAME.ordinal())).setCellEditor(filenameEditor); } else { for (int i = 0; i < columnModel.getColumnCount(); i++) { columnModel.getColumn(i).setCellEditor(filenameEditor); } } } // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if (usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } } /** * Returns true if the specified column is currently visible. * * @param column column, see {@link Column} for possible values * @return true if the specified column is currently visible */ public boolean isColumnVisible(Column column) { FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); return fileTableColumnModel == null || fileTableColumnModel.isColumnVisible(column); } /** * Returns true if the given column can be displayed given the current folder. Certain columns such as * {@link Column#OWNER} and {@link Column#GROUP} can be displayed only if current folder's files are capable * of supplying this information. * Note that the return value does not take into account the column's current enabled state. * * @param column column, see {@link Column} for possible values * @return true if the given column can be displayed given the current folder */ public boolean isColumnDisplayable(Column column) { // Check this against the children's file implementation whenever possible: certain file implementations may // return different values for the current folder than for its children. For instance, this is the case for file // protocols that have a special file implementation for the root folder (s3 is one). AbstractFile file = getFileTableModel().getFileAt(0); if (file == null) { file = folderPanel.getCurrentFolder(); } // The Owner and Group columns are displayable only if current folder has this information return switch (column) { case OWNER -> file.canGetOwner(); case GROUP -> file.canGetGroup(); default -> true; }; } /** * Updates the visibility of all columns based on their enabled state, and for conditional columns on the * current folder. */ public void updateColumnsVisibility() { FileTableColumnModel columnModel = getFileTableColumnModel(); if (columnModel != null) { // Full mode for (Column c : Column.values()) { columnModel.setColumnVisible(c, columnModel.isColumnEnabled(c) && isColumnDisplayable(c)); } } } /** * Returns true if the specified column is enabled. * * @param column column, see {@link Column} for possible values * @return true if the specified column is enabled */ public boolean isColumnEnabled(Column column) { FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); return fileTableColumnModel == null || fileTableColumnModel.isColumnEnabled(column); } /** * Enables/disables the specified column. Disabling a column will make it invisible. Enabling a column will make it * visible only if the column can be displayed. See {@link #isColumnDisplayable(Column)} for more information about * this. * *

    If the current sort criterion corresponds to the specified column and this * column is disabled, the sort criterion will be reset to {@link Column#NAME} to prevent the table from being * sorted by an invisible column/criterion. * * @param column column, see {@link Column} for possible values * @param enabled true to enable the column, false to disable it. */ public void setColumnEnabled(Column column, boolean enabled) { FileTableColumnModel columnModel = getFileTableColumnModel(); if (columnModel == null) { return; } // Full view mode columnModel.setColumnEnabled(column, enabled); // Update the visibility of the column updateColumnsVisibility(); // The column may be the current 'sort by' criterion and may have become invisible. // If that is the case, change the criterion to NAME. if (sortInfo.getCriterion() == column && !columnModel.isColumnVisible(column)) { sortBy(Column.NAME); } } public int getColumnPosition(Column column) { FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); if (fileTableColumnModel == null) { return 0; } return fileTableColumnModel.getColumnPosition(column.ordinal()); } /** * Reverses the current sort order, from ascending to descending or vice-versa. */ public void reverseSortOrder() { boolean newSortOrder = !sortInfo.getAscendingOrder(); sortInfo.setAscendingOrder(newSortOrder); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if (usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } // Repaint header getTableHeader().repaint(); // Sorts table while keeping current file selected sortTable(); } /** * Turns on the filename editor on current row. */ public void editCurrentFilename() { // Forces CommandBar to return to its normal state as modify key release event is never fired to FileTable mainFrame.getCommandBar().setAlternateActionsMode(false); // Temporarily enable editing tableModel.setNameColumnEditable(true); // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called :/ // Adjust row height to match filename editor's height // setRowHeight(row, (int)filenameEditor.filenameField.getPreferredSize().getHeight()); // Starts editing clicked cell's name column if (viewMode == TableViewMode.FULL) { editCellAt(currentRow, convertColumnIndexToView(Column.NAME.ordinal())); } else { editCellAt(currentRow, currentColumn); } getFolderPanel().getFolderChangeMonitor().setPaused(true); // Saves current/editing row in the filename editor and requests focus on the text field filenameEditor.notifyEditing(currentRow, currentColumn); // Disable editing tableModel.setNameColumnEditable(false); } /** * Sorts this FileTable and repaints it. Marked files and selected file will remain the same, only * their position will have changed in the newly sorted table. */ private synchronized void sortTable() { // Save currently selected file AbstractFile selectedFile = tableModel.getFileAt(currentRow, currentColumn); // Sort table, doesn't affect marked files tableModel.sortRows(); // Restore selected file selectFile(selectedFile); // Repaint table repaint(); } /** * Adds the given TableSelectionListener to the list of listeners that are registered to receive * notifications when the currently selected file changes. * * @param listener the TableSelectionListener instance to add to the list of registered listeners. */ public void addTableSelectionListener(TableSelectionListener listener) { tableSelectionListeners.put(listener, null); } /** * Removes the given TableSelectionListener from the list of listeners that are registered to receive * notifications when the currently selected file changes. * The listener will not receive any further notification after this method has been called * (or soon after if events are pending). * * @param listener the TableSelectionListener instance to add to the list of registered listeners. */ public void removeTableSelectionListener(TableSelectionListener listener) { tableSelectionListeners.remove(listener); } /** * Notifies all registered listeners that the currently selected file has changed on this FileTable. */ private void fireSelectedFileChangedEvent() { for (TableSelectionListener listener : tableSelectionListeners.keySet()) { listener.selectedFileChanged(this); } } /** * Notifies all registered listeners that the currently marked files have changed on this FileTable. */ public void fireMarkedFilesChangedEvent() { for (TableSelectionListener listener : tableSelectionListeners.keySet()) { listener.markedFilesChanged(this); } } private FontMetrics cellFontMetrics; private Font lastCellFont; private int dirStringWidth; private int wwwStringWidth; private void doAutoLayout(boolean respectSize) { final AbstractFile currentFolder = getFolderPanel().getCurrentFolder(); if (cellFontMetrics == null || lastCellFont != FileTableCellRenderer.getCellFont()) { lastCellFont = FileTableCellRenderer.getCellFont(); cellFontMetrics = getFontMetrics(lastCellFont); final int dirStringWidth1 = cellFontMetrics.stringWidth(FileTableModel.DIRECTORY_SIZE_STRING); final int dirStringWidth2 = cellFontMetrics.stringWidth(SizeFormat.format(1024 * 1024 * 555, FileTableModel.getSizeFormat())); // some big value with big string-length final int dirStringWidth3 = cellFontMetrics.stringWidth(SizeFormat.format(1016 * 1024, FileTableModel.getSizeFormat())); // some other big value with big string-length dirStringWidth = Math.max(Math.max(dirStringWidth1, dirStringWidth2), dirStringWidth3); wwwStringWidth = cellFontMetrics.stringWidth("WWWW"); } // final FontMetrics fm = getFontMetrics(FileTableCellRenderer.getCellFont()); pageSize = getParent().getSize().height / getRowHeight(); int remainingWidth = getSize().width - RESERVED_NAME_COLUMN_WIDTH; FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); if (fileTableColumnModel == null) { respectSize = false; } // Calculate columns width in full view mode if (viewMode != TableViewMode.FULL) { return; } Iterator columns = respectSize ? new Enumerator<>(getColumnModel().getColumns()) : fileTableColumnModel.getAllColumns(); TableColumn nameColumn = null; while (columns.hasNext()) { TableColumn column = columns.next(); Column c = Column.valueOf(column.getModelIndex()); if (c == Column.NAME) { nameColumn = column; } else { int columnWidth; if (c == Column.EXTENSION) { columnWidth = (int) FileIcons.getIconDimension().getWidth(); } else if (c == Column.DATE) { String val = currentFolder != null ? CustomDateFormat.format(currentFolder.getLastModifiedDate()) : ""; columnWidth = Math.max(MIN_COLUMN_AUTO_WIDTH, cellFontMetrics.stringWidth(val)); columnWidth *= 1.1; } else if (c == Column.SIZE) { long size = 1000 * 1024 * 1024; String val = SizeFormat.format(size, BaseFileTableModel.getSizeFormat()); columnWidth = Math.max(dirStringWidth, cellFontMetrics.stringWidth(val)); columnWidth *= 1.1; } else if (c == Column.PERMISSIONS) { try { String permissionStr = currentFolder != null ? currentFolder.getPermissionsString() : "----"; columnWidth = Math.max(wwwStringWidth, cellFontMetrics.stringWidth(permissionStr)); } catch (Exception e) { columnWidth = wwwStringWidth; } } else { columnWidth = MIN_COLUMN_AUTO_WIDTH; int rowCount = getModel().getRowCount(); for (int rowNum = 0; rowNum < rowCount; rowNum++) { if (rowNum >= MAX_ROWS_FOR_AUTO_LAYOUT_CALCULATION) { break; } String val = (String)getModel().getValueAt(rowNum, column.getModelIndex()); int stringWidth = val == null ? 0 : c == Column.SIZE ? dirStringWidth : cellFontMetrics.stringWidth(val); columnWidth = Math.max(columnWidth, stringWidth); } } if (respectSize) { columnWidth = Math.min(columnWidth, remainingWidth); } columnWidth += 2 * CellLabel.CELL_BORDER_WIDTH; column.setWidth(columnWidth); // Update subtotal remainingWidth -= columnWidth; if (remainingWidth < 0) { remainingWidth = 0; } } } if (nameColumn != null) { nameColumn.setWidth(remainingWidth + RESERVED_NAME_COLUMN_WIDTH); } } private void doStaticLayout() { final int width = getSize().width; pageSize = getParent().getSize().height / getRowHeight(); // If ve have compact layout type then just use average column width if (getFileTableColumnModel() == null) { int columns = getColumnModel().getColumnCount(); int columnWidth = width / getColumnModel().getColumnCount(); for (int i = 0; i < columns; i++) { getColumnModel().getColumn(i).setWidth(columnWidth); } // update model if need BaseFileTableModel model = getFileTableModel(); if (model instanceof CompactFileTableModel compactModel) { if (compactModel.getVisibleRows() != pageSize) { compactModel.setVisibleRows(pageSize); SwingUtilities.invokeLater(this::invalidate); } } final AbstractFile selectedFile = getSelectedFile(); ((CompactFileTableModel) getModel()).setVisibleRows(pageSize); // Need to restore file selection SwingUtilities.invokeLater(() -> { if (selectedFile == null && getParent() != null) { try { selectFile(0); } catch (Exception e) { e.printStackTrace(); } } else { selectFile(selectedFile); } }); invalidate(); return; } // Calculate columns width for full panel view type if (width - getColumnModel().getTotalColumnWidth() == 0) { return; } TableColumn nameColumn = getColumnModel().getColumn(convertColumnIndexToView(Column.NAME.ordinal())); nameColumn.setWidth(Math.max(nameColumn.getWidth() + width, RESERVED_NAME_COLUMN_WIDTH)); } /** * Overrides JTable's doLayout() method to use a custom column layout (if auto-column sizing is enabled). */ @Override public void doLayout() { FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); if (!autoSizeColumnsEnabled) { if (getTableHeader().getResizingColumn() != null) { super.doLayout(); } else if (fileTableColumnModel != null && !fileTableColumnModel.wereColumnSizesSet()) { doAutoLayout(false); } else { doStaticLayout(); } } else { // Custom layout doAutoLayout(true); } // Ensures that current row is visible (within current viewport), and if not adjusts viewport to center it Rectangle visibleRect = getVisibleRect(); final Rectangle cellRect = getCellRect(currentRow, 0, false); if (cellRect.y < visibleRect.y || cellRect.y + getRowHeight( ) >visibleRect.y + visibleRect.height) { if (scrollpaneWrapper != null) { // At this point JViewport is not yet aware of the new FileTable dimensions, calling setViewPosition // would not work. Instead, SwingUtilities.invokeLater is used to delay the call after all pending // UI events (including JViewport revalidation) have been processed. SwingUtilities.invokeLater(() -> scrollpaneWrapper.getViewport().setViewPosition(new Point(0, Math.max(0, cellRect.y-scrollpaneWrapper.getHeight()/2-getRowHeight()/2))) ); } } } /** * Method overridden to return a custom TableCellRenderer. */ @Override public TableCellRenderer getCellRenderer(int row, int column) { return cellRenderer; } /** * Method overridden to consume keyboard events when quick search is active or when a row is being editing * in order to prevent registered actions from being fired. */ @Override protected boolean processKeyBinding(KeyStroke ks, KeyEvent ke, int condition, boolean pressed) { if (quickSearch.isActive() || isEditing()) { return true; } // if(ActionKeymap.isKeyStrokeRegistered(ks)) // return false; return super.processKeyBinding(ks, ke, condition, pressed); } /** * Overrides the changeSelection method from JTable to track the current selected row (the one that has focus) * and fire a {@link com.mucommander.ui.event.TableSelectionListener#selectedFileChanged(FileTable)} event * to registered listeners. */ @Override public void changeSelection(int row, int column, boolean toggle, boolean extend) { // For shift+click lastRow = currentRow; int lastColumn = currentColumn; currentRow = row; currentColumn = column; super.changeSelection(row, column, toggle, extend); // Sometimes cursor gets "sticky". // Here we detect this case and fix it by generating RuntimeException if (getSelectedRow() == lastSelectedRow && getSelectedColumn() == lastSelectedCol) { lastSelectedEqCnt++; if (lastSelectedEqCnt == 10) { logger.warn("Sticky cursor!"); throw new RuntimeException("Sticky cursor!"); /* at com.mucommander.ui.main.table.FileTable.changeSelection(FileTable.java:1432) at javax.swing.plaf.basic.BasicTableUI$Handler.mouseDragged(BasicTableUI.java:1253) at javax.swing.plaf.basic.BasicTableUI$MouseInputHandler.mouseDragged(BasicTableUI.java:818) at java.awt.AWTEventMulticaster.mouseDragged(AWTEventMulticaster.java:319) at java.awt.AWTEventMulticaster.mouseDragged(AWTEventMulticaster.java:319) at java.awt.AWTEventMulticaster.mouseDragged(AWTEventMulticaster.java:319) at java.awt.Component.processMouseMotionEvent(Component.java:6573) at javax.swing.JComponent.superProcessMouseMotionEvent(JComponent.java:3348) at javax.swing.Autoscroller.actionPerformed(Autoscroller.java:176) at javax.swing.Timer.fireActionPerformed(Timer.java:313) at javax.swing.Timer$DoPostEvent.run(Timer.java:245) at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311) at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:749) at java.awt.EventQueue.access$500(EventQueue.java:97) at java.awt.EventQueue$3.run(EventQueue.java:702) at java.awt.EventQueue$3.run(EventQueue.java:696) at java.security.AccessController.doPrivileged(Native Method) at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75) at java.awt.EventQueue.dispatchEvent(EventQueue.java:719) at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201) at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93) at java.awt.EventDispatchThread.run(EventDispatchThread.java:82) */ } } else { lastSelectedEqCnt = 0; lastSelectedRow = row; lastSelectedCol = column; } // If file changed if (currentRow != lastRow || (currentColumn != lastColumn && viewMode != TableViewMode.FULL)) { // Update selection changed timestamp selectionChangedTimestamp = System.currentTimeMillis(); // notify registered TableSelectionListener instances that the currently selected file has changed fireSelectedFileChangedEvent(); } // // Don't refresh status bar if up, down, space or insert key is pressed (repeated key strokes). // // Status bar will be refreshed whenever the key is released. // // We need this limit because refreshing status bar takes time. // if(downKeyDown || upKeyDown || spaceKeyDown || insertKeyDown) // return; } @Override public Dimension getPreferredSize() { Container parentComp = getParent(); // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called :/ /* int height; if(isEditing()) height = (tableModel.getRowCount()-1)*getRowHeight() + editorRowHeight; else height = tableModel.getRowCount()*getRowHeight(); return new Dimension(parentComp==null?0:parentComp.getWidth(), height); */ return new Dimension(parentComp == null ? 0 : parentComp.getWidth(), tableModel.getRowCount()*getRowHeight()); } @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } @Override public String toString() { return getClass().getName()+"@"+hashCode() +" currentFolder="+folderPanel.getCurrentFolder()+" hasFocus="+hasFocus()+" currentRow="+currentRow; } @Override public void mouseClicked(MouseEvent e) { // Discard mouse events while in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } Object source = e.getSource(); // Under Linux with GNOME and KDE, Java does not honour the multi/double-click speed preferences // (see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5076635) and defaults to a 200ms double-click // interval, which for most people is too low. Therefore, we cannot rely on MouseEvent#getClickCount() and // MouseEvent#getMultiClickInterval() to always work properly and have to detect double-clicks using the // proper system multi-click interval returned by DefaultManager#getMultiClickInterval(). if ((System.currentTimeMillis() - doubleClickTime) < DOUBLE_CLICK_INTERVAL && selectionChangedTimestamp < doubleClickTime) { if (doubleClickCounter == 1) { doubleClickCounter = 2; // increase only once e.consume(); // and make sure this event is not sent anywhere else } } else { // reset the counter for the double click count doubleClickTime = System.currentTimeMillis(); doubleClickCounter = 1; } // If one of the table cells was left clicked... if (source == this && DesktopManager.isLeftMouseButton(e)) { // Clicking on the selected row's ... : // - 'name' label triggers the filename editor // - 'date' label triggers the change date dialog // - 'permissions' label triggers the change permissions dialog, only if permissions can be changed // Timestamp check is used to make sure that this mouse click did not trigger current row selection if (doubleClickCounter == 1 && (System.currentTimeMillis() - selectionChangedTimestamp) > EDIT_NAME_CLICK_DELAY) { int clickX = e.getX(); Point p = new Point(clickX, e.getY()); final int row = rowAtPoint(p); final int viewColumn = columnAtPoint(p); final Column column = viewMode == TableViewMode.FULL ? Column.valueOf(convertColumnIndexToModel(viewColumn)) : null; final boolean isNameColumn = column == null || column == Column.NAME; final boolean isDateColumn = column == Column.DATE; final boolean isPermissionColumn = column == Column.PERMISSIONS; // Test if the clicked row is current row, if column is name column, and if current row is not '..' file if (row == currentRow && !isParentFolderSelected() && (isNameColumn || isDateColumn || isPermissionColumn)) { // Test if clicked point is inside the label and abort if not FontMetrics fm = getFontMetrics(FileTableCellRenderer.getCellFont()); int labelWidth; if (column != null) { labelWidth = fm.stringWidth((String) tableModel.getValueAt(row, column.ordinal())); } else { labelWidth = fm.stringWidth((String) tableModel.getValueAt(row, viewColumn)); } int columnX = (int) getTableHeader().getHeaderRect(viewColumn).getX(); if (clickX < columnX+CellLabel.CELL_BORDER_WIDTH || clickX > columnX+labelWidth+CellLabel.CELL_BORDER_WIDTH) { return; } // The following test ensures that this mouse click is not the one that gave the focus to this table. // Not checking for this would cause a single click on the inactive table's current row to trigger // the filename/date/permission editor if (hasFocus() && System.currentTimeMillis() - focusGainedTime > 100) { // create a new thread and sleep long enough to ensure that this click was not the first of a double click new Thread(() -> { try { Thread.sleep(800); } catch (InterruptedException ignore) {} // Do not execute this block (cancel editing) if: // - a double click was made in the last second // - current row changed // - isEditing() is true which could happen if multiple clicks were made if ((System.currentTimeMillis() - lastDoubleClickTimestamp) > 1000 && row == currentRow) { if (isNameColumn) { if (!isEditing()) { editCurrentFilename(); } } else if (isDateColumn) { ActionManager.performAction(ChangeDateAction.Descriptor.ACTION_ID, mainFrame); } else if (isPermissionColumn) { if (getSelectedFile().getChangeablePermissions().getIntValue() != 0) { ActionManager.performAction(ChangePermissionsAction.Descriptor.ACTION_ID, mainFrame); } } } }).start(); } } } // Double-clicking on a row opens the file/folder else if (doubleClickCounter == 2) { // Note: user can double-click multiple times this.lastDoubleClickTimestamp = System.currentTimeMillis(); ActionManager.performAction(e.isShiftDown() ? OpenNativelyAction.Descriptor.ACTION_ID : OpenAction.Descriptor.ACTION_ID , mainFrame); } } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { // Discard mouse events while in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } if (e.getSource() != this) { return; } // Right-click brings a contextual popup menu if (DesktopManager.isRightMouseButton(e)) { // Find the row that was right-clicked int x = e.getX(); int y = e.getY(); Point point = new Point(x, y); int clickedRow = rowAtPoint(point); int clickedCol = columnAtPoint(point); // Does the row correspond to the parent '..' folder ? boolean parentFolderClicked = clickedRow == 0 && tableModel.hasParentFolder(); // Select clicked row if it is not selected already if (currentRow != clickedRow) { int index = tableModel.getFileIndexAt(clickedRow, clickedCol); selectFile(index); } // Request focus on this FileTable is focus is somewhere else if (!hasFocus()) { requestFocus(); } // Popup menu where the user right-clicked new TablePopupMenu(mainFrame, folderPanel.getCurrentFolder(), parentFolderClicked?null:tableModel.getFileAt(clickedRow, clickedCol), parentFolderClicked, tableModel.getMarkedFiles()).show(this, x, y); } // Middle-click on a row marks or unmarks it // Control left-click also works else if (DesktopManager.isMiddleMouseButton(e)) { // Used by mouseDragged lastDraggedRow = rowAtPoint(e.getPoint()); int lastDraggedCol = columnAtPoint(e.getPoint()); markOnRightClick = !tableModel.isFileMarked(lastDraggedRow, lastDraggedCol); int lastDraggedFile = tableModel.getFileIndexAt(lastDraggedRow, lastDraggedCol); setFileMarked(lastDraggedFile, markOnRightClick); } else if (DesktopManager.isLeftMouseButton(e)) { if (e.isShiftDown()) { // Marks a group of rows, from last current row to clicked row (current row) setRangeMarked(currentRow, lastRow, !tableModel.isFileMarked(currentRow, currentColumn)); } else if (e.isControlDown()) { // Marks the clicked file int rowNum = rowAtPoint(e.getPoint()); int colNum = columnAtPoint(e.getPoint()); int index = tableModel.getFileIndexAt(rowNum, colNum); setFileMarked(index, !tableModel.isFileMarked(rowNum, colNum)); } } } @Override public void mouseReleased(MouseEvent e) { } public void mouseDragged(MouseEvent e) { // Discard mouse motion events while in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } // Marks or unmarks every row that was between the last mouseDragged point // and the current one if (DesktopManager.isMiddleMouseButton(e) && lastDraggedRow >= 0) { int draggedRow = rowAtPoint(e.getPoint()); // Mouse was dragged outside of the FileTable if (draggedRow < 0) { return; } setRangeMarked(lastDraggedRow, draggedRow, markOnRightClick); lastDraggedRow = draggedRow; } } @Override public void mouseMoved(MouseEvent e) { } @Override public void keyPressed(KeyEvent e) { // Handle Left/Right keys for compact modes if (viewMode != TableViewMode.FULL) { if (e.getKeyCode() == KeyEvent.VK_LEFT) { if (getSelectedFileIndex() > 0) { int newFileIndex = Math.max(getSelectedFileIndex() - getRowCount(), 0); selectFile(newFileIndex); //repaintRow(getSelectedRow()); repaint(0, 0, getWidth(), getHeight()); e.consume(); } } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { int newFileIndex = Math.min(getSelectedFileIndex() + getRowCount(), getFilesCount() - 1); selectFile(newFileIndex); //repaintRow(getSelectedRow()); repaint(0, 0, getWidth(), getHeight()); e.consume(); } } } @Override public void keyTyped(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { // Discard keyReleased events while quick search is active if (quickSearch.isActive()) { return; } // Test if the event corresponds to the 'Mark/unmark selected file' action keystroke. if (ActionManager.getActionInstance(MarkSelectedFileAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e))) { // Reset variables used to detect repeated key strokes markKeyRepeated = false; lastRowMarked = false; } } @Override public void activePanelChanged(FolderPanel folderPanel) { isActiveTable = folderPanel == getFolderPanel(); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers // instead of a custom header renderer. These indicators change when the active table has changed. if (usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } if (isActiveTable) { focusGained(); } else { focusLost(); } } /** * Listens to certain configuration variables. */ @Override public void configurationChanged(ConfigurationEvent event) { switch (event.getVariable()) { case TcPreferences.DISPLAY_COMPACT_FILE_SIZE: FileTableModel.setSizeFormat(event.getBooleanValue()); tableModel.fillCellCache(mainFrame.getActiveTable()); resizeAndRepaint(); break; case TcPreferences.DATE_FORMAT: case TcPreferences.DATE_SEPARATOR: case TcPreferences.TIME_FORMAT: // Note: for the update to work properly, CustomDateFormat's configurationChanged() method has to be called // before FileTable's, so that CustomDateFormat gets notified of date format first. // Since listeners are stored by MuConfiguration in a hash map, order is pretty much random. // So CustomDateFormat#updateDateFormat() has to be called before to ensure that is uses the new date format. CustomDateFormat.updateDateFormat(); tableModel.fillCellCache(mainFrame.getActiveTable()); resizeAndRepaint(); break; case TcPreferences.TABLE_ICON_SCALE: // Repaint file icons if their size has changed // Recalculate row height, revalidate and repaint the table setRowHeight(); break; case TcPreferences.USE_SYSTEM_FILE_ICONS: // Repaint file icons if the system file icons policy has changed repaint(); } } /** *

    A Custom CellEditor which provides the following functionalities: *

      *
    • Filename selection (without extension) when filename starts being edited. *
    • Can be cancelled by pressing ESCAPE *
    • Starts renaming the file when ENTER is pressed *
    * *

    Only once instance per FileTable is created. * *

    Implementation note: stopCellEditing() and cancelCellEditing() should not be overridden to detect * accept/cancel user events as they are totally unrealiable and often not called, for example when clicking * on one of the table's headers (many other cases). */ private class FilenameEditor extends DefaultCellEditor { private final JTextField filenameField; /** Row that is currently being edited */ private int editingRow; /** Column that is currently being edited */ private int editingCol; /** * Creates a new FilenameEditor instance. * * @param textField the text field to use for editing filenames */ FilenameEditor(JTextField textField) { super(textField); this.filenameField = textField; // Sets the font to the same one that's used for cell rendering (user-defined) filenameField.setFont(FileTableCellRenderer.getCellFont()); filenameField.setFocusTraversalKeysEnabled(false); textField.addKeyListener( new FilePathFieldKeyListener(textField, false) { // Cancel editing when escape key pressed, this is unfortunately not DefaultCellEditor's default behavior @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { cancelCellEditing(); } else if (e.getKeyCode() == KeyEvent.VK_TAB) { cancelCellEditing(); consecutiveRename = editingRow < tableModel.getRowCount() - 1; rename(); } } } ); textField.addActionListener(e -> rename()); textField.addFocusListener(new FocusAdapter() { public void focusLost(FocusEvent e) { cancelCellEditing(); FileTable.this.repaint(); } }); } /** * Renames the currently edited name cell, only if the filename has changed. */ private void rename() { String newName = filenameField.getText(); AbstractFile fileToRename = tableModel.getFileAt(editingRow, editingCol); if (!newName.equals(fileToRename.getName())) { AbstractFile current = folderPanel.getCurrentFolder(); // Starts moving files ProgressDialog progressDialog = new ProgressDialog(mainFrame, Translator.get("move_dialog.moving")); FileSet files = new FileSet(current); files.add(fileToRename); MoveJob renameJob; if (!consecutiveRename) { renameJob = new MoveJob(null, mainFrame, files, current, newName, FileCollisionDialog.ASK_ACTION, true); } else { AbstractFile fileToBeSelected = tableModel.getFileAt(editingRow+1, editingCol); renameJob = new MoveJob(null, mainFrame, files, current, newName, FileCollisionDialog.ASK_ACTION, true) { @Override protected void selectFileWhenFinished(AbstractFile file) { super.selectFileWhenFinished(fileToBeSelected); } }; } progressDialog.start(renameJob);//renameJob.start(); // renameJob= new MoveJob(progressDialog, mainFrame, files, current, newName, FileCollisionDialog.ASK_ACTION, true); // progressDialog.start(renameJob); } else if (consecutiveRename) { selectFile(editingRow + 1); fireSelectedFileChangedEvent(); long tm = System.currentTimeMillis(); if (tm - lastInvokeEitCurrentFilename > 10) { SwingUtilities.invokeLater(FileTable.this::editCurrentFilename); } lastInvokeEitCurrentFilename = tm; // SwingUtilities.invokeLater(FileTable.this::editCurrentFilename); // [java] at com.mucommander.ui.main.table.FileTable$FilenameEditor.rename(FileTable.java:1960) // [java] at com.mucommander.ui.main.table.FileTable$FilenameEditor$1.keyPressed(FileTable.java:1912) // [java] at java.desktop/java.awt.AWTEventMulticaster.keyPressed(AWTEventMulticaster.java:257) // [java] at com.mucommander.ui.main.table.FileTable$FilenameEditor.rename(FileTable.java:1960) // [java] at com.mucommander.ui.main.table.FileTable$FilenameEditor$1.keyPressed(FileTable.java:1912) // [java] at java.desktop/java.awt.AWTEventMulticaster.keyPressed(AWTEventMulticaster.java:258) } } @Override public void cancelCellEditing() { getFolderPanel().getFolderChangeMonitor().setPaused(false); super.cancelCellEditing(); } static long lastInvokeEitCurrentFilename = System.currentTimeMillis(); /* public void restore() { // Filename editor's row resize disabled because of Java bug #4398268 which prevents new rows from being visible after setRowHeight(row, height) has been called. // Add to that the fact that DefaultCellEditor's stopCellEditing() and cancelCellEditing() are not always called, for instance when table header is clicked. // setRowHeight(currentRow, cellRenderer.getFontMetrics(cellRenderer.getCellFont()).getHeight()+cellRenderer.CELL_BORDER_HEIGHT); } */ /** * Notifies this editor that the given row's filename cell is being edited. This method has to be called once * when a row just started being edited. It will save the row number and select the filename without * its extension to make it easier to rename. * * @param row row which is being edited * @param col column which is being edited * @see AbstractCopyDialog#selectDestinationFilename(AbstractFile, String, int) */ void notifyEditing(int row, int col) { // The editing row has to be saved as it could change after row editing has been started this.editingRow = row; this.editingCol = col; AbstractFile file = tableModel.getFileAt(editingRow, editingCol); AbstractCopyDialog.selectDestinationFilename(file, file.getName(), 0).feedToPathField(filenameField); // Request focus on text field filenameField.requestFocus(); } // private void notifyEditingRow(int row) { // // The editing row has to be saved as it could change after row editing has been started // this.editingRow = row; // // AbstractFile file = tableModel.getFileAtRow(editingRow); // AbstractCopyDialog.selectDestinationFilename(file, file.getName(), 0).feedToPathField(filenameField); // //// filenameField.setBorder(BorderFactory.createLineBorder(cellRenderer.getBakgroundOfSelectedFileInInactiveTable())); // // // Request focus on text field // filenameField.requestFocus(); // } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { Component result = super.getTableCellEditorComponent(table, value, isSelected, row, column); if (viewMode != TableViewMode.FULL) { JPanel panel = new JPanel(new BorderLayout()); panel.add(result, BorderLayout.CENTER); final AbstractFile file = tableModel.getFileAt(row, column); CellLabel lblIcon = new CellLabel(); lblIcon.setIcon(FileIconsCache.getInstance().getIcon(file)); lblIcon.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][(row % 2 == 0) ? ThemeCache.NORMAL : ThemeCache.ALTERNATE]); panel.setBackground(lblIcon.getBackground()); //lblIcon.setHasSeparator(column < tableModel.getColumnCount() - 1); panel.add(lblIcon, BorderLayout.WEST); return panel; } return result; } } /** * This inner class adds 'quick search' functionality to the FileTable */ private class FileTableQuickSearch extends QuickSearch { /** * Creates a new QuickSearch instance, only one instance per FileTable should be created. */ private FileTableQuickSearch() { super(FileTable.this); } @Override protected void searchStarted() { sortInfo.setQuickSearchMatchesFirst(isQuickSearchMatchesFirst()); // Repaint the table to add the 'dim' effect on non-matching files scrollpaneWrapper.dimBackground(); } @Override protected void searchStopped() { if (sortInfo.getQuickSearchMatchesFirst()) { sortTable(); } mainFrame.getStatusBar().updateSelectedFilesInfo(); // Removes the 'dim' effect on non-matching files. scrollpaneWrapper.undimBackground(); // re-sort table if need } @Override protected int getNumOfItems() { return tableModel.getFilesCount(); } @Override protected String getItemString(int index) { return tableModel.getFileNameAt(index); } @Override protected void searchStringBecameEmpty(String searchString) { mainFrame.getStatusBar().setStatusInfo(searchString); // TODO: is needed? } @Override protected void matchFound(int index, String searchString, boolean itsBestSearch) { // Select best match's row AbstractFile fileToSelect = tableModel.getFileAt(index); if (sortInfo.getQuickSearchMatchesFirst()) { //sortTable(); tableModel.sortRows(); // Restore selected file selectFile(0); if (fileToSelect != null) { selectFile(fileToSelect); } // Repaint table repaint(); } else { if (fileToSelect != null) { selectFile(fileToSelect); repaint(); } } // Display the new search string in the status bar // that indicates that the search has yielded a match String hint = "" + Translator.get("quick_search") + ": " + searchString + ""; if (sortInfo.getQuickSearchMatchesFirst()) { hint += " (" + Translator.get("status_bar.quick_search.press_esc_to_stop_search") + ")"; } mainFrame.getStatusBar().setStatusInfo(hint, IconManager.getIcon(IconManager.IconSet.STATUS_BAR, QUICK_SEARCH_OK_ICON), false); // re-sort table if need } @Override protected void matchNotFound(String searchString) { // No file matching the search string, display the new search string with an icon // that indicates that the search has failed if (sortInfo.getQuickSearchMatchesFirst()) { int currentFileIndex = tableModel.getFileIndexAt(currentRow, currentColumn); if (currentFileIndex >= 0) { if (currentFileIndex > 0) { selectFile(tableModel.hasParentFolder() ? 1 : 0); } AbstractFile currentFile = tableModel.getFileAt(currentFileIndex); if (quickSearch.matches(currentFile.getName())) { return; } } } String hint = "" + Translator.get("quick_search") + ": " + searchString + ""; if (sortInfo.getQuickSearchMatchesFirst()) { hint += " (" + Translator.get("status_bar.quick_search.press_esc_to_stop_search") + ")"; } mainFrame.getStatusBar().setStatusInfo(hint, IconManager.getIcon(IconManager.IconSet.STATUS_BAR, QUICK_SEARCH_KO_ICON), false); } @Override public synchronized void keyPressed(KeyEvent e) { // Discard key events while in 'no events mode' if (mainFrame.getNoEventsMode()) { return; } char keyChar = e.getKeyChar(); // If quick search is not active... if (!isActive()) { // Return (do not start quick search) if the key is not a valid quick search input if (!isValidQuickSearchInput(e)) { return; } // Return (do not start quick search) if the typed key corresponds to a registered action's accelerator if (ActionKeymap.isKeyStrokeRegistered(KeyStroke.getKeyStrokeForEvent(e))) { return; } // Start the quick search and continue to process the current key event start(); } // At this point, quick search is active int keyCode = e.getKeyCode(); boolean keyHasModifiers = (e.getModifiersEx() & (KeyEvent.SHIFT_DOWN_MASK | KeyEvent.ALT_DOWN_MASK | KeyEvent.CTRL_DOWN_MASK | KeyEvent.META_DOWN_MASK)) != 0; // Backspace removes the last character of the search string if (keyCode == KeyEvent.VK_BACK_SPACE && !keyHasModifiers) { // Search string is empty already if (isSearchStringEmpty()) { return; } removeLastCharacterFromSearchString(); // Find the row that best matches the new search string and select it findMatch(0, true, true); } // Escape immediately cancels the quick search else if (keyCode == KeyEvent.VK_ESCAPE && !keyHasModifiers) { stop(); } // Up/Down jumps to previous/next match // Shift+Up/Shift+Down marks currently selected file and jumps to previous/next match else if ((keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN) && !keyHasModifiers) { // Find the first row before/after the current row that matches the search string jumpPrevNext(keyCode == KeyEvent.VK_DOWN); } // MarkSelectedFileAction and MarkNextRowAction mark the current row and moves to the next match else if (ActionManager.getActionInstance(MarkSelectedFileAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e)) || ActionManager.getActionInstance(MarkNextRowAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e))) { int currentIndex = tableModel.getFileIndexAt(currentRow, currentColumn); if (!isParentFolderSelected()) { // Don't mark/unmark the '..' file setFileMarked(currentIndex, !tableModel.isFileMarked(currentRow, currentColumn)); } // Find the first the next row that matches the search string findMatch(currentIndex+1, true, false); } // MarkPreviousRowAction marks the current row and moves to the previous match else if (ActionManager.getActionInstance(MarkPreviousRowAction.Descriptor.ACTION_ID, mainFrame).isAccelerator(KeyStroke.getKeyStrokeForEvent(e))) { int currentIndex = tableModel.getFileIndexAt(currentRow, currentColumn); if (!isParentFolderSelected()) { // Don't mark/unmark the '..' file setFileMarked(currentIndex, !tableModel.isFileMarked(currentRow, currentColumn)); } // Find the first the previous row that matches the search string findMatch(currentIndex-1, false, false); } // If no modifier other than Shift is pressed and the typed character is not a control character (space is ok) // and a valid Unicode character, add it to the current search string else if (isValidQuickSearchInput(e)) { appendCharacterToSearchString(keyChar); // Find the row that best matches the new search string and select it findMatch(0, true, true); } else { // Test if the typed key combination corresponds to a registered action. // If that's the case, the quick search is canceled and the action is performed. String actionId = ActionKeymap.getRegisteredActionIdForKeystroke(KeyStroke.getKeyStrokeForEvent(e)); if (actionId != null) { // Consume the key event otherwise it would be fired again on the FileTable // (or any other KeyListener on this FileTable) e.consume(); if (getViewMode() != TableViewMode.FULL) { if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT) { return; } } // Cancel quick search if (!isQuickSearchMatchesFirst()) { stop(); } // Perform the action ActionManager.getActionInstance(actionId, mainFrame).performAction(); } // Do not update last search string's change timestamp return; } // Update last search string's change timestamp setLastSearchStringChange(System.currentTimeMillis()); } private void jumpPrevNext(boolean next) { int currentIndex = tableModel.getFileIndexAt(currentRow, currentColumn); if (sortInfo.getQuickSearchMatchesFirst()) { if (currentIndex <= 1 && !next && tableModel.hasParentFolder()) { findMatch(getFilesCount() - 1, false, false); } else { findMatch(currentIndex + (next ? 1 : -1), next, false); } } else { if (currentIndex != 1 || next || !tableModel.hasParentFolder()) { findMatch(currentIndex + (next ? 1 : -1), next, false); } } } } // End of QuickSearch class /** * Not used. */ @Override public void colorChanged(ColorChangedEvent event) {} /** * Receives theme font changes notifications. */ @Override public void fontChanged(FontChangedEvent event) { if (event.getFontId() == Theme.FILE_TABLE_FONT) { // Changes filename editor's font filenameEditor.filenameField.setFont(event.getFont()); // Recalculate row height, revalidate and repaint the table setRowHeight(); } } public FileTableConfiguration getConfiguration() { FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); return fileTableColumnModel == null ? null : fileTableColumnModel.getConfiguration(); } public int getColumnWidth(Column column) { FileTableColumnModel fileTableColumnModel = getFileTableColumnModel(); if (fileTableColumnModel != null) { return fileTableColumnModel.getColumnFromId(column.ordinal()).getWidth(); } var model = getCompactFileTableColumnModel(); if (model == null) { return 0; } var c = getCompactFileTableColumnModel().getColumn(0); return c == null ? 0 : c.getWidth(); } /** * This thread performs the change of current folder. * * @author Nicolas Rinaudo, Maxence Bernard */ private class FolderChangeThread implements Runnable { private final AbstractFile folder; private final AbstractFile[] children; private final FileSet markedFiles; private final AbstractFile selectedFile; private FolderChangeThread(AbstractFile folder, AbstractFile[] children, FileSet markedFiles, AbstractFile selectedFile) { this.folder = folder; this.children = children; this.markedFiles = markedFiles; this.selectedFile = selectedFile; setName(getClass().getName()); } public void run() { try { // Set the new current folder. tableModel.setCurrentFolder(folder, children, FileTable.this); // Update the visibility state of conditional columns FileTableColumnModel columnModel = getFileTableColumnModel(); updateColumnsVisibility(); // The column corresponding to the current 'sort by' criterion may have become invisible. // If that is the case, change the criterion to NAME. if (columnModel != null && !columnModel.isColumnVisible(sortInfo.getCriterion())) { sortInfo.setCriterion(Column.NAME); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers if (usesTableHeaderRenderingProperties()) { setTableHeaderRenderingProperties(); } } // Sort the new folder using the current sort criteria, ascending/descending order and // 'show folders first' values. tableModel.sortRows(); // Computes the index of the new row selection. int indexToSelect = getIndexToSelect(); selectFile(indexToSelect); fireSelectedFileChangedEvent(); // Restore previously marked files (if any / current folder hasn't changed) if (markedFiles != null) { // Restore previously marked files int nbMarkedFiles = markedFiles.size(); for (int i = 0; i < nbMarkedFiles; i++) { int fileIndex = tableModel.getFileIndex(markedFiles.elementAt(i)); if (fileIndex != -1) { tableModel.setFileMarked(fileIndex, true); } } // Notify registered listeners that currently marked files have changed on this FileTable fireMarkedFilesChangedEvent(); } if (consecutiveRename) { editCurrentFilename(); consecutiveRename = false; } resizeAndRepaint(); } catch (Throwable e) { // While no such thing should happen, we want to make absolutely sure no exception // is propagated to the AWT event dispatch thread. logger.warn("Caught exception while changing folder, this should not happen!", e); logger.warn(e.getMessage()); } finally { // Notify #setCurrentFolder that we're done changing the folder. synchronized(this) { notify(); } } } private int getIndexToSelect() { int currentIndex = tableModel.getFileIndexAt(currentRow, currentColumn); if (selectedFile == null) { // If no file was marked as needing to be selected, selects the first line. return 0; } // Tries to find the index of the file to select. If it cannot be found (the file might not // exist anymore, for example), use the closest possible row. int indexToSelect = tableModel.getFileIndex(selectedFile); if (indexToSelect < 0) { int filesCount = tableModel.getFilesCount(); return currentIndex < filesCount ? currentIndex : filesCount - 1; } return indexToSelect; } } void updateSelectedFilesStatusBar() { mainFrame.getStatusBar().updateSelectedFilesInfo(); } /** * For full view mode returns current row, for compact mode returns current file index * @return full selected file index (that equals to selected row for full view mode) */ public int getSelectedFileIndex() { if (viewMode == TableViewMode.FULL) { return getSelectedRow(); } else { CompactFileTableModel model = (CompactFileTableModel)getModel(); return getSelectedRow() + getSelectedColumn() * getRowCount() + model.getOffset(); } } public int getFilesCount() { return ((BaseFileTableModel)getModel()).getFilesCount(); } private static boolean isQuickSearchMatchesFirst() { return TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_QUICK_SEARCH_MATCHES_FIRST, TcPreferences.DEFAULT_SHOW_QUICK_SEARCH_MATCHES_FIRST); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/FileTableHeader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.impl.ToggleAutoSizeAction; import com.mucommander.ui.main.MainFrame; import ru.trolsoft.ui.TCheckBoxMenuItem; import ru.trolsoft.ui.TMenuSeparator; import javax.swing.*; import javax.swing.table.JTableHeader; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; /** * @author Maxence Bernard */ public class FileTableHeader extends JTableHeader implements MouseListener { private final FileTable table; FileTableHeader(FileTable table) { super(table.getColumnModel()); this.table = table; addMouseListener(this); } @Override public boolean getReorderingAllowed() { return true; } public void mouseClicked(MouseEvent e) { Column col = Column.valueOf(table.convertColumnIndexToModel(getColumnModel().getColumnIndexAtX(e.getX()))); table.requestFocus(); if (DesktopManager.isLeftMouseButton(e)) { // One of the table headers was left-clicked, sort the table by the clicked column's criterion changeSortOrder(col); } else if (DesktopManager.isRightMouseButton(e)) { // One of the table headers was right-clicked, popup a menu that offers to hide the column showSortPopupMenu(e); } } private void showSortPopupMenu(MouseEvent e) { JPopupMenu popupMenu = new JPopupMenu(); MainFrame mainFrame = table.getFolderPanel().getMainFrame(); for (Column c : Column.values()) { if (c == Column.NAME) { continue; } JCheckBoxMenuItem checkboxMenuItem = new TCheckBoxMenuItem(ActionManager.getActionInstance(c.getToggleColumnActionId(), mainFrame)); checkboxMenuItem.setSelected(table.isColumnEnabled(c)); checkboxMenuItem.setEnabled(table.isColumnDisplayable(c)); // Override the action's label to a shorter one checkboxMenuItem.setText(c.getLabel()); popupMenu.add(checkboxMenuItem); } popupMenu.add(new TMenuSeparator()); JCheckBoxMenuItem checkboxMenuItem = new TCheckBoxMenuItem(ActionManager.getActionInstance(ToggleAutoSizeAction.Descriptor.ACTION_ID, mainFrame)); checkboxMenuItem.setSelected(mainFrame.isAutoSizeColumnsEnabled()); popupMenu.add(checkboxMenuItem); popupMenu.show(this, e.getX(), e.getY()); popupMenu.setVisible(true); } private void changeSortOrder(Column col) { // If the table was already sorted by this criteria, reverse order if (table.getSortInfo().getCriterion() == col) { table.reverseSortOrder(); } else { table.sortBy(col); } } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/FileTableHeaderRenderer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.ui.icon.IconManager; import javax.swing.*; import javax.swing.border.Border; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.JTableHeader; import java.awt.*; /** * Custom table header renderer that displays an icon to indicate the current sort criterion and the sort order * (ascending or descending). * * @author Maxence Bernard */ public class FileTableHeaderRenderer extends DefaultTableCellRenderer { private final static ImageIcon ASCENDING_ICON = IconManager.getIcon(IconManager.IconSet.COMMON, "arrow_up.png"); private final static ImageIcon DESCENDING_ICON = IconManager.getIcon(IconManager.IconSet.COMMON, "arrow_down.png"); public FileTableHeaderRenderer() { // These properties can be set only once // Icon should be on the right setHorizontalTextPosition(LEFT); // Increase gap size between text and icon (default is 4 pixels) setIconTextGap(6); // Note: the label is left-aligned by default setHorizontalAlignment(JLabel.CENTER); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { // Note: the label is returned by DefaultTableHeaderRenderer#getTableCellRendererComponent() is in fact this JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (table != null) { JTableHeader header = table.getTableHeader(); if (header != null) { label.setForeground(header.getForeground()); label.setBackground(header.getBackground()); label.setFont(header.getFont()); } FileTable fileTable = (FileTable)table; if (fileTable.getSortInfo().getCriterion() == Column.valueOf(fileTable.convertColumnIndexToModel(column))) { // This header is the currently selected one label.setIcon(getSortingIcon(fileTable)); } else { // The renderer component acts as a rubber-stamp, therefore the icon value needs to be set to null explicitly // as it might still hold a previous value label.setIcon(null); } } // Use borders made specifically for table headers Border border = UIManager.getBorder("TableHeader.cellBorder"); label.setBorder(border); // Add a tooltip as headers are sometimes too small for the text to fit entirely label.setToolTipText((String)value); return label; } private static ImageIcon getSortingIcon(FileTable fileTable) { return fileTable.getSortInfo().getAscendingOrder() ? ASCENDING_ICON : DESCENDING_ICON; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/FileTableWrapperForDisplay.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.commons.file.AbstractFile; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.border.MutableLineBorder; import com.mucommander.ui.dnd.FileDropTargetListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.TablePopupMenu; import com.mucommander.ui.theme.*; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.awt.dnd.DropTarget; import java.awt.event.*; import static com.mucommander.ui.theme.ThemeManager.getCurrentColor; /** * This class is responsible for the viewing aspects of a FileTable component: * 1. Wraps the FileTable with a JScrollPane which allows it to scroll. * 2. Sets the colors of the FileTable. * 3. Sets other presentation aspects of the FileTable component. * 4. Initiates a popup window on right click on the FileTable component. * * @author Arik Hadas */ public class FileTableWrapperForDisplay extends JScrollPane implements FocusListener, ThemeListener { /** The FileTable being wrapped for display */ private final FileTable fileTable; /** Colors relevant for the FileTable or its ScrollPane wrapper */ private Color borderColor; private Color unfocusedBorderColor; private Color backgroundColor; private Color unfocusedBackgroundColor; private Color unmatchedBackgroundColor; // /** Frame containing this file table. */ // private final MainFrame mainFrame; // /** Panel containing this file table */ // private final FolderPanel folderPanel; FileTableWrapperForDisplay(final FileTable fileTable, final FolderPanel folderPanel, final MainFrame mainFrame) { super(fileTable, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); // this.mainFrame = mainFrame; // this.folderPanel = folderPanel; this.fileTable = fileTable; backgroundColor = getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR); unmatchedBackgroundColor = getCurrentColor(Theme.FILE_TABLE_UNMATCHED_BACKGROUND_COLOR); unfocusedBorderColor = getCurrentColor(Theme.FILE_TABLE_INACTIVE_BORDER_COLOR); unfocusedBackgroundColor = getCurrentColor(Theme.FILE_TABLE_INACTIVE_BACKGROUND_COLOR); // Sets the table border. setBorder(new MutableLineBorder(unfocusedBorderColor, 1)); borderColor = getCurrentColor(Theme.FILE_TABLE_BORDER_COLOR); // Set scroll pane's background color to match the one of this panel and FileTable getViewport().setBackground(unfocusedBackgroundColor); fileTable.setBackground(unfocusedBackgroundColor); // Remove default action mappings that conflict with corresponding mu actions InputMap inputMap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); inputMap.clear(); inputMap.setParent(null); fileTable.addFocusListener(this); // Enable drop support to copy/move/change current folder when files are dropped on the FileTable FileDropTargetListener dropTargetListener = new FileDropTargetListener(fileTable.getFolderPanel(), false); fileTable.setDropTarget(new DropTarget(fileTable, dropTargetListener)); setDropTarget(new DropTarget(this, dropTargetListener)); // Listens to theme events ThemeManager.addCurrentThemeListener(this); // Catch mouse events on the ScrollPane addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { // Left-click requests focus on the FileTable if (DesktopManager.isLeftMouseButton(e)) { fileTable.requestFocus(); } // Right-click brings a contextual popup menu else if (DesktopManager.isRightMouseButton(e)) { if (!fileTable.hasFocus()) { fileTable.requestFocus(); } AbstractFile currentFolder = folderPanel.getCurrentFolder(); new TablePopupMenu(mainFrame, currentFolder, null, false, fileTable.getFileTableModel().getMarkedFiles()).show(FileTableWrapperForDisplay.this, e.getX(), e.getY()); } } }); addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { int rotation = e.getWheelRotation(); // if (rotation > 0) { // move(1); // } else if (rotation < 0) { // move(-1); // } if (rotation != 0) { move(rotation); } } private void move(int move) { Point pt = viewport.getViewPosition(); pt.y += move; pt.y = Math.max(0, pt.y); pt.y = Math.min(getMaxYExtent(), pt.y); viewport.setViewPosition(pt); } private int getMaxYExtent() { int result = viewport.getView().getHeight() - viewport.getHeight(); return Math.max(result, 0); } }); } @Override public void setVisible(boolean visible) { if (visible) { super.setVisible(true); } } @Override public boolean requestFocusInWindow() { return fileTable.requestFocusInWindow(); } /** * Dims the scrollpane's background, called by {@link com.mucommander.ui.quicksearch.QuickSearch} when a quick search is started. */ void dimBackground() { fileTable.setBackground(unmatchedBackgroundColor); getViewport().setBackground(unmatchedBackgroundColor); } /** * Stops dimming the scrollpane's background (returns to a normal background color), called by * {@link com.mucommander.ui.quicksearch.QuickSearch} when a quick search is over. */ void undimBackground() { // Identifies the new background color. Color newColor = fileTable.hasFocus() ? backgroundColor : unfocusedBackgroundColor; // If the old and new background color differ, set the new background // color. // Otherwise, repaint the table - if we were to skip that step, quicksearch // cancellation might result in a corrupt display. if (newColor.equals(getViewport().getBackground())) { fileTable.repaint(); } else { fileTable.setBackground(newColor); getViewport().setBackground(newColor); } } @Override public void focusGained(FocusEvent e) { setBorderColor(borderColor); getViewport().setBackground(backgroundColor); fileTable.setBackground(backgroundColor); getViewport().repaint(); } @Override public void focusLost(FocusEvent e) { setBorderColor(unfocusedBorderColor); getViewport().setBackground(unfocusedBackgroundColor); fileTable.setBackground(unfocusedBackgroundColor); } private void setBorderColor(Color color) { Border border = getBorder(); // Some (rather evil) look and feels will change borders outside of muCommander's control, // this check is necessary to ensure no exception is thrown. if (border instanceof MutableLineBorder) { ((MutableLineBorder) border).setLineColor(color); } } @Override public void colorChanged(ColorChangedEvent event) { switch (event.getColorId()) { case Theme.FILE_TABLE_BORDER_COLOR: borderColor = event.getColor(); if (fileTable.hasFocus()) { setBorderColor(borderColor); repaint(); } break; case Theme.FILE_TABLE_INACTIVE_BORDER_COLOR: unfocusedBorderColor = event.getColor(); if (!fileTable.hasFocus()) { setBorderColor(unfocusedBorderColor); repaint(); } break; case Theme.FILE_TABLE_BACKGROUND_COLOR: backgroundColor = event.getColor(); if (fileTable.hasFocus()) { getViewport().setBackground(backgroundColor); fileTable.setBackground(backgroundColor); } break; case Theme.FILE_TABLE_INACTIVE_BACKGROUND_COLOR: unfocusedBackgroundColor = event.getColor(); if (!fileTable.hasFocus()) { getViewport().setBackground(unfocusedBackgroundColor); fileTable.setBackground(unfocusedBackgroundColor); } break; case Theme.FILE_TABLE_UNMATCHED_BACKGROUND_COLOR: unmatchedBackgroundColor = event.getColor(); break; } } /** * Not used. */ public void fontChanged(FontChangedEvent event) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/SortInfo.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; /** * This class holds information describes how a {@link FileTable} is currently sorted: sort criterion, * ascending/descending order, whether directories are displayed first or mixed with regular files. * *

    The values are not meant to be changed outside this package: all setters are package-protected. * Use {@link FileTable} methods to change how the table is sorted. * * @author Maxence Bernard */ public class SortInfo implements Cloneable { /** Current sort criterion */ private Column criterion = Column.NAME; /** Ascending/descending order */ private boolean ascendingOrder = true; /** Should folders be displayed first, or mixed with regular files */ private boolean showFoldersFirst = TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_FOLDERS_FIRST, TcPreferences.DEFAULT_SHOW_FOLDERS_FIRST); /** Should Folders also get sorted or always alphabetical ... only possible if Folders First enabled */ private boolean foldersAlwaysAlphabetical = TcConfigurations.getPreferences().getVariable(TcPreference.FOLDERS_ALWAYS_ALPHABETICAL, TcPreferences.DEFAULT_FOLDERS_ALWAYS_ALPHABETICAL); private boolean showQuickSearchMatchesFirst = TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_QUICK_SEARCH_MATCHES_FIRST, TcPreferences.DEFAULT_SHOW_QUICK_SEARCH_MATCHES_FIRST); SortInfo() { } /** * Returns the column used as a criterion to sort the table. * * @return the column used as a criterion to sort the table. */ public Column getCriterion() { return criterion; } /** * Sets the column to be used as a criterion to sort the table. * * @param criterion the column to be used as a criterion to sort the table, see {@link Column} for possible values */ public void setCriterion(Column criterion) { this.criterion = criterion; } /** * Returns true if the current sort order of is ascending, false if it is descending. * * @return true if the current sort order is ascending, false if it is descending */ public boolean getAscendingOrder() { return ascendingOrder; } /** * Sets the sort order of the column corresponding to the current criterion. * * @param ascending true if the current sort order is ascending, false if it is descending */ public void setAscendingOrder(boolean ascending) { this.ascendingOrder = ascending; } /** * Sets whether folders are currently sorted and displayed before regular files or mixed with them. * * @param showFoldersFirst true if folders are sorted and displayed before regular files, false if they are mixed with regular files and sorted altogether */ public void setFoldersFirst(boolean showFoldersFirst) { this.showFoldersFirst = showFoldersFirst; } /** * Sets whether folders are currently sorted always alphabetical. * * @param foldersAlwaysAlphabetical true if folders are sorted always alphabetical */ public void setFoldersAlwaysAlphabetical(boolean foldersAlwaysAlphabetical) { this.foldersAlwaysAlphabetical = foldersAlwaysAlphabetical; } /** * Returns true if folders are sorted and displayed before regular files, false if they * are mixed with regular files and sorted altogether. * * @return true if folders are sorted and displayed before regular files, false if they are mixed with regular files and sorted altogether */ public boolean getFoldersFirst() { return showFoldersFirst; } /** * Returns true if folders are sorted always alphabetical * * @return true if folders are sorted always alphabetical */ public boolean getFoldersAlwaysAlphabetical() { return foldersAlwaysAlphabetical; } /** * Sets whether matched files are currently sorted and displayed before other files or mixed with them on quick search. * * @param quickSearchMatchesFirst true if matched are sorted and displayed before other files, false if they are mixed with regular files and sorted altogether */ void setQuickSearchMatchesFirst(boolean quickSearchMatchesFirst) { this.showQuickSearchMatchesFirst = quickSearchMatchesFirst; } /** * Returns true if quick search matched are sorted and displayed before other files, false if they * are mixed with other files and sorted altogether. * * @return true if matched are sorted and displayed before other files, false if they are mixed with other files and sorted altogether */ public boolean getQuickSearchMatchesFirst() { return showQuickSearchMatchesFirst; } @Override public SortInfo clone() { try { return (SortInfo)super.clone(); } catch(CloneNotSupportedException e) { // Should never happen return null; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/TransparentCellLabel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table; import java.awt.*; /** * @author Oleg Trifonov * Created on 26/01/17. */ public class TransparentCellLabel extends CellLabel { public TransparentCellLabel() { super(); } @Override public void paint(Graphics g) { Graphics2D g2 = (Graphics2D) g.create(); g2.setComposite(AlphaComposite.SrcAtop.derive(0.3f)); super.paint(g2); g2.dispose(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/package.html ================================================ Contains all the classes used to display files in the main muCommander window. ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/BaseCellRenderer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.main.table.CellLabel; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.theme.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.table.TableCellRenderer; import java.awt.Font; /** * @author Oleg Trifonov * Created on 03/04/15. */ public abstract class BaseCellRenderer implements TableCellRenderer, ThemeListener { private static Logger logger; protected final FileTable table; protected final BaseFileTableModel tableModel; /** Custom JLabel that render specific column cells */ protected CellLabel[] cellLabels; protected BaseCellRenderer(FileTable table) { this.table = table; this.tableModel = table.getFileTableModel(); } protected static int getFileColorIndex(int fileIndex, AbstractFile file, BaseFileTableModel tableModel) { // Parent directory. if (fileIndex == 0 && tableModel.hasParentFolder()) { return ThemeCache.FOLDER; } // Marked file if (tableModel.isFileMarked(fileIndex)) { return ThemeCache.MARKED; } // Symlink if (file.isSymlink()) { return ThemeCache.SYMLINK; } // Hidden file/folder if (file.isHidden()) { return file.isDirectory() ? ThemeCache.HIDDEN_FOLDER : ThemeCache.HIDDEN_FILE; } // Directory if (file.isDirectory()) { return ThemeCache.FOLDER; } // Archive if (file.isBrowsable()) { return ThemeCache.ARCHIVE; } // Executable if (file.isExecutable()) { return ThemeCache.EXECUTABLE; } // Plain file return ThemeCache.PLAIN_FILE; } /** * Returns the font used to render all table cells. */ public static Font getCellFont() { return ThemeCache.tableFont; } /** * Sets CellLabels' font to the current one. */ // TODO protected void setCellLabelsFont(Font newFont) { // Set custom font for (Column c : Column.values()) { // No need to set extension label's font as this label renders only icons and no text if (c == Column.EXTENSION) { continue; } cellLabels[c.ordinal()].setFont(newFont); } } /** * Receives theme color changes notifications. */ @Override public void colorChanged(ColorChangedEvent event) { table.repaint(); } /** * Receives theme font changes notifications. */ @Override public void fontChanged(FontChangedEvent event) { if (event.getFontId() == Theme.FILE_TABLE_FONT) { setCellLabelsFont(ThemeCache.tableFont); } } protected void debug(String s) { getLogger().debug(s); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(BaseCellRenderer.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/BaseFileTableModel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.impl.CachedFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.FileComparator; import com.mucommander.commons.file.util.FileSet; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.main.table.CalculateDirectorySizeWorker; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.SortInfo; import com.mucommander.ui.quicksearch.QuickSearch; import javax.swing.table.AbstractTableModel; import java.awt.Cursor; import java.util.*; /** * @author Oleg Trifonov * Created on 04/04/15. */ public abstract class BaseFileTableModel extends AbstractTableModel { private static final Cursor WAIT_CURSOR = new Cursor(Cursor.WAIT_CURSOR); /** String used as size information for directories */ public static final String DIRECTORY_SIZE_STRING = "

    "; /** String used as size information for directories that queued to size calculation */ protected static final String QUEUED_DIRECTORY_SIZE_STRING = "<...>"; /** True if the name column is temporarily editable */ protected boolean nameColumnEditable; /** SizeFormat format used to create the size column's string */ protected static int sizeFormat; /** Contains sort-related variables */ private SortInfo sortInfo; /** * QuickSearch object to sorting and filtering matched files */ protected QuickSearch quickSearch; /** Index array */ protected int[] fileArrayIndex; /** The current folder */ protected AbstractFile currentFolder; /** Date of the current folder when it was changed */ protected long currentFolderDateSnapshot; /** The current folder's parent folder, may be null */ protected AbstractFile parent; /** Cached file instances */ private AbstractFile cachedFiles[]; /** Combined size of files currently marked */ private long markedTotalSize; /** Number of files currently marked */ private int nbFilesMarked; /** Marked files array */ private boolean fileMarked[]; /** Tasks queue for directory size calculate */ protected final List calculateSizeQueue = new LinkedList<>(); /** Worker to calculate directories sizes */ private CalculateDirectorySizeWorker calculateDirectorySizeWorker; /** True if the table has directories with calculated size */ protected boolean hasCalculatedDirectories; /** Stores marked directories to calculate these size if need */ private final Set markedDirectories = new HashSet<>(); /** Here will be stored sizes of directories calculated by F3 command */ protected final Map directorySizes = new HashMap<>(); private FileComparator fileComparator; /* * First visible row */ //protected int firstVisibleRow; static { // Initialize the size column format based on the configuration setSizeFormat(getFileSizeFormat()); } private static boolean getFileSizeFormat() { return TcConfigurations.getPreferences().getVariable(TcPreference.DISPLAY_COMPACT_FILE_SIZE, TcPreferences.DEFAULT_DISPLAY_COMPACT_FILE_SIZE); } public abstract void fillCellCache(FileTable fileTable); public abstract int getFileRow(int index); /** * Init and fill cell cache to speed up table even more */ protected abstract void initCellValuesCache(); /** * Returns index of file in directory (index of '..' == 0) * @param row table row * @param col table column * @return index file in the table */ public abstract int getFileIndexAt(int row, int col); protected BaseFileTableModel() { fileArrayIndex = new int[0]; fileMarked = new boolean[0]; // Init arrays to avoid NullPointerExceptions until setCurrentFolder() gets called for the first time cachedFiles = new AbstractFile[0]; } public synchronized void setupFromModel(BaseFileTableModel model) { if (model == null) { return; } this.sortInfo = model.sortInfo; this.fileArrayIndex = model.fileArrayIndex; this.currentFolder = model.currentFolder; this.currentFolderDateSnapshot = model.currentFolderDateSnapshot; this.parent = model.parent; this.cachedFiles = model.cachedFiles; this.markedTotalSize = model.markedTotalSize; this.nbFilesMarked = model.nbFilesMarked; this.fileMarked = model.fileMarked; stopSizeCalculation(); } /** * Sets the SizeFormat format used to create the size column's string. * * @param compactSize true to use a compact size format, false for full size in bytes */ public static void setSizeFormat(boolean compactSize) { if (compactSize) { sizeFormat = SizeFormat.DIGITS_MEDIUM | SizeFormat.UNIT_SHORT;// | SizeFormat.ROUND_TO_KB; } else { sizeFormat = SizeFormat.DIGITS_FULL; } sizeFormat |= SizeFormat.INCLUDE_SPACE; } /** * Returns the SizeFormat format used to create the size column's string * @return SizeFormat bit mask */ public static int getSizeFormat() { return sizeFormat; } /** * Pre-fetch the attributes that are used by the table renderer and some actions from the given CachedFile. * By doing so, the attributes will be available when the associated getters are called and thus the methods won't * be I/O bound and will not lock. * * @param cachedFile a CachedFile instance from which to pre-fetch attributes */ private static void prefetchCachedFileAttributes(AbstractFile cachedFile) { cachedFile.isDirectory(); cachedFile.isBrowsable(); cachedFile.isHidden(); // Pre-fetch isSymlink attribute and if the file is a symlink, pre-fetch the canonical file and its attributes if (cachedFile.isSymlink()) { AbstractFile canonicalFile = cachedFile.getCanonicalFile(); if (canonicalFile != cachedFile) { // Cheap test to prevent infinite recursion on bogus file implementations prefetchCachedFileAttributes(canonicalFile); } } } /** * Returns the file located at the given index, not including the parent file. * Returns null if fileIndex is lower than 0 or is greater than or equals {@link #getFileCount() getFileCount()}. * * @param fileIndex index of a file, comprised between 0 and #getFileCount() * @return the file located at the given index, not including the parent file */ public synchronized AbstractFile getFileAt(int fileIndex) { if (parent != null) { if (fileIndex == 0) { return parent; } fileIndex--; } // Need to check that row index is not larger than actual number of rows // because if table has just been changed (rows have been removed), // JTable may have an old row count value and may try to repaint rows that are out of bounds. if (fileIndex >= 0 && fileIndex < fileArrayIndex.length) { return ((CachedFile)cachedFiles[fileArrayIndex[fileIndex]]).getProxiedFile(); } return null; } /** * Returns the actual number of files the current folder contains, including the parent '..' file (if any). * * @return the actual number of files the current folder contains, including the parent '..' file (if any) */ public synchronized int getFileCount() { return cachedFiles.length + (parent != null ? 1 : 0); } /** * Returns the actual number of files the current folder contains, not including the parent '..' file (if any). * * @return the actual number of files the current folder contains, not including the parent '..' file (if any) */ public synchronized int getFileCountWithoutParent() { return cachedFiles.length; } /** * Returns the current folder's children. The returned array contains {@link AbstractFile} instances, and not * CachedFile instances contrary to {@link #getCachedFiles()}. * * @return the current folder's children * @see #getCachedFiles() */ public synchronized AbstractFile[] getFiles() { int nbFiles = cachedFiles.length; AbstractFile[] files = new AbstractFile[nbFiles]; for (int i = 0; i < nbFiles; i++) { files[i] = cachedFiles[i] == null ? null : ((CachedFile) cachedFiles[i]).getProxiedFile(); } return files; } /** * Returns the current folder's children. The returned array contains {@link CachedFile} instances, where * most attributes have already been fetched and cached. * * @return the current folder's children, as an array of CachedFile instances * @see #getFiles() */ private synchronized AbstractFile[] getCachedFiles() { // Clone the array to make sure it can't be modified outside of this class AbstractFile[] cachedFilesCopy = new AbstractFile[cachedFiles.length]; System.arraycopy(cachedFiles, 0, cachedFilesCopy, 0, cachedFiles.length); return cachedFilesCopy; } /** * Sorts rows by the current criterion, ascending/descending order and 'folders first' value. */ public synchronized void sortRows() { this.fileComparator = createFileComparator(sortInfo); sort(0, fileArrayIndex.length - 1); this.fileComparator = null; } ////////////////// // Sort methods // ////////////////// private FileComparator createFileComparator(SortInfo sortInfo) { QuickSearch qs = quickSearch != null && quickSearch.isActive() && sortInfo.getQuickSearchMatchesFirst() ? quickSearch : null; return new FileComparator(sortInfo.getCriterion().getFileComparatorCriterion(), sortInfo.getAscendingOrder(), sortInfo.getFoldersFirst(), sortInfo.getFoldersAlwaysAlphabetical(), qs); } /** * Quick sort implementation, based on James Gosling's implementation. */ protected void sort(int lo0, int hi0) { int lo = lo0; int hi = hi0; if (lo >= hi) { return; } else if (lo == hi - 1) { // sort a two element list by swapping if necessary int loIndex = fileArrayIndex[lo]; int hiIndex = fileArrayIndex[hi]; if (compare(loIndex, hiIndex) > 0) { fileArrayIndex[lo] = hiIndex; fileArrayIndex[hi] = loIndex; } return; } // Pick a pivot and move it out of the way int pivotIndex = fileArrayIndex[(lo + hi) / 2]; fileArrayIndex[(lo + hi) / 2] = fileArrayIndex[hi]; fileArrayIndex[hi] = pivotIndex; while (lo < hi) { // Search forward from files[lo] until an element is found that // is greater than the pivot or lo >= hi //while (compare(cachedFiles[fileArrayIndex[lo]], pivot)<=0 && lo < hi) { while (compare(fileArrayIndex[lo], pivotIndex) <= 0 && lo < hi) { lo++; } // Search backward from files[hi] until element is found that // is less than the pivot, or lo >= hi //while (compare(pivot, cachedFiles[fileArrayIndex[hi]])<=0 && lo < hi ) { while (compare(pivotIndex, fileArrayIndex[hi]) <= 0 && lo < hi ) { hi--; } // Swap elements files[lo] and files[hi] if (lo < hi) { int temp = fileArrayIndex[lo]; fileArrayIndex[lo] = fileArrayIndex[hi]; fileArrayIndex[hi] = temp; } } // Put the median in the "center" of the list fileArrayIndex[hi0] = fileArrayIndex[hi]; fileArrayIndex[hi] = pivotIndex; // Recursive calls, elements files[lo0] to files[lo-1] are less than or // equal to pivot, elements files[hi+1] to files[hi0] are greater than pivot. sort(lo0, lo-1); sort(hi+1, hi0); } private int compare(int index1, int index2) { if (index1 == index2) { return 0; } return fileComparator.compare(cachedFiles[index1], cachedFiles[index2]); } /** * Returns the current folder, i.e. the last folder set using {@link #setCurrentFolder(com.mucommander.commons.file.AbstractFile, com.mucommander.commons.file.AbstractFile[], FileTable table)}. * * @return the current folder */ public synchronized AbstractFile getCurrentFolder() { return currentFolder; } /** * Sets the {@link SortInfo} instance that describes how the associated table is * sorted. * * @param sortInfo SortInfo instance that describes how the associated table is sorted */ public void setSortInfo(SortInfo sortInfo) { this.sortInfo = sortInfo; } public void setQuickSearch(QuickSearch quickSearch) { this.quickSearch = quickSearch; } /** * Sets the current folder and its children. * * @param folder the current folder * @param children the current folder's children */ public synchronized void setCurrentFolder(AbstractFile folder, AbstractFile children[], FileTable table) { int nbFiles = children.length; this.currentFolder = (folder instanceof CachedFile) ? folder : new CachedFile(folder, true); this.parent = currentFolder.getParent(); // Note: the returned parent is a CachedFile instance if (parent != null) { // Pre-fetch the attributes that are used by the table renderer and some actions. prefetchCachedFileAttributes(parent); } stopSizeCalculation(); // Initialize file indexes and create CachedFile instances to speed up table display and navigation this.cachedFiles = children; this.fileArrayIndex = new int[nbFiles]; // we needn't prefetch local files for performance optimization purposes // in the case of local files the lazy initialization will be enough boolean needPrefetch = nbFiles > 0 && !(children[0] instanceof LocalFile); for (int i = 0; i < nbFiles; i++) { AbstractFile child = children[i]; AbstractFile file = child instanceof CachedFile ? child : new CachedFile(child, true); // Pre-fetch the attributes that are used by the table renderer and some actions. if (needPrefetch) { prefetchCachedFileAttributes(file); } cachedFiles[i] = file; fileArrayIndex[i] = i; } // Reset marked files //this.rowMarked = new boolean[getRowCount()]; this.fileMarked = new boolean[getFilesCount()]; this.markedTotalSize = 0; this.nbFilesMarked = 0; // Init and fill cell cache to speed up table even more initCellValuesCache(); fillCellCache(table); } /** * Returns the date of the current folder, when it was set using * {@link #setCurrentFolder(com.mucommander.commons.file.AbstractFile, com.mucommander.commons.file.AbstractFile[], FileTable table)}. * In other words, the returned date is a snapshot of the current folder's date which is never updated. * * @return Returns the date of the current folder, when it was set using #setCurrentFolder(Abstract, Abstract[]) */ public synchronized long getCurrentFolderDateSnapshot() { return currentFolderDateSnapshot; } /** * Returns true if the current folder has a parent. * * @return true if the current folder has a parent */ public synchronized boolean hasParentFolder() { return parent != null; } /** * Returns the current folder's parent if there is one, null otherwise. * * @return the current folder's parent if there is one, null otherwise */ public synchronized AbstractFile getParentFolder() { return parent; } /** * Returns the index of the first row that can be marked/unmarked : 1 if the current folder has a * parent folder, 0 otherwise (parent folder row '..' cannot be marked). * * @return the index of the first row that can be marked/unmarked */ public int getFirstMarkableIndex() { return parent == null ? 0 : 1; } /** * Marks/unmarks the given row range, delimited by the provided start row index and end row index (inclusive). * End row may be less, greater or equal to the start row. * * @param start index of the first file to mark/unmark * @param end index of the last file to mark/ummark, startRow may be less or greater than startRow * @param marked if true, all the files within the range will be marked, unmarked otherwise */ public void setRangeMarked(int start, int end, boolean marked) { if (end >= start) { for (int i = start; i <= end; i++) { setFileMarked(i, marked); } } else { for (int i = start; i >= end; i--) { setFileMarked(i, marked); } } } /** * Marks/Unmarks the given file. * * @param file the file to mark/unmark * @param marked true to mark the row, false to unmark it. */ public synchronized void setFileMarked(AbstractFile file, boolean marked) { int index = getFileIndex(file); if (index >= 0) { setFileMarked(index, marked); } } /** * Marks/unmarks the files that match the given {@link FileFilter}. * * @param filter the FileFilter to match the files against * @param marked if true, matching files will be marked, if false, they will be unmarked */ public synchronized void setFilesMarked(FileFilter filter, boolean marked) { int nbFiles = getFilesCount(); for (int i = parent == null ? 0 : 1; i < nbFiles; i++) { if (filter.match(getCachedFileAt(i))) { setFileMarked(i, marked); } } } /** * Returns a {@link com.mucommander.commons.file.util.FileSet FileSet} with all currently marked files. *

    * The returned FileSet is a freshly created instance, so it can be safely modified. * However, it won't be kept current : the returned FileSet is just a snapshot * which might not reflect the current marked files state after this method has returned and additional * files have been marked/unmarked. * * @return a FileSet containing all the files that are currently marked */ public synchronized FileSet getMarkedFiles() { FileSet markedFiles = new FileSet(currentFolder, nbFilesMarked); int nbFiles = getFilesCount(); if (parent == null) { for (int i = 0; i < nbFiles; i++) { if (fileMarked[fileArrayIndex[i]]) { markedFiles.add(getFileAt(i)); } } } else { for (int i = 1, iMinusOne = 0; i < nbFiles; i++) { if (fileMarked[fileArrayIndex[iMinusOne]]) { markedFiles.add(getFileAt(i)); } iMinusOne = i; } } return markedFiles; } /** * Returns a CachedFile instance of the file located at the given row index. * This method can return the parent folder file ('..') if a parent exists and rowIndex is 0. * *

    Returns null if rowIndex is lower than 0 or is greater than or equals * {@link #getFilesCount() getFilesCount()}. * * @param row a row index, comprised between 0 and #getRowCount()-1 * @param col a column index, comprised between 0 and #getColumnCount()-1 * @return a CachedFile instance of the file located at the given row index */ public synchronized AbstractFile getCachedFileAt(int row, int col) { int index = getFileIndexAt(row, col); if (parent != null) { if (index == 0) { return parent; } index--; } // Need to check that row index is not larger than actual number of rows // because if table has just been changed (rows have been removed), // JTable may have an old row count value and may try to repaint rows that are out of bounds. if (index >= 0 && index < fileArrayIndex.length) { return cachedFiles[fileArrayIndex[index]]; } return null; } public synchronized AbstractFile getCachedFileAt(int index) { if (parent != null) { if (index == 0) { return parent; } index--; } // Need to check that row index is not larger than actual number of rows // because if table has just been changed (rows have been removed), // JTable may have an old row count value and may try to repaint rows that are out of bounds. if (index >= 0 && index < fileArrayIndex.length) { return cachedFiles[fileArrayIndex[index]]; } return null; } /** * Returns the file located at the given row index. * This method can return the parent folder file ('..') if a parent exists and rowIndex is 0. * *

    Returns null if rowIndex is lower than 0 or is greater than or equals * {@link #getFilesCount() getFilesCount()}. * * @param row a row index, comprised between 0 and #getRowCount()-1 * @param col a column index, comprised between 0 and #getColumnCount()-1 * @return the file located at the given row index */ public synchronized AbstractFile getFileAt(int row, int col) { AbstractFile file = getCachedFileAt(row, col); if (file instanceof CachedFile) { return ((CachedFile) file).getProxiedFile(); } return file; } /** * Returns the index of the row where the given file is located, -1 if the file is not in the * current folder. * * @param file the file for which to find the row index * @return the index of the file where the given file is located, -1 if the file is not in the * current folder */ public synchronized int getFileIndex(AbstractFile file) { // Handle parent folder file if (parent != null && file.equals(parent)) { return 0; } // Use dichotomic binary search rather than a dumb linear search since file array is sorted, complexity is // reduced to O(log n) instead of O(n^2) int left = parent == null ? 0 : 1; int right = getFilesCount() - 1; FileComparator fc = createFileComparator(sortInfo); while (left <= right) { int mid = (right-left)/2 + left; AbstractFile midFile = getCachedFileAt(mid); if (midFile.equals(file)) { return mid; } if (fc.compare(file, midFile) < 0) { right = mid - 1; } else { left = mid+1; } } return -1; } /** * Returns true if the given file is marked (/!\ not selected). If the specified row corresponds to the * special '..' parent file, false is always returned. * * @param row index of a row to test * @param col index of a column to test * @return true if the given row is marked */ public synchronized boolean isFileMarked(int row, int col) { if (row == 0 && col <= 0 && parent != null) { return false; } final int firstOffset = parent == null ? 0 : 1; int total = fileArrayIndex.length + firstOffset; //return row < total && rowMarked[fileArrayIndex[row - firstOffset]]; //row -= firstOffset; int index = getFileIndexAt(row, col); try { return index < total && fileMarked[fileArrayIndex[index - firstOffset]]; } catch (Exception e) { e.printStackTrace(); return false; } } public synchronized boolean isFileMarked(int index) { if (index == 0 && parent != null) { return false; } final int firstOffset = parent == null ? 0 : 1; int total = fileArrayIndex.length + firstOffset; try { return index < total && fileMarked[fileArrayIndex[index - firstOffset]]; } catch (Exception e) { e.printStackTrace(); return false; } } /** * Makes the name column temporarily editable. This method should only be called by FileTable. * * @param editable true to make the name column editable, false to prevent it from being edited */ public void setNameColumnEditable(boolean editable) { this.nameColumnEditable = editable; } /** * Marks/Unmarks the given row. If the specified row corresponds to the special '..' parent file, the row won't * be marked. * * @param index the file index to mark/unmark * @param marked true to mark the row, false to unmark it */ public synchronized void setFileMarked(int index, boolean marked) { if (index == 0 && parent != null) { return; } // Return if the row is already marked/unmarked final int fileIndex = fileArrayIndex[parent != null ? index - 1 : index]; // if((marked && rowMarked[fileIndex]) || (!marked && !rowMarked[fileIndex])) // return; if (marked == fileMarked[fileIndex]) { return; } AbstractFile file = getCachedFileAt(index); // Do not call getSize() on directories, it's unnecessary and the value is most likely not cached by CachedFile yet long fileSize; if (file.isDirectory()) { markedDirectories.add(file); fileSize = 0; } else { fileSize = file.getSize(); } // Update : // - Combined size of marked files // - marked files FileSet if (marked) { // File size can equal -1 if not available, do not count that in total if (fileSize > 0) { markedTotalSize += fileSize; } nbFilesMarked++; } else { // File size can equal -1 if not available, do not count that in total if (fileSize > 0) { markedTotalSize -= fileSize; } nbFilesMarked--; } fileMarked[fileIndex] = marked; } /** * Returns the number of marked files. This number is pre-calculated so calling this method is much faster than * retrieving the list of marked files and counting them. * * @return the number of marked files */ public int getNbMarkedFiles() { return nbFilesMarked; } /** * Returns the combined size of marked files. This number consists of two parts: * 1) pre-calculated size of files so calling this method is much faster * than retrieving the list of marked files and calculating their combined size. * 2) calculated size of directories (if that was calculated) * * @return the combined size of marked files and directories */ public long getTotalMarkedSize() { return markedTotalSize + calcMarkedDirectoriesSize(); } /** * Add directory to size calculation and start calculation worker if it doesn't busy * @param table file table * @param file directory to add */ public void startDirectorySizeCalculation(FileTable table, AbstractFile file) { if (!file.isDirectory()) { return; } hasCalculatedDirectories = true; synchronized (directorySizes) { if (directorySizes.containsKey(file)) { return; } } synchronized (calculateSizeQueue) { if (calculateSizeQueue.contains(file)) { return; } calculateSizeQueue.add(file); } if (calculateDirectorySizeWorker == null) { processNextQueuedFile(table); } } /** * Takes a first ask for queue and starts calculation worker * @param table file table */ private void processNextQueuedFile(FileTable table) { AbstractFile nextFile; synchronized (calculateSizeQueue) { nextFile = calculateSizeQueue.isEmpty() ? null : calculateSizeQueue.removeFirst(); } if (nextFile == null) { calculateDirectorySizeWorker = null; table.getParent().setCursor(Cursor.getDefaultCursor()); } else { calculateDirectorySizeWorker = new CalculateDirectorySizeWorker(this, table, nextFile); table.getParent().setCursor(WAIT_CURSOR); calculateDirectorySizeWorker.execute(); } } /** * Called from size-calculation worker after it finish or requests to repaint table. * Updates map of directory sizes and starts next task if worker finished * * @param path directory to process * @param table file table * @param size calculated directory size * @param finish true if worker completely finish task, false if it will just repaint table */ public void addProcessedDirectory(AbstractFile path, FileTable table, long size, boolean finish) { synchronized (directorySizes) { directorySizes.put(path, size); } synchronized (calculateSizeQueue) { calculateSizeQueue.remove(path); } if (finish) { processNextQueuedFile(table); } } /** * Stops directory calculation, clears calculated size ant tasks queue, interrupts currently executed worker if exists */ private void stopSizeCalculation() { synchronized (directorySizes) { directorySizes.clear(); } synchronized (calculateSizeQueue) { calculateSizeQueue.clear(); } if (calculateDirectorySizeWorker != null) { try { calculateDirectorySizeWorker.cancel(true); } catch (Exception ignore) { } calculateDirectorySizeWorker = null; } synchronized (this) { markedDirectories.clear(); } hasCalculatedDirectories = false; } private long calcMarkedDirectoriesSize() { if (!hasCalculatedDirectories) { return 0; } long result = 0; synchronized (this) { for (AbstractFile file : markedDirectories) { Long dirSize; synchronized (directorySizes) { dirSize = directorySizes.get(file); } if (dirSize != null) { result += dirSize; } } } return result; } public String getFileNameAt(int index) { return (index == 0 && hasParentFolder()) ? ".." : getFileAt(index).getName(); } public synchronized int getFilesCount() { return fileArrayIndex.length + (parent == null ? 0 : 1); } // public void setFirstVisibleRow(int row) { // this.firstVisibleRow = row; // } public AbstractFile getCurrentCalculatedSizeDirectory() { if (calculateDirectorySizeWorker != null) { return calculateDirectorySizeWorker.getFile(); } return null; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/TableViewMode.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.compact.CompactFileTableCellRenderer; import com.mucommander.ui.main.table.views.full.FileTableCellRenderer; /** * @author Oleg Trifonov * Created on 03/04/15. */ public enum TableViewMode { FULL(Column.values().length) { @Override public BaseCellRenderer createCellRenderer(FileTable table) { return new FileTableCellRenderer(table); } }, COMPACT(2) { @Override public BaseCellRenderer createCellRenderer(FileTable table) { return new CompactFileTableCellRenderer(table); } }, SHORT(3) { @Override public BaseCellRenderer createCellRenderer(FileTable table) { return new CompactFileTableCellRenderer(table); } }; private final int columns; TableViewMode(int columns) { this.columns = columns; } public int getColumnsCount() { return columns; } public abstract BaseCellRenderer createCellRenderer(FileTable table); } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/compact/CompactFileTableCellRenderer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.compact; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.table.CellLabel; import com.mucommander.ui.main.table.FileGroupResolver; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseCellRenderer; import com.mucommander.ui.quicksearch.QuickSearch; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.utils.FileIconsCache; import javax.swing.JTable; import javax.swing.table.TableColumn; import java.awt.Color; import java.awt.Component; /** * @author Oleg Trifonov * Created on 04/04/15. */ public class CompactFileTableCellRenderer extends BaseCellRenderer { private final CellLabel emptyLabel = new CellLabel(); public CompactFileTableCellRenderer(FileTable table) { super(table); this.cellLabels = new CellLabel[table.getColumnCount()]; for (int i = 0; i < cellLabels.length; i++) { this.cellLabels[i] = new CellLabel(); } } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { // Need to check that row index is not out of bounds because when the folder // has just been changed, the JTable may try to repaint the old folder and // ask for a row index greater than the length if the old folder contained more files if (row < 0 || row >= tableModel.getRowCount()) { return null; } CompactFileTableModel model = (CompactFileTableModel)tableModel; // Sanity check. final AbstractFile file = model.getFileAt(row, column); if (file == null) { debug("tableModel.getCachedFileAtRow( " + row + ") RETURNED NULL !"); // emptyLabel.setupText("", 0); // emptyLabel.setIcon(null); // final QuickSearch search = this.table.getQuickSearch(); // int matchesColorIndex; // if (table.hasFocus() && search.isActive()) { // matchesColorIndex = ThemeCache.NORMAL; // } else { // matchesColorIndex = (row % 2 == 0) ? ThemeCache.NORMAL : ThemeCache.ALTERNATE; // } final int focusedIndex = table.hasFocus() ? ThemeCache.ACTIVE : ThemeCache.INACTIVE; // emptyLabel.setBackground(ThemeCache.backgroundColors[focusedIndex][matchesColorIndex]); emptyLabel.setBackground(ThemeCache.backgroundColors[focusedIndex][ThemeCache.NORMAL]); emptyLabel.setHasSeparator(column < tableModel.getColumnCount()-1); return emptyLabel; } final QuickSearch search = this.table.getQuickSearch(); final boolean matches = !table.hasFocus() || !search.isActive() || (file != tableModel.getParentFolder() && search.matches(file.getName())); // Retrieves the various indexes of the colors to apply. // Selection only applies when the table is the active one final int selectedIndex = (isSelected && ((FileTable)table).isActiveTable()) ? ThemeCache.SELECTED : ThemeCache.NORMAL; final int focusedIndex = table.hasFocus() ? ThemeCache.ACTIVE : ThemeCache.INACTIVE; final int fileIndex = model.getFileIndexAt(row, column); final int colorIndex = getFileColorIndex(fileIndex, file, tableModel); final CellLabel label = cellLabels[column]; label.setIcon(fileIndex == 0 && tableModel.hasParentFolder() ? IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.PARENT_FOLDER_ICON_NAME, FileIcons.getScaleFactor()) : FileIconsCache.getInstance().getIcon(file)); String text = (String)value; final TableColumn tableColumn = table.getColumnModel().getColumn(column); label.setupText(text, tableColumn.getWidth()); // Set foreground color Color foregroundColor; if (matches || isSelected) { int group = (selectedIndex == ThemeCache.SELECTED) ? -1 : FileGroupResolver.getInstance().resolve(file); if (group >= 0 && colorIndex != ThemeCache.MARKED) { foregroundColor = ThemeCache.groupColors[group]; } else { foregroundColor = ThemeCache.foregroundColors[focusedIndex][selectedIndex][colorIndex]; } } else { foregroundColor = ThemeCache.unmatchedForeground; } label.setForeground(foregroundColor); // Set background color depending on whether the row is selected or not, and whether the table has focus or not if (selectedIndex == ThemeCache.SELECTED) { label.setBackground(ThemeCache.backgroundColors[focusedIndex][ThemeCache.SELECTED], ThemeCache.backgroundColors[focusedIndex][ThemeCache.SECONDARY]); } else if (matches) { int matchesColorIndex; if (table.hasFocus() && search.isActive()) { matchesColorIndex = ThemeCache.NORMAL; } else { matchesColorIndex = (row % 2 == 0) ? ThemeCache.NORMAL : ThemeCache.ALTERNATE; } label.setBackground(ThemeCache.backgroundColors[focusedIndex][matchesColorIndex]); } else { label.setBackground(ThemeCache.unmatchedBackground); } if (selectedIndex == ThemeCache.SELECTED) { label.setOutline(table.hasFocus() ? ThemeCache.activeOutlineColor : ThemeCache.inactiveOutlineColor); } else { label.setOutline(null); } label.setHasSeparator(column < tableModel.getColumnCount()-1); return label; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/compact/CompactFileTableColumnModel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.compact; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.FileTableHeaderRenderer; import com.mucommander.ui.main.table.views.full.FileTableConfiguration; import javax.swing.DefaultListSelectionModel; import javax.swing.ListSelectionModel; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.WeakHashMap; /** * @author Oleg Trifonov * Created on 04/04/15. */ public class CompactFileTableColumnModel implements TableColumnModel, PropertyChangeListener { /** Even though we're not using column selection, the table API forces us to return this instance or will crash. */ private static final ListSelectionModel SELECTION_MODEL = new DefaultListSelectionModel(); /** If {@link #widthCache} is set to this, it needs to be recalculated. */ private static final int CACHE_OUT_OF_DATE = -1; private final TableColumn[] columns; /** Cache for the table's total width. */ private int widthCache = CACHE_OUT_OF_DATE; /** All registered listeners. */ private final WeakHashMap listeners = new WeakHashMap<>(); public CompactFileTableColumnModel(int columns, FileTableConfiguration conf) { super(); this.columns = new TableColumn[columns]; for (int i = 0; i < columns; i++) { TableColumn column = new TableColumn(); column.setCellEditor(null); column.setHeaderValue("Name"); column.addPropertyChangeListener(this); column.setMinWidth(200); column.setModelIndex(i); this.columns[i] = column; // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers. // On other platforms, we use a custom table header renderer. if (!FileTable.usesTableHeaderRenderingProperties()) { column.setHeaderRenderer(new FileTableHeaderRenderer()); } column.addPropertyChangeListener(this); } } @Override public void addColumn(TableColumn aColumn) { } @Override public void removeColumn(TableColumn column) { } @Override public void moveColumn(int columnIndex, int newIndex) { } /** * Ignored. */ @Override public void setColumnMargin(int newMargin) { } @Override public int getColumnCount() { return columns.length; } /** * Returns an enumeration on all visible columns. * @return an enumeration on all visible columns. */ @Override public Enumeration getColumns() { return new ColumnEnumeration(); } @Override public int getColumnIndex(Object columnIdentifier) { return 0; } @Override public TableColumn getColumn(int columnIndex) { return columns[columnIndex]; } /** * Returns 0. * @return 0. */ @Override public int getColumnMargin() { return 0; } /** * Returns the index of the column at the specified position. * @param x position of the column to look for. * @return the index of the column at the specified position, -1 if not found. */ @Override public int getColumnIndexAtX(int x) { int count = getColumnCount(); for (int i = 0; i < count; i++) { x = x - getColumn(i).getWidth(); if (x < 0) { return i; } } return -1; } @Override public int getTotalColumnWidth() { if (widthCache == CACHE_OUT_OF_DATE) { computeWidthCache(); } return widthCache; } /** * Computes the model's width. */ private void computeWidthCache() { Enumeration elements = getColumns(); widthCache = 0; while (elements.hasMoreElements()) { widthCache += elements.nextElement().getWidth(); } } /** * Ignored. */ @Override public void setColumnSelectionAllowed(boolean flag) { } @Override public boolean getColumnSelectionAllowed() { return true; } /** * Returns an integer array of size 0. * @return an integer array of size 0. */ @Override public int[] getSelectedColumns() { return new int[]{0}; } /** * Returns 0. * @return 0. */ @Override public int getSelectedColumnCount() { return 1; } /** * Ignored. */ @Override public void setSelectionModel(ListSelectionModel newModel) { } /** * Returns a default list selection model. *

    * Ideally, we'd like to return null here, but the table API takes a dim view * of this and we're forced to keep a useless reference. * * @return a default list selection model. */ @Override public ListSelectionModel getSelectionModel() { return SELECTION_MODEL; } @Override public void addColumnModelListener(TableColumnModelListener listener) { listeners.put(listener, null); } @Override public void removeColumnModelListener(TableColumnModelListener listener) { listeners.remove(listener); } @Override public void propertyChange(PropertyChangeEvent evt) { } /** * Browses through the model's visible columns * @author Oleg Trifonov */ private class ColumnEnumeration implements Enumeration { /** Index of the next available element in the enumeration. */ private int nextIndex; /** * Creates a new column enumeration. */ ColumnEnumeration() { nextIndex = 0; } /** * Returns true if there's a next element in the enumeration. * @return true if there's a next element in the enumeration, false otherwise. */ public boolean hasMoreElements() { return nextIndex < columns.length; } /** * Returns the next element in the enumeration. * @return the next element in the enumeration. * @throws NoSuchElementException if there is no next element in the enumeration. */ public TableColumn nextElement() { // Makes sure we have at least one more element to return. if (!hasMoreElements()) { throw new NoSuchElementException(); } return columns[nextIndex++]; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/compact/CompactFileTableModel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.compact; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.views.BaseFileTableModel; /** * @author Oleg Trifonov * Created on 04/04/15. */ public class CompactFileTableModel extends BaseFileTableModel { final int columns; private int visibleRows; // private int rowCount; private int offset; /** Cell values cache */ private String[] cellValuesCache; public CompactFileTableModel(int columns, int visibleRows) { super(); this.columns = columns; this.visibleRows = visibleRows; //rowCount = calcRowCount(); } @Override public synchronized void setupFromModel(BaseFileTableModel model) { super.setupFromModel(model); initCellValuesCache(); } private int calcRowCount() { int files = getFileCount(); if (parent != null ) { files++; } if (files <= visibleRows) { return files; } int rows = files / columns; if (files % columns != 0) { rows++; } return Math.max(visibleRows, rows); } public synchronized void setCurrentFolder(AbstractFile folder, AbstractFile children[], FileTable table) { super.setCurrentFolder(folder, children, table); //rowCount = calcRowCount(); } @Override public void fillCellCache(FileTable fileTable) { int len = cellValuesCache.length; if (len == 0) { return; } // Special '..' file if (parent != null) { // cellValuesCache[0] = ".."; currentFolderDateSnapshot = currentFolder.getLastModifiedDate(); } for (int i = 0; i < len; i++) { cellValuesCache[i] = null; } // int fileIndex = 0; // final int indexOffset = parent == null ? 0 : 1; // for (int i = indexOffset; i < len; i++) { // int cellIndex = fileIndex + indexOffset; // cellValuesCache[cellIndex] = null; // fileIndex++; // } } /** * Init and fill cell cache to speed up table even more */ @Override protected void initCellValuesCache() { //int files = parent == null ? getFileCount() : getFileCount() + 1; this.cellValuesCache = new String[getFileCount()]; } @Override public int getRowCount() { return visibleRows; } @Override public int getColumnCount() { return columns; } @Override public Object getValueAt(int row, int column) { int fileIndex = getFileIndexAt(row, column); if (parent != null) { // Handle special '..' file if (fileIndex == 0) { return ".."; } fileIndex--; } // Need to check that file index is not larger than actual number of files if (fileIndex < 0 || fileIndex >= fileArrayIndex.length) { return null; } int index = fileArrayIndex[fileIndex]; String result = cellValuesCache[index]; if (result == null) { result = fillOneCellCache(parent != null ? fileIndex + 1 : fileIndex); cellValuesCache[index] = result; } // TODO preload icons for all visible files return result; //return fileIndex + ":" + offset + ":" + result; } /** * Returns true if name column has temporarily be made editable by FileTable * and given row doesn't correspond to parent file '..', false otherwise. */ @Override public boolean isCellEditable(int row, int column) { // Name column can temporarily be made editable by FileTable // but parent file '..' name should never be editable return nameColumnEditable && (parent == null || row != 0 || column != 0 || offset != 0); } private String fillOneCellCache(int fileIndex) { AbstractFile file = getCachedFileAt(fileIndex); return file.getName(); } public AbstractFile getFileAt(int row, int column) { int index = offset + row + column * visibleRows; return (index == 0 && hasParentFolder()) ? parent : getFileAt(index); } public int getFileIndexAt(int row, int column) { if (row < 0 || column < 0) { return -1; } return offset + row + column * visibleRows; } @Override public int getFileRow(int index) { return (index - offset) % visibleRows; } public String getFileNameAt(int row, int column) { int index = offset + row + column * visibleRows; return (index == 0 && hasParentFolder()) ? ".." : getFileAt(index).getName(); } public int getVisibleRows() { return visibleRows; } public void setVisibleRows(int visibleRows) { this.visibleRows = visibleRows; //rowCount = calcRowCount(); fireTableDataChanged(); } public int getOffset() { return offset; } public void setOffset(int offset) { this.offset = offset; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/full/FileTableCellRenderer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.full; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.table.*; import com.mucommander.ui.main.table.views.BaseCellRenderer; import com.mucommander.ui.quicksearch.QuickSearch; import com.mucommander.ui.theme.*; import com.mucommander.utils.FileIconsCache; import ru.trolsoft.macosx.FileLabelCache; import javax.swing.*; import javax.swing.table.TableColumn; import java.awt.Color; import java.awt.Component; /** * The custom TableCellRenderer class used by {@link FileTable} to render all table cells. * *

    Quote from Sun's Javadoc : The table class defines a single cell renderer and uses it as a * as a rubber-stamp for rendering all cells in the table; it renders the first cell, * changes the contents of that cell renderer, shifts the origin to the new location, re-draws it, and so on. * *

    This TableCellRender is written from scratch instead of overriding DefaultTableCellRender * to provide a more efficient (and more specialized) implementation: each column is rendered using a dedicated * {@link com.mucommander.ui.main.table.CellLabel CellLabel} which takes into account the column's specificities. * Having a dedicated for each column avoids calling the label's set methods (alignment, border, font...) * each time {@link #getTableCellRendererComponent(javax.swing.JTable, Object, boolean, boolean, int, int)}} * is invoked, making cell rendering faster. * *

    Contrarily to DefaultTableCellRender, FileTableCellRenderer does not extend JLabel, * instead the dedicated {@link CellLabel} class is used to render cells, making the implementation * less confusing IMO. * * @author Maxence Bernard, Nicolas Rinaudo */ public class FileTableCellRenderer extends BaseCellRenderer { private static int progressIndicatorCounter; private final CellLabel transparentLabel = new TransparentCellLabel(); public FileTableCellRenderer(FileTable table) { super(table); this.cellLabels = new CellLabel[Column.values().length]; // create a label for each column for (Column c : Column.values()) { this.cellLabels[c.ordinal()] = new CellLabel(); } // Set labels' font. setCellLabelsFont(ThemeCache.tableFont); // Set labels' text alignment cellLabels[Column.EXTENSION.ordinal()].setHorizontalAlignment(CellLabel.CENTER); cellLabels[Column.NAME.ordinal()].setHorizontalAlignment(CellLabel.LEFT); cellLabels[Column.SIZE.ordinal()].setHorizontalAlignment(CellLabel.RIGHT); cellLabels[Column.DATE.ordinal()].setHorizontalAlignment(CellLabel.RIGHT); cellLabels[Column.PERMISSIONS.ordinal()].setHorizontalAlignment(CellLabel.LEFT); cellLabels[Column.OWNER.ordinal()].setHorizontalAlignment(CellLabel.LEFT); cellLabels[Column.GROUP.ordinal()].setHorizontalAlignment(CellLabel.LEFT); // Listens to certain configuration variables ThemeCache.addThemeListener(this); } /////////////////////////////// // TableCellRenderer methods // /////////////////////////////// public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { // Need to check that row index is not out of bounds because when the folder // has just been changed, the JTable may try to repaint the old folder and // ask for a row index greater than the length if the old folder contained more files if (row < 0 || row >= tableModel.getRowCount()) { return null; } // Sanity check. final AbstractFile file = tableModel.getCachedFileAt(row, col); if (file == null) { debug("tableModel.getCachedFileAtRow( " + row + ") RETURNED NULL !"); return null; } boolean isCalculatedSizeDir = file.isDirectory() && tableModel.getCurrentCalculatedSizeDirectory() == file; final QuickSearch search = this.table.getQuickSearch(); //final boolean matches = !table.hasFocus() || !search.isActive() || search.matches(this.tableModel.getFileNameAt(row)); final boolean searchMatches = search.isActive() && search.matches(file); //final boolean matches = !table.hasFocus() || !search.isActive() || searchMatches; final boolean matches = !search.isActive() || searchMatches; // Retrieves the various indexes of the colors to apply. // Selection only applies when the table is the active one final int selectedIndex = (isSelected && ((FileTable)table).isActiveTable()) ? ThemeCache.SELECTED : ThemeCache.NORMAL; final int focusedIndex = table.hasFocus() ? ThemeCache.ACTIVE : ThemeCache.INACTIVE; final int colorIndex = getFileColorIndex(row, file, tableModel); final Column column = Column.valueOf(table.convertColumnIndexToModel(col)); CellLabel label = cellLabels[column.ordinal()]; if (isCalculatedSizeDir && column == Column.NAME) { label.setProgressValue(progressIndicatorCounter++); } else { label.setProgressValue(0); } // Extension/icon column: return ImageIcon instance if (column == Column.EXTENSION) { if (search.isActive() && !searchMatches && row != 0) { label = transparentLabel; } label.setIcon(row == 0 && tableModel.hasParentFolder() ? IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.PARENT_FOLDER_ICON_NAME, FileIcons.getScaleFactor()) // : FileIcons.getFileIcon(file)); : FileIconsCache.getInstance().getIcon(file)); } else { // Any other column (name, date or size) String text = (String)value; Color foregroundColor; if (matches || isSelected) { int group = (selectedIndex == ThemeCache.SELECTED) ? -1 : FileGroupResolver.getInstance().resolve(file); if (group >= 0 && colorIndex != ThemeCache.MARKED) { foregroundColor = ThemeCache.groupColors[group]; } else { foregroundColor = ThemeCache.foregroundColors[focusedIndex][selectedIndex][colorIndex]; } } else { foregroundColor = ThemeCache.unmatchedForeground; } label.setForeground(foregroundColor); // Set the label's text, before calculating it width // label.setText(text); final TableColumn tableColumn = table.getColumnModel().getColumn(col); label.setupText(text, tableColumn.getWidth()); // If label's width is larger than the column width: // - truncate the text from the center and equally to the left and right sides, adding an ellipsis ('...') // where characters have been removed. This allows both the start and end of filename to be visible. // - set a tooltip text that will display the whole text when mouse is over the label /* final TableColumn tableColumn = table.getColumnModel().getColumn(columnIndex); if (tableColumn.getWidth() < label.getPreferredSize().getWidth()) { final int tl = text.length(); final int tl2 = tl/2; String leftText = text.substring(0, tl2); String rightText = text.substring(tl2, tl); while (tableColumn.getWidth() < label.getPreferredSize().getWidth() && !leftText.isEmpty() && !rightText.isEmpty()) { // Prevents against going out of bounds final int ltl = leftText.length(); final int rtl = rightText.length(); if (ltl > rtl) { leftText = leftText.substring(0, ltl - 1); } else { rightText = rightText.substring(1, rtl); } label.setText(leftText + DOTS + rightText); } // Set the tool tip label.setToolTipText(text); } else { // Have to set it to null otherwise the defaultRender sets the tooltip text to the last one specified label.setToolTipText(null); } */ } // Set background color depending on whether the row is selected or not, and whether the table has focus or not if (selectedIndex == ThemeCache.SELECTED) { label.setBackground(ThemeCache.backgroundColors[focusedIndex][ThemeCache.SELECTED], ThemeCache.backgroundColors[focusedIndex][ThemeCache.SECONDARY]); } else if (matches) { int matchesColorIndex; if (table.hasFocus() && search.isActive()) { matchesColorIndex = ThemeCache.NORMAL; } else { matchesColorIndex = (row % 2 == 0) ? ThemeCache.NORMAL : ThemeCache.ALTERNATE; } label.setBackground(ThemeCache.backgroundColors[focusedIndex][matchesColorIndex]); } else { label.setBackground(ThemeCache.unmatchedBackground); } if (selectedIndex == ThemeCache.SELECTED) { label.setOutline(table.hasFocus() ? ThemeCache.activeOutlineColor : ThemeCache.inactiveOutlineColor); } else { label.setOutline(null); } if (column == Column.NAME) { label.setMarkerColor(FileLabelCache.getInstance().getLabelColor(file)); } else { label.setMarkerColor(null); } return label; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/full/FileTableColumnModel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.full; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.FileTable; import com.mucommander.ui.main.table.FileTableHeaderRenderer; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.TableColumnModelEvent; import javax.swing.event.TableColumnModelListener; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.*; /** * Used to keep track of a file table's columns position and visibility settings. * @author Nicolas Rinaudo, Maxence Bernard */ public class FileTableColumnModel implements TableColumnModel, PropertyChangeListener { /** If {@link #widthCache} is set to this, it needs to be recalculated. */ private static final int CACHE_OUT_OF_DATE = -1; /** Even though we're not using column selection, the table API forces us to return this instance or will crash. */ private static final ListSelectionModel SELECTION_MODEL = new DefaultListSelectionModel(); /** All registered listeners. */ private final WeakHashMap listeners = new WeakHashMap<>(); /** Cache for the table's total width. */ private int widthCache = CACHE_OUT_OF_DATE; /** All available columns. */ private final List columns = new ArrayList<>(Column.values().length); /** Enabled state of each column. */ private final boolean[] enabled = new boolean[Column.values().length]; /** Visibility state of each column. */ private final boolean[] visibility = new boolean[Column.values().length]; /** Cache for the number of available columns. */ private int countCache; /** Whether the column sizes were set already. */ private boolean columnSizesSet; private int[] internalIndexCache; /** * Creates a new file table column model. */ public FileTableColumnModel(FileTableConfiguration conf) { // The name column is always visible, so we know that the column count is always // at least 1. countCache = 1; // Initializes the columns. for (Column c : Column.values()) { int columnIndex = c.ordinal(); TableColumn column = new TableColumn(columnIndex); // Buffer for the current column. columns.add(column); column.setCellEditor(null); column.setHeaderValue(c.getLabel()); // Mac OS X 10.5 (Leopard) and up uses JTableHeader properties to render sort indicators on table headers. // On other platforms, we use a custom table header renderer. if (!FileTable.usesTableHeaderRenderingProperties()) { column.setHeaderRenderer(new FileTableHeaderRenderer()); } column.addPropertyChangeListener(this); // Sets the column's initial width. if (conf.getWidth(c) != 0) { column.setWidth(conf.getWidth(c)); } // Initializes the column's visibility and minimum width. if (c == Column.NAME) { enabled[columnIndex] = true; } else { if ((enabled[columnIndex] = conf.isEnabled(c))) { countCache++; } } column.setMinWidth(c.getMinimumColumnWidth()); // Init visibility state to enabled state, FileTable will adjust the values for conditional columns later visibility[columnIndex] = enabled[columnIndex]; } // Sorts the columns. columns.sort(new ColumnSorter(conf)); } public synchronized FileTableConfiguration getConfiguration() { FileTableConfiguration conf = new FileTableConfiguration(); for (Column c : Column.values()) { TableColumn column = columns.get(c.ordinal()); Column modelC = Column.valueOf(column.getModelIndex()); int modelCIndex = modelC.ordinal(); conf.setEnabled(modelC, enabled[modelCIndex]); conf.setPosition(modelC, c.ordinal()); conf.setWidth(modelC, column.getWidth()); } return conf; } /** * Returns true if the specified column is enabled. * @param column column, see {@link Column} for possible values * @return true if the column is enabled, false if disabled. */ public synchronized boolean isColumnEnabled(Column column) { return enabled[column.ordinal()]; } /** * Sets the specified column's enabled state. * @param column column, see {@link Column} for possible values * @param enabled true to enable the column, false to disable it. */ public synchronized void setColumnEnabled(Column column, boolean enabled) { this.enabled[column.ordinal()] = enabled; } /** * Sets the specified column's visibility state. * @param column column, see {@link Column} for possible values * @param visible whether the column should be visible or not. */ public synchronized void setColumnVisible(Column column, boolean visible) { // Ignores calls that won't actually change anything. int columnVal = column.ordinal(); if (visibility[columnVal] != visible) { visibility[columnVal] = visible; widthCache = CACHE_OUT_OF_DATE; if (visible) { // Adds the column. countCache++; triggerColumnAdded(new TableColumnModelEvent(this, columnVal, columnVal)); } else { // Removes the column. countCache--; triggerColumnRemoved(new TableColumnModelEvent(this, columnVal, columnVal)); } internalIndexCache = null; } } /** * Returns true if the specified column is visible. * @param column column, see {@link Column} for possible values * @return true if the specified column is visible, false otherwise. */ public boolean isColumnVisible(Column column) { return visibility[column.ordinal()]; } /** * Adds the specified column to the model. * @param column column to add to the model. */ public void addColumn(TableColumn column) { setColumnVisible(Column.valueOf(column.getModelIndex()), true); } /** * Removes the specified column from the model. * @param column column to remove from the model. */ public void removeColumn(TableColumn column) { setColumnVisible(Column.valueOf(column.getModelIndex()), false); } // - Column retrieval ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- private int getInternalIndex(int index) { if (internalIndexCache != null) { return internalIndexCache[index]; } // // Looks for the visible column of index 'index'. // int visibleIndex = -1; // for (int i = 0; i < visibility.length; i++) { // TableColumn column = columns.get(i); // if (visibility[column.getModelIndex()]) { // if (++visibleIndex == index) { // return i; // } // } // } // // Index doesn't exist. // throw new ArrayIndexOutOfBoundsException(Integer.toString(index)); // Looks for the visible column of index 'index'. buildColumnIndexCache(); return internalIndexCache[index]; } private synchronized void buildColumnIndexCache() { internalIndexCache = new int[visibility.length]; int visibleIndex = 0; for (int i = 0; i < visibility.length; i++) { TableColumn column = columns.get(i); if (visibility[column.getModelIndex()]) { internalIndexCache[visibleIndex++] = i; } } while (visibleIndex < visibility.length) { internalIndexCache[visibleIndex++] = -1; } } /** * Returns the specified column. * @param index index of the column in the model. * @return the requested column. */ public synchronized TableColumn getColumn(int index) { return columns.get(getInternalIndex(index)); } public synchronized TableColumn getColumnFromId(int id) { return columns.get(id); } public synchronized int getColumnPosition(int id) { for (int i = 0; i < visibility.length; i++) { if (columns.get(i).getModelIndex() == id) { return i; } } return -1; } /** * Returns the number of columns currently displayed. * @return the number of columns currently displayed. */ public synchronized int getColumnCount() {return countCache;} /** * Moves a column. * @param from index of the column to move. * @param to where to move the column. */ public synchronized void moveColumn(int from, int to) { // We need to trigger these for the file table to display the 'column dragging' animation. if (from == to) { triggerColumnMoved(new TableColumnModelEvent(this, from, to)); return; } // Locates the internal index of the requested column // and removes that column int index = getInternalIndex(from); // Used to store the internal index of 'from' and 'to'. TableColumn column = columns.get(index); // Buffer for the table to remove. columns.remove(index); // If the column needs to be moved at the end of the set, no need to locate its correct index. if (to == countCache - 1) { columns.add(column); } else { // Otherwise, finds the column's internal index and inserts it there. index = getInternalIndex(to); columns.add(index, column); } // Notifies listeners and stores the new configuration. triggerColumnMoved(new TableColumnModelEvent(this, from, to)); internalIndexCache = null; } public int getColumnIndex(Object identifier) { return 0; } // - Columns width ------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Returns the index of the column at the specified position. * @param x position of the column to look for. * @return the index of the column at the specified position, -1 if not found. */ @Override public int getColumnIndexAtX(int x) { int count = getColumnCount(); for (int i = 0; i < count; i++) { x = x - getColumn(i).getWidth(); if (x < 0) { return i; } } return -1; } /** * Returns the total width of the table column model. * @return the total width of the table column model. */ public int getTotalColumnWidth() { if (widthCache == CACHE_OUT_OF_DATE) { computeWidthCache(); } return widthCache; } /** * Computes the model's width. */ private void computeWidthCache() { Enumeration elements = getColumns(); widthCache = 0; while (elements.hasMoreElements()) { widthCache += elements.nextElement().getWidth(); } } /** * Invalidates the width cache if a column's width has changed. */ public void propertyChange(PropertyChangeEvent event) { String name = event.getPropertyName(); if ("width".equals(name)) { columnSizesSet = true; widthCache = CACHE_OUT_OF_DATE; // Notifies the table that columns width have changed and that it should repaint itself. triggerColumnMarginChanged(new ChangeEvent(this)); } } public boolean wereColumnSizesSet() { return columnSizesSet; } // - Columns margin ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- // Column margin is fixed to 0. /** * Returns 0. * @return 0. */ @Override public int getColumnMargin() { return 0; } /** * Ignored. */ @Override public void setColumnMargin(int margin) {} // - Listeners ----------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Registers the specified column model listener. * @param listener listener to register. */ public void addColumnModelListener(TableColumnModelListener listener) { listeners.put(listener, null); } /** * Removes the specified column model listener. * @param listener listener to remove. */ public void removeColumnModelListener(TableColumnModelListener listener) { listeners.remove(listener); } /** * Calls all registered listeners' columnAdded(event). * @param event event to propagate. */ private void triggerColumnAdded(TableColumnModelEvent event) { for (TableColumnModelListener listener: listeners.keySet()) { listener.columnAdded(event); } } /** * Calls all registered listeners' columnMarginChanged(event). * @param event event to propagate. */ private void triggerColumnMarginChanged(ChangeEvent event) { for (TableColumnModelListener listener: listeners.keySet()) { listener.columnMarginChanged(event); } } /** * Calls all registered listeners' columnMoved(event). * @param event event to propagate. */ private void triggerColumnMoved(TableColumnModelEvent event) { for (TableColumnModelListener listener: listeners.keySet()) { listener.columnMoved(event); } } /** * Calls all registered listeners' columnRemoved(event). * @param event event to propagate. */ private void triggerColumnRemoved(TableColumnModelEvent event) { for (TableColumnModelListener listener: listeners.keySet()) { listener.columnRemoved(event); } } // - Column selection ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- // Column selection is not allowed, this methods are ignored or return default values. /** * Returns false. * @return false. */ public boolean getColumnSelectionAllowed() { return false; } /** * Returns 0. * @return 0. */ @Override public int getSelectedColumnCount() { return 0; } /** * Returns an integer array of size 0. * @return an integer array of size 0. */ @Override public int[] getSelectedColumns() { return new int[0]; } /** * Ignored. */ public void setColumnSelectionAllowed(boolean flag) {} /** * Returns a default list selection model. *

    * Ideally, we'd like to return null here, but the table API takes a dim view * of this and we're forced to keep a useless reference. * * @return a default list selection model. */ public ListSelectionModel getSelectionModel() {return SELECTION_MODEL;} /** * Ignored. */ public void setSelectionModel(ListSelectionModel model) {} // - Column enumeration -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Returns an enumeration on all visible columns. * @return an enumeration on all visible columns. */ public Enumeration getColumns() { return new ColumnEnumeration(); } public Iterator getAllColumns() { return columns.iterator(); } /** * Browses through the model's visible columns *

    * This will enumerate all the elements of {@link FileTableColumnModel#columns}, skipping * over any that's marked as invisible. * * @author Nicolas Rinaudo */ private class ColumnEnumeration implements Enumeration { /** Index of the next available element in the enumeration. */ private int nextIndex; /** * Creates a new column enumeration. */ ColumnEnumeration() { nextIndex = -1; findNextElement(); } /** * Finds the next visible element in the model. */ private void findNextElement() { for (nextIndex++; nextIndex < visibility.length; nextIndex++) { TableColumn column = columns.get(nextIndex); if (visibility[column.getModelIndex()]) { break; } } } /** * Returns true if there's a next element in the enumeration. * @return true if there's a next element in the enumeration, false otherwise. */ public boolean hasMoreElements() { return nextIndex < visibility.length; } /** * Returns the next element in the enumeration. * @return the next element in the enumeration. * @throws NoSuchElementException if there is no next element in the enumeration. */ public TableColumn nextElement() { // Makes sure we have at least one more element to return. if (!hasMoreElements()) { throw new NoSuchElementException(); } // Retrieves the next element. TableColumn column = columns.get(nextIndex); // Looks for the next one. findNextElement(); return column; } } // - Column sorting ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Used to sort the model's columns at boot time. *

    * The sort is done by first comparing each column's index as defined in the configuration and, * if there's a conflict, by comparing each column's identifier. * * @author Nicolas Rinaudo */ private static class ColumnSorter implements Comparator { /** Defines the columns order. */ private final FileTableConfiguration conf; /** * Loads the columns order as defined in the configuration. */ ColumnSorter(FileTableConfiguration conf) { this.conf = conf; } /** * Compares o1 and o2. */ public int compare(TableColumn tc1, TableColumn tc2) { int id1 = tc1.getModelIndex(); // Identifier of the first column. int id2 = tc2.getModelIndex(); // Identifier of the second column. // Retrieves the two columns' indexes and identifiers. int index1 = conf.getPosition(Column.valueOf(id1)); // Index of the first column as defined in the configuration. int index2 = conf.getPosition(Column.valueOf(id2)); // Index of the second column as defined in the configuration. // Sort by index, then by identifier. if (index1 < index2) { return -1; } if (index1 == index2) { if (id1 < id2) { return -1; } if (id1 == id2) { return 0; } } return 1; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/full/FileTableConfiguration.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.full; import com.mucommander.ui.main.table.Column; /** * Describes a file table's initial configuration. * @author Nicolas Rinaudo */ public class FileTableConfiguration { /** Each column's enabled state. */ private final boolean[] enabled; /** Initial width of each column. */ private final int[] width; /** Columns initial order. */ private final int[] order; /** * Creates a new file table configuration. */ public FileTableConfiguration() { int columnCount = Column.values().length; enabled = new boolean[columnCount]; width = new int[columnCount]; order = new int[columnCount]; } /** * Returns true if the specified column is enabled. * @param column column whose enabled state should be returned. * @return true if the specified column is enabled, false otherwise. */ public boolean isEnabled(Column column) { return enabled[column.ordinal()]; } /** * Sets the enabled state of the specified column. *

    * Note that the {@link Column#NAME} column's enabled state is ignored as it will always be enabled. * * @param column column whose enabled state should be set. * @param flag whether the column should be enabled. */ public void setEnabled(Column column, boolean flag) { enabled[column.ordinal()] = flag; } /** * Returns the initial width of the specified column. * @param column column whose width should be retrieved. * @return the requested column's width. */ public int getWidth(Column column) { return width[column.ordinal()]; } /** * Sets the specified column's width. *

    * Note that the {@link Column#NAME} column's width will be ignored, as it depends on the frame's * initial dimensions. * * @param column column whose width should be set. * @param value column's initial width. */ public void setWidth(Column column, int value) { width[column.ordinal()] = value; } /** * Returns the desired initial position of the specified column. *

    * Note that the returned value isn't necessarily a legal column position. It's used * as a comparison value rather than an index. * * @param column column whose initial position will be returned. * @return the desired initial position of the specified column. */ public int getPosition(Column column) { return order[column.ordinal()]; } /** * Sets the specified column's initial position. * @param column column whose position will be set. * @param position desired position for the specified column. */ public void setPosition(Column column, int position) { order[column.ordinal()] = position; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/table/views/full/FileTableModel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.table.views.full; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.main.table.FileTable; import com.mucommander.utils.text.CustomDateFormat; import com.mucommander.utils.text.SizeFormat; import com.mucommander.ui.main.table.Column; import com.mucommander.ui.main.table.views.BaseFileTableModel; import org.jetbrains.annotations.NotNull; /** * This class maps table cells onto file attributes. * * @author Maxence Bernard */ public class FileTableModel extends BaseFileTableModel { /** Cell values cache */ private Object[][] cellValuesCache; private int columnsVisibilityMask; /** * Creates a new FileTableModel, without any initial current folder. */ public FileTableModel() { super(); cellValuesCache = new Object[0][Column.values().length-1]; } @Override public synchronized void setupFromModel(BaseFileTableModel model) { super.setupFromModel(model); initCellValuesCache(); fillCellCache(null); } /** * Init and fill cell cache to speed up table even more */ @Override protected void initCellValuesCache() { this.cellValuesCache = new Object[getRowCount()][Column.values().length-1]; } /** * Retrieves all cell values and stores them in an array for fast access. */ @Override public synchronized void fillCellCache(FileTable fileTable) { int len = cellValuesCache.length; if (len == 0) { return; } columnsVisibilityMask = calcColumnVisibilityMask(fileTable); // Special '..' file if (parent != null) { Object[] cell = cellValuesCache[0]; cell[Column.NAME.ordinal()-1] = ".."; cell[Column.SIZE.ordinal()-1] = DIRECTORY_SIZE_STRING; currentFolderDateSnapshot = currentFolder.getLastModifiedDate(); cell[Column.DATE.ordinal()-1] = CustomDateFormat.format(currentFolderDateSnapshot); // Don't display parent's permissions as they can have a different format from the folder contents // (e.g. for archives) and this looks weird cell[Column.PERMISSIONS.ordinal()-1] = ""; cell[Column.OWNER.ordinal()-1] = ""; cell[Column.GROUP.ordinal()-1] = ""; } int fileIndex = 0; final int indexOffset = parent == null ? 0 : 1; for (int i = indexOffset; i < len; i++) { int cellIndex = fileIndex + indexOffset; //int cellIndex = fileArrayIndex[fileIndex] + indexOffset; //fillOneCellCache(cellIndex, cellIndex); Object[] cell = cellValuesCache[cellIndex]; for (int ci = Column.NAME.ordinal()-1; ci <= Column.GROUP.ordinal()-1; ci++) { cell[ci] = null; } fileIndex++; } } private static int calcColumnVisibilityMask(FileTable fileTable) { if (fileTable == null) { return 0xffff; } int mask = 0; for (Column column : Column.values()) { if (fileTable.isColumnVisible(column)) { mask |= 1 << column.ordinal(); } } return mask; } private Object[] fillOneCellCache(int cellIndex, int fileIndex) { AbstractFile file = getCachedFileAt(fileIndex); Object[] cell = cellValuesCache[cellIndex]; cell[Column.NAME.ordinal()-1] = file.getName(); if (isColumnVisible(Column.SIZE)) { cell[Column.SIZE.ordinal() - 1] = getSizeValue(file); } if (isColumnVisible(Column.DATE)) { cell[Column.DATE.ordinal() - 1] = CustomDateFormat.format(file.getLastModifiedDate()); } if (isColumnVisible(Column.PERMISSIONS)) { cell[Column.PERMISSIONS.ordinal() - 1] = file.getPermissionsString(); } if (isColumnVisible(Column.OWNER) && file.canGetOwner()) { cell[Column.OWNER.ordinal() - 1] = file.getOwner(); } if (isColumnVisible(Column.GROUP) && file.canGetGroup()) { cell[Column.GROUP.ordinal() - 1] = file.getGroup(); } return cell; } private boolean isColumnVisible(Column column) { return (columnsVisibilityMask & (1 << column.ordinal())) != 0; } @NotNull private String getSizeValue(AbstractFile file) { if (file.isDirectory()) { if (hasCalculatedDirectories) { Long dirSize; synchronized (directorySizes) { dirSize = directorySizes.get(file); } if (dirSize != null) { return SizeFormat.format(dirSize, sizeFormat); } else { synchronized (calculateSizeQueue) { return calculateSizeQueue.contains(file) ? QUEUED_DIRECTORY_SIZE_STRING : DIRECTORY_SIZE_STRING; } } } else { return DIRECTORY_SIZE_STRING; } } else { return SizeFormat.format(file.getSize(), sizeFormat); } } ////////////////////////////////////////// // Overridden AbstractTableModel methods // ////////////////////////////////////////// @Override public int getColumnCount() { return Column.values().length; // icon, name, size, date, permissions, owner, group } @Override public String getColumnName(int columnIndex) { return Column.valueOf(columnIndex).getLabel(); } /** * Returns the total number of rows, including the special parent folder file '..', if there is one. */ @Override public synchronized int getRowCount() { return fileArrayIndex.length + (parent == null ? 0 : 1); } @Override public synchronized Object getValueAt(int rowIndex, int columnIndex) { if (rowIndex >= cellValuesCache.length || columnIndex >= cellValuesCache[rowIndex].length) { return null; } // Need to check that row index is not larger than actual number of rows // because if table has just been changed (rows have been removed), // JTable may have an old row count value and may try to repaint rows that are out of bounds. if (rowIndex >= getRowCount()) { // Returning null will have JTable ignore this row return null; } // Icon/extension column, return a null value Column column = Column.valueOf(columnIndex); if (column == Column.EXTENSION) { return null; } // Decrement column index for cellValuesCache array columnIndex--; // Handle special '..' file if (rowIndex == 0 && parent != null) { return cellValuesCache[0][columnIndex]; // Object result = cellValuesCache[0][columnIndex]; // if (result == null) { // result = fillOneCellCache(0, 0)[columnIndex]; // } // return result; } int fileIndex = parent == null ? rowIndex : rowIndex-1; int index = fileArrayIndex[fileIndex]; if (parent != null) { index++; } Object result = cellValuesCache[index][columnIndex]; if (result == null) { result = fillOneCellCache(index, parent != null ? fileIndex + 1 : fileIndex)[columnIndex]; } return result; } /** * Returns true if name column has temporarily be made editable by FileTable * and given row doesn't correspond to parent file '..', false otherwise. */ @Override public boolean isCellEditable(int rowIndex, int columnIndex) { // Name column can temporarily be made editable by FileTable // but parent file '..' name should never be editable return Column.valueOf(columnIndex) == Column.NAME && (parent == null || rowIndex != 0) && nameColumnEditable; } public String getFileNameAt(int index) { return (index == 0 && hasParentFolder()) ? ".." : getFileAt(index).getName(); } public int getFileIndexAt(int row, int column) { return row; } @Override public int getFileRow(int index) { return index; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/ClonedFileTableTabFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.commons.file.FileURL; import com.mucommander.core.LocalLocationHistory; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.tabs.TabFactory; /** * Factory for creating {@link com.mucommander.ui.main.tabs.FileTableTab} which is a clone of a given {@link com.mucommander.ui.main.tabs.FileTableTab} * * @author Arik Hadas */ public class ClonedFileTableTabFactory implements TabFactory { private final FolderPanel folderPanel; ClonedFileTableTabFactory(FolderPanel folderPanel) { this.folderPanel = folderPanel; } public FileTableTab createTab(FileTableTab tab) { if (tab.getLocation() == null) { throw new RuntimeException("Invalid location"); } return new ClonedFileTableTab(tab, folderPanel); } static class ClonedFileTableTab extends FileTableTab { /** The location presented in this tab */ private FileURL location; /** Flag that indicates whether the tab is locked or not */ private boolean locked; /** Title that is assigned for the tab */ private String title; /** History of accessed location within the tab */ private final LocalLocationHistory locationHistory; /** * Private constructor * * @param tab - the location that would be presented in the tab */ private ClonedFileTableTab(FileTableTab tab, FolderPanel folderPanel) { this.location = tab.getLocation(); this.locked = tab.isLocked(); this.title = tab.getTitle(); locationHistory = new LocalLocationHistory(folderPanel); } @Override public void setLocation(FileURL location) { this.location = location; // add location to the history (See LocalLocationHistory to see how it handles the first location it gets) locationHistory.tryToAddToHistory(location); } @Override public FileURL getLocation() { return location; } @Override public void setLocked(boolean locked) { this.locked = locked; } @Override public void setTitle(String title) { this.title = title; } @Override public String getTitle() { return title; } @Override public boolean isLocked() { return locked; } @Override public LocalLocationHistory getLocationHistory() { return locationHistory; } @Override public boolean equals(Object obj) { if (obj instanceof FileTableTab) { FileTableTab other = ((FileTableTab) obj); return location.equals(other.getLocation()) && locked == ((FileTableTab) obj).isLocked(); } return false; } @Override public int hashCode() { return location.hashCode(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/ConfFileTableTab.java ================================================ package com.mucommander.ui.main.tabs; import com.mucommander.commons.file.FileURL; import com.mucommander.core.LocalLocationHistory; public class ConfFileTableTab extends FileTableTab { private final boolean lock; private final FileURL location; private final String title; public ConfFileTableTab(FileURL location) { this(false, location, null); } public ConfFileTableTab(boolean lock, FileURL location, String title) { this.lock = lock; this.location = location; this.title = title; } @Override public boolean isLocked() { return lock; } @Override public FileURL getLocation() { return location; } @Override public String getTitle() { return title; } @Override public void setLocation(FileURL location) { throw new UnsupportedOperationException("cannot change location of configuration tab"); } @Override public void setLocked(boolean locked) { throw new UnsupportedOperationException("cannot lock configuration tab"); } @Override public void setTitle(String title) { throw new UnsupportedOperationException("cannot change title of configuration tab"); } @Override public LocalLocationHistory getLocationHistory() { return null; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/DefaultFileTableTabFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.commons.file.FileURL; import com.mucommander.core.LocalLocationHistory; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.tabs.TabFactory; /** * Factory for creating regular {@link com.mucommander.ui.main.tabs.FileTableTab} presenting the given location * * @author Arik Hadas */ public class DefaultFileTableTabFactory implements TabFactory { private final FolderPanel folderPanel; public DefaultFileTableTabFactory(FolderPanel folderPanel) { this.folderPanel = folderPanel; } public FileTableTab createTab(FileURL location) { if (location == null) throw new RuntimeException("Invalid location"); return new DefaultFileTableTab(location, folderPanel); } static class DefaultFileTableTab extends FileTableTab { /** The location presented in this tab */ private FileURL location; /** Flag that indicates whether the tab is locked or not */ private boolean locked; /** Title that is assigned for the tab */ private String title; /** History of accessed location within the tab */ private final LocalLocationHistory locationHistory; /** * Private constructor * * @param location - the location that would be presented in the tab */ private DefaultFileTableTab(FileURL location, FolderPanel folderPanel) { this.location = location; this.locked = false; locationHistory = new LocalLocationHistory(folderPanel); } @Override public void setLocation(FileURL location) { this.location = location; // add location to the history (See LocalLocationHistory to see how it handles the first location it gets) locationHistory.tryToAddToHistory(location); } @Override public FileURL getLocation() { return location; } @Override public void setLocked(boolean locked) { this.locked = locked; } @Override public boolean isLocked() { return locked; } @Override public void setTitle(String title) { this.title = title; } @Override public String getTitle() { return title; } @Override public LocalLocationHistory getLocationHistory() { return locationHistory; } @Override public boolean equals(Object obj) { if (obj instanceof FileTableTab) { FileTableTab other = (FileTableTab) obj; return location.equals(other.getLocation()) && locked == other.isLocked(); } return false; } @Override public int hashCode() { return location.hashCode(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/DefaultFileTableTabHeaderFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.ui.main.FolderPanel; /** * * @author Arik Hadas */ public class DefaultFileTableTabHeaderFactory extends FileTableTabHeaderFactory { public DefaultFileTableTabHeaderFactory(FolderPanel folderPanel) { super(folderPanel); } @Override public FileTableTabHeader create(FileTableTab tab) { return new FileTableTabHeader(folderPanel, true, tab); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTab.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.bookmark.BookmarkManager; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.core.LocalLocationHistory; import com.mucommander.utils.text.Translator; import com.mucommander.ui.tabs.Tab; /** * Interface of tab in the {@link com.mucommander.ui.main.FolderPanel} that contains a {@link com.mucommander.ui.main.table.FileTable} * * @author Arik Hadas */ public abstract class FileTableTab implements Tab { /** * Setter for the location presented in the tab * * @param location the file that is going to be presented in the tab */ public abstract void setLocation(FileURL location); /** * Getter for the location presented in the tab * * @return the file that is being presented in the tab */ public abstract FileURL getLocation(); /** * Set the tab to be locked or unlocked according to the given flag * * @param locked flag that indicates whether the tab should be locked or not */ public abstract void setLocked(boolean locked); /** * Returns whether the tab is locked * * @return indication whether the tab is locked */ public abstract boolean isLocked(); /** * Set the title of the tab to the given string * * @param title - predefined title to be assigned to the tab, null for no predefined title */ public abstract void setTitle(String title); /** * Returns the title that was assigned for the tab * * @return the title that was assigned for the tab, null is returned if no title was assigned */ public abstract String getTitle(); /** * Returns a string representation for the tab: * the tab's fixed title will be returned if such title was assigned, * otherwise, a string representation will be created based on the tab's location: * for local file, the filename will be returned ("/" in case the root folder is presented) * for remote file, the returned pattern will be "\<host\>:\<filename\>" * * @return String representation of the tab */ String getDisplayableTitle() { String title = getTitle(); return title != null ? title : createDisplayableTitleFromLocation(getLocation()); } private String createDisplayableTitleFromLocation(FileURL location) { if (BookmarkManager.isBookmark(location) && location.getHost() == null) { return Translator.get("bookmarks_menu"); } boolean local = FileURL.LOCALHOST.equals(location.getHost()); return getHostRepresentation(location.getHost(), local) + getFilenameRepresentation(location.getFilename(), local); } private String getHostRepresentation(String host, boolean local) { return local ? "" : host + ":"; } private String getFilenameRepresentation(String filename, boolean local) { // Under for OSes with 'root drives' (Windows, OS/2), remove the leading '/' character if (local && LocalFile.hasRootDrives() && filename != null) { return PathUtils.removeLeadingSeparator(filename, "/"); } // Under other OSes, if the filename is empty return "/" return filename == null ? "/" : filename; } /** * Returns the tracker of the last accessed locations within the tab * * @return tracker of the last accessed locations within the tab */ public abstract LocalLocationHistory getLocationHistory(); } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabHeader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import java.awt.Dimension; import java.awt.Font; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.plaf.basic.BasicButtonUI; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.FolderPanel; /** * This panel is the header of the presented tabs under Java 1.6 and above. * The panel contains a button for closing the tab. * * @author Arik Hadas, Maxence Bernard */ public class FileTableTabHeader extends JPanel implements ActionListener { private final FolderPanel folderPanel; private static final String CLOSE_ICON_NAME = "close.png"; private static final String CLOSE_ROLLOVER_ICON_NAME = "close_rollover.png"; private static final int CLOSE_ICON_SIZE = 12; public static final String LOCKED_ICON_NAME = "lock.png"; private static final int LOCKED_ICON_SIZE = 12; FileTableTabHeader(FolderPanel folderPanel, boolean closable, FileTableTab tab) { super(new GridBagLayout()); this.folderPanel = folderPanel; setOpaque(false); GridBagConstraints gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.LINE_START; gbc.gridy = 0; // Locked tab icon JLabel lockedIcon = new LockedIcon(); gbc.weightx = 0; // required otherwise extra width may be redistributed around the button gbc.gridx = 0; lockedIcon.setVisible(false); add(lockedIcon, gbc); // Label JLabel label = new JLabel(); Font font = new JLabel().getFont(); label.setFont(font.deriveFont(font.getStyle(), font.getSize()-2)); // Add extra space between the label and the button label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5)); gbc.weightx = 1; gbc.gridx = 1; add(label, gbc); if (closable && !tab.isLocked()) { // Close tab button JButton closeButton = new CloseButton(); closeButton.addActionListener(this); gbc.weightx = 1; // required otherwise extra width may be redistributed around the button gbc.gridx = 2; add(closeButton, gbc); } setText(tab.getDisplayableTitle()); lockedIcon.setVisible(tab.isLocked()); } private void setText(String text) { JLabel label = (JLabel)getComponent(1); // Truncate the title if it is too long. // Note: 31 is the maximum title length displayed in tabs by Firefox and Safari at the time of this writing if(text.length()>31) text = text.substring(0, 32) + "…"; label.setText(text); validate(); } /******************************** * ActionListener Implementation ********************************/ public void actionPerformed(ActionEvent e) { folderPanel.getTabs().close(this); } /************************************************** * Buttons which are presented in the tab's header **************************************************/ private static class CloseButton extends JButton { CloseButton() { setPreferredSize(new Dimension(CLOSE_ICON_SIZE, CLOSE_ICON_SIZE)); //Make the button looks the same for all Laf's setUI(new BasicButtonUI()); //Make it transparent setContentAreaFilled(false); //No need to be focusable setFocusable(false); setBorderPainted(false); setIcon(IconManager.getIcon(IconManager.IconSet.COMMON, CLOSE_ICON_NAME)); //Making nice rollover effect setRolloverEnabled(true); setRolloverIcon(IconManager.getIcon(IconManager.IconSet.COMMON, CLOSE_ROLLOVER_ICON_NAME)); } // Remove default insets @Override public Insets getInsets() { return new Insets(0,0,0,0); } // We don't want to update UI for this button @Override public void updateUI() { } } private static class LockedIcon extends JLabel { LockedIcon() { super(IconManager.getIcon(IconManager.IconSet.COMMON, LOCKED_ICON_NAME)); setPreferredSize(new Dimension(LOCKED_ICON_SIZE, LOCKED_ICON_SIZE)); //No need to be focusable setFocusable(false); } // Remove default insets @Override public Insets getInsets() { return new Insets(0,0,0,0); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabHeaderFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.ui.main.FolderPanel; /** * * @author Arik Hadas */ public abstract class FileTableTabHeaderFactory { protected final FolderPanel folderPanel; FileTableTabHeaderFactory(FolderPanel folderPanel) { this.folderPanel = folderPanel; } public abstract FileTableTabHeader create(FileTableTab tab); } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabPopupMenu.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.ui.action.impl.CloneTabToOtherPanelAction; import com.mucommander.ui.action.impl.CloseDuplicateTabsAction; import com.mucommander.ui.action.impl.CloseOtherTabsAction; import com.mucommander.ui.action.impl.CloseTabAction; import com.mucommander.ui.action.impl.DuplicateTabAction; import com.mucommander.ui.action.impl.MoveTabToOtherPanelAction; import com.mucommander.ui.action.impl.SetTabTitleAction; import com.mucommander.ui.action.impl.ToggleLockTabAction; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.popup.TcActionsPopupMenu; /** * Contextual popup menu invoked by {@link FileTableTabbedPane} when right-clicking on a tab's title. * * @author Arik Hadas */ class FileTableTabPopupMenu extends TcActionsPopupMenu { FileTableTabPopupMenu(MainFrame mainFrame) { super(mainFrame); //FileTableTab tab = mainFrame.getActivePanel().getTabs().getCurrentTab(); addAction(DuplicateTabAction.Descriptor.ACTION_ID); addAction(CloseTabAction.Descriptor.ACTION_ID); addAction(CloseOtherTabsAction.Descriptor.ACTION_ID); addAction(CloseDuplicateTabsAction.Descriptor.ACTION_ID); addSeparator(); addAction(ToggleLockTabAction.Descriptor.ACTION_ID); addAction(SetTabTitleAction.Descriptor.ACTION_ID); addSeparator(); addAction(MoveTabToOtherPanelAction.Descriptor.ACTION_ID); addAction(CloneTabToOtherPanelAction.Descriptor.ACTION_ID); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabbedPane.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.macosx.TabbedPaneUICustomizer; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.tabs.TabbedPane; import javax.swing.*; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; /** * TabbedPane that present the FileTable tabs. * This TabbedPane doesn't contain different FileTable for each tab, instead * it use one FileTable instance as a shared object for all tabs. when switching between * tabs, the FileTable instance is updated as needed according to the selected tab state. * * @author Arik Hadas */ public class FileTableTabbedPane extends TabbedPane implements FocusListener { /** The FileTable instance presented in each tab */ private final JComponent fileTableComponent; private final MainFrame mainFrame; private final FolderPanel folderPanel; private final FileTableTabHeaderFactory headersFactory; FileTableTabbedPane(MainFrame mainFrame, FolderPanel folderPanel, JComponent fileTableComponent, FileTableTabHeaderFactory headersFactory) { this.fileTableComponent = fileTableComponent; this.mainFrame = mainFrame; this.folderPanel = folderPanel; this.headersFactory = headersFactory; setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { final Point clickedPoint = e.getPoint(); int selectedTabIndex = indexAtLocation(clickedPoint.x, clickedPoint.y); if (selectedTabIndex != -1) { setSelectedIndex(selectedTabIndex); if (DesktopManager.isRightMouseButton(e)) { // Open the popup menu only after all swing events are finished, to ensure that when the popup menu is shown // and asks for the currently selected tab in the active panel, it'll get the right one SwingUtilities.invokeLater(() -> new FileTableTabPopupMenu(mainFrame).show(FileTableTabbedPane.this, clickedPoint.x, clickedPoint.y)); } if (DesktopManager.isMiddleMouseButton(e)) { ActionManager.performAction(com.mucommander.ui.action.impl.CloseTabAction.Descriptor.ACTION_ID, mainFrame); } } } }); DesktopManager.customizeTabbedPaneUI(this); addFocusListener(this); } @Override public boolean requestFocusInWindow() { return fileTableComponent.requestFocusInWindow(); } @Override public void removeTabAt(int index) { super.removeTabAt(index); if (index == 0 && getTabCount() > 0) { setComponentAt(0, fileTableComponent); } } private void setTabHeader(int index, FileTableTabHeader component) { super.setTabComponentAt(index, component); } @Override public void add(FileTableTab tab) { add(tab, getTabCount()); } @Override public void add(FileTableTab tab, int index) { add(getTabCount() == 0 ? fileTableComponent : new JLabel(), index); update(tab, index); } @Override public void setSelectedIndex(int index) { // Allow tabs switching only when no-events-mode is disabled if (!mainFrame.getNoEventsMode()) { super.setSelectedIndex(index); requestFocusInWindow(); } } @Override public void update(FileTableTab tab, int index) { setTabHeader(index, headersFactory.create(tab)); String locationText = tab.getLocation().getPath(); // For OSes with 'root drives' (Windows, OS/2), remove the leading '/' character if (LocalFile.hasRootDrives()) { locationText = PathUtils.removeLeadingSeparator(locationText, "/"); } setToolTipTextAt(index, locationText); SwingUtilities.invokeLater(this::validate); } ////////////////////////////////// // FocusListener implementation // ////////////////////////////////// public void focusGained(FocusEvent e) { folderPanel.getTabs().requestFocus(); } public void focusLost(FocusEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabs.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.tabs.HideableTabbedPane; import com.mucommander.ui.tabs.TabFactory; /** * HideableTabbedPane of {@link com.mucommander.ui.main.tabs.FileTableTab} instances. * * @author Arik Hadas */ public class FileTableTabs extends HideableTabbedPane implements LocationListener { /** FolderPanel containing those tabs */ private final FolderPanel folderPanel; /** Factory of instances of FileTableTab */ private final TabFactory defaultTabsFactory; /** Factory of instances of FileTableTab */ private final TabFactory clonedTabsFactory; public FileTableTabs(MainFrame mainFrame, FolderPanel folderPanel, ConfFileTableTab[] initialTabs) { super(new FileTableTabsWithoutHeadersViewerFactory(folderPanel), new FileTableTabsWithHeadersViewerFactory(mainFrame, folderPanel)); this.folderPanel = folderPanel; defaultTabsFactory = new DefaultFileTableTabFactory(folderPanel); clonedTabsFactory = new ClonedFileTableTabFactory(folderPanel); // Register to location change events folderPanel.getLocationManager().addLocationListener(this); // Add the initial folders for (FileTableTab tab : initialTabs) { addTab(clonedTabsFactory.createTab(tab)); } } @Override public void selectTab(int index) { super.selectTab(index); show(index); } @Override protected void show(final int tabIndex) { try { folderPanel.tryChangeCurrentFolderInternal(getTab(tabIndex).getLocation(), this::fireActiveTabChanged); } catch (Throwable ignore) {} } /** * Return the currently selected tab * * @return currently selected tab */ public FileTableTab getCurrentTab() { FileTableTab result = getTab(getSelectedIndex()); return result != null ? result : getTab(0); } private void updateTabLocation(final FileURL location) { updateCurrentTab(tab -> tab.setLocation(location)); } private void updateTabLocking(final boolean lock) { updateCurrentTab(tab -> tab.setLocked(lock)); } private void updateTabTitle(final String title) { updateCurrentTab(tab -> tab.setTitle(title)); } @Override protected boolean showSingleTabHeader() { int nbTabs = getTabs().count(); if (nbTabs == 1) { FileTableTab tab = getTab(0); // If there's just single tab that is locked don't remove his header if (tab.isLocked()) return true; } return super.showSingleTabHeader(); } @Override protected FileTableTab removeTab() { return !getCurrentTab().isLocked() ? super.removeTab() : null; } /******************** * MuActions support ********************/ public void add(AbstractFile file) { addTab(defaultTabsFactory.createTab(file.getURL())); } public void add(FileTableTab tab) { addAndSelectTab(tab); } public void add(FileURL fileURL) { addTab(defaultTabsFactory.createTab(fileURL)); } public FileTableTab closeCurrentTab() { return removeTab(); } public void closeDuplicateTabs() { removeDuplicateTabs(); } public void closeOtherTabs() { removeOtherTabs(); } public void duplicate() { add(clonedTabsFactory.createTab(getCurrentTab())); } public void lock() { updateTabLocking(true); } public void unlock() { updateTabLocking(false); } public void setTitle(String title) { updateTabTitle(title); } /**************** * Other Actions ****************/ public void close(FileTableTabHeader fileTableTabHeader) { removeTab(fileTableTabHeader); } @Override public void locationChanged(LocationEvent locationEvent) { AbstractFile folder = folderPanel.getCurrentFolder(); if (folder != null) { updateTabLocation(folder.getURL()); } } @Override public void locationCancelled(LocationEvent locationEvent) { AbstractFile folder = folderPanel.getCurrentFolder(); if (folder != null) { updateTabLocation(folder.getURL()); } } @Override public void locationFailed(LocationEvent locationEvent) { AbstractFile folder = folderPanel.getCurrentFolder(); if (folder != null) { updateTabLocation(folder.getURL()); } } @Override public void locationChanging(LocationEvent locationEvent) { } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabsWithHeadersViewerFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.tabs.TabsCollection; import com.mucommander.ui.tabs.TabsViewer; import com.mucommander.ui.tabs.TabsViewerFactory; import com.mucommander.ui.tabs.TabsWithHeaderViewer; /** * Factory that creates viewers presenting tabs with headers * * @author Arik Hadas */ public class FileTableTabsWithHeadersViewerFactory implements TabsViewerFactory { private final FolderPanel folderPanel; private final MainFrame mainFrame; public FileTableTabsWithHeadersViewerFactory(MainFrame mainFrame, FolderPanel folderPanel) { this.folderPanel = folderPanel; this.mainFrame = mainFrame; } @Override public TabsViewer create(TabsCollection tabs) { FileTableTabHeaderFactory headersFactory = tabs.count() == 1 ? new NotClosableFileTableTabHeaderFactory(folderPanel) : new DefaultFileTableTabHeaderFactory(folderPanel); return new TabsWithHeaderViewer<>(tabs, new FileTableTabbedPane(mainFrame, folderPanel, folderPanel.getFileTable().getAsUIComponent(), headersFactory)); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/FileTableTabsWithoutHeadersViewerFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.tabs.TabWithoutHeaderViewer; import com.mucommander.ui.tabs.TabsCollection; import com.mucommander.ui.tabs.TabsViewer; import com.mucommander.ui.tabs.TabsViewerFactory; /** * Factory that creates viewers presenting tabs with no header * * @author Arik Hadas */ public class FileTableTabsWithoutHeadersViewerFactory implements TabsViewerFactory { private final FolderPanel folderPanel; FileTableTabsWithoutHeadersViewerFactory(FolderPanel folderPanel) { this.folderPanel = folderPanel; } /*********************************** * TabsViewerFactory Implementation ***********************************/ public TabsViewer create(TabsCollection tabs) { return new TabWithoutHeaderViewer<>(tabs, folderPanel.getFileTable().getAsUIComponent()); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/NotClosableFileTableTabHeaderFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.ui.main.FolderPanel; /** * * @author Arik Hadas */ public class NotClosableFileTableTabHeaderFactory extends FileTableTabHeaderFactory { NotClosableFileTableTabHeaderFactory(FolderPanel folderPanel) { super(folderPanel); } @Override public FileTableTabHeader create(FileTableTab tab) { return new FileTableTabHeader(folderPanel, false, tab); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tabs/PrintableFileTableTabFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tabs; import com.mucommander.commons.file.FileURL; import com.mucommander.core.LocalLocationHistory; import com.mucommander.ui.tabs.TabFactory; /** * Factory for creating {@link com.mucommander.ui.main.tabs.FileTableTab} for presentation in {@link com.mucommander.ui.main.quicklist.TabsQL} * * @author Arik Hadas */ public class PrintableFileTableTabFactory implements TabFactory { public FileTableTab createTab(FileTableTab tab) { return new PrintableFileTableTab(tab); } /** * Implementation of the Decorator design pattern which is used to modify the way FileTableTab * is presented (by overriding its toString method) and the way it's compared to * FileTableTabFactory.DefaultFileTableTab instances (by overriding its equals method) */ private static class PrintableFileTableTab extends FileTableTab { private final FileTableTab tab; private PrintableFileTableTab(FileTableTab tab) { this.tab = tab; } @Override public void setLocation(FileURL location) { tab.setLocation(location); } @Override public FileURL getLocation() { return tab.getLocation(); } @Override public void setLocked(boolean locked) { tab.setLocked(locked); } @Override public boolean isLocked() { return tab.isLocked(); } @Override public void setTitle(String title) { tab.setTitle(title); } @Override public String getTitle() { return tab.getTitle(); } @Override public LocalLocationHistory getLocationHistory() { return tab.getLocationHistory(); } @Override public boolean equals(Object obj) { return tab == obj; } @Override public String toString() { return getDisplayableTitle(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/toolbar/ToolBar.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.toolbar; import java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.HashMap; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JPopupMenu; import javax.swing.JToolBar; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.core.LocalLocationHistory; import com.mucommander.desktop.DesktopManager; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.GoBackAction; import com.mucommander.ui.action.impl.GoForwardAction; import com.mucommander.ui.action.impl.OpenLocationAction; import com.mucommander.ui.action.impl.ToggleToolBarAction; import com.mucommander.ui.button.NonFocusableButton; import com.mucommander.ui.button.PopupButton; import com.mucommander.ui.button.RolloverButtonAdapter; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.MainFrame; import ru.trolsoft.macosx.RetinaImageIcon; /** * This class is the icon toolbar attached to a MainFrame, triggering events when buttons are clicked. * * @author Maxence Bernard, Arik Hadas */ public class ToolBar extends JToolBar implements ConfigurationListener, MouseListener, ToolBarAttributesListener { private final MainFrame mainFrame; /** Dimension of button separators */ private final static Dimension SEPARATOR_DIMENSION = new Dimension(10, 16); /** Whether to use the new JButton decorations introduced in Mac OS X 10.5 (Leopard) */ private final static boolean USE_MAC_OS_X_CLIENT_PROPERTIES = OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher(); /** Current icon scale value */ // The math.max(1.0f, ...) part is to workaround a bug which cause(d) this value to be set to 0.0 in the configuration file. private static float scaleFactor = Math.max(1.0f, TcConfigurations.getPreferences().getVariable(TcPreference.TOOLBAR_ICON_SCALE, TcPreferences.DEFAULT_TOOLBAR_ICON_SCALE)); /** * Creates a new toolbar and attaches it to the given frame. */ public ToolBar(MainFrame mainFrame) { this.mainFrame = mainFrame; // Decoration properties setBorderPainted(false); setFloatable(false); putClientProperty("JToolBar.isRollover", Boolean.TRUE); // Listen to mouse events in order to popup a menu when toolbar is right-clicked addMouseListener(this); // Listen to configuration changes to reload toolbar buttons when icon size has changed TcConfigurations.addPreferencesListener(this); // create buttons for each action and add them to the toolbar addButtons(ToolBarAttributes.getActions()); ToolBarAttributes.addToolBarAttributesListener(this); } private void addButtons(String[] actionIds) { for (String actionId : actionIds) { if (actionId == null) { addSeparator(SEPARATOR_DIMENSION); } else { // Get a MuAction instance TcAction action = ActionManager.getActionInstance(actionId, mainFrame); // Do not add buttons for actions that do not have an icon if (action != null && action.getIcon() != null) { addButton(action); } } } if (USE_MAC_OS_X_CLIENT_PROPERTIES) { int nbComponents = getComponentCount(); // Set the 'segment position' required for the 'segmented capsule' style for( int i = 0; i < nbComponents; i++) { Component comp = getComponent(i); if (!(comp instanceof JButton)) { continue; } boolean hasPrevious = i != 0 && (getComponent(i-1) instanceof JButton); boolean hasNext = i != nbComponents-1 && (getComponent(i+1) instanceof JButton); String segmentPosition; if (hasPrevious && hasNext) { segmentPosition = "middle"; } else if (hasPrevious) { segmentPosition = "last"; } else if (hasNext) { segmentPosition = "first"; } else { segmentPosition = "only"; } ((JButton)comp).putClientProperty("JButton.segmentPosition", segmentPosition); } } } /** * Adds a button to this toolbar using the given action. */ private void addButton(TcAction action) { JButton button; if (action instanceof GoBackAction || action instanceof GoForwardAction) { button = new HistoryPopupButton(action); } else { button = new NonFocusableButton(action); } // Remove label button.setText(null); // Add tooltip using the action's label and accelerator String toolTipText = action.getLabel(); String acceleratorText = action.getAcceleratorText(); if (acceleratorText != null) { toolTipText += " (" + acceleratorText + ")"; } button.setToolTipText(toolTipText); // Sets the button icon, taking into account the icon scale factor setButtonIcon(button); if (USE_MAC_OS_X_CLIENT_PROPERTIES) { if (button.getIcon() == null || button.getIcon().getIconHeight() <= 16) { button.putClientProperty("JButton.buttonType", "segmentedTextured"); } button.setRolloverEnabled(true); } else { RolloverButtonAdapter.decorateButton(button); } add(button); } /** * Sets the specified button's icon to the proper scale. * * @param button the button to update */ private void setButtonIcon(JButton button) { // Note: the action's icon must not be changed and remain in its original, non-scaled size ImageIcon icon = IconManager.getScaledIcon((ImageIcon)button.getAction().getValue(Action.SMALL_ICON), scaleFactor); if (!USE_MAC_OS_X_CLIENT_PROPERTIES) { // Add padding around the icon so the button feels less crowded icon = IconManager.getPaddedIcon(icon, new Insets(3, 4, 3, 4)); } button.setIcon(icon); if (icon instanceof RetinaImageIcon) { button.setDisabledIcon(((RetinaImageIcon)icon).buildDisabledIcon()); button.setPressedIcon(icon); } } /** * Listens to certain configuration variables. */ @Override public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); // Rescale buttons icon if (var.equals(TcPreferences.TOOLBAR_ICON_SCALE)) { scaleFactor = event.getFloatValue(); Component[] components = getComponents(); for (Component component : components) { if (component instanceof JButton) { setButtonIcon((JButton) component); } } } } @Override public void mouseClicked(MouseEvent e) { Object source = e.getSource(); // Right-clicking on the toolbar brings up a popup menu if (source == this) { if (DesktopManager.isRightMouseButton(e)) { // if (e.isPopupTrigger()) { // Doesn't work under Mac OS X (CTRL+click doesn't return true) JPopupMenu popupMenu = new JPopupMenu(); popupMenu.add(ActionManager.getActionInstance(ToggleToolBarAction.Descriptor.ACTION_ID, mainFrame)); popupMenu.show(this, e.getX(), e.getY()); popupMenu.setVisible(true); } } } @Override public void mouseEntered(MouseEvent e) { Object source = e.getSource(); if (source instanceof JButton) { ((JButton) source).setBorderPainted(true); } } @Override public void mouseExited(MouseEvent e) { Object source = e.getSource(); if (source instanceof JButton) { ((JButton) source).setBorderPainted(false); } } @Override public void mouseReleased(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void toolBarActionsChanged() { removeAll(); addButtons(ToolBarAttributes.getActions()); } /** * PopupButton used for 'Go back' and 'Go forward' actions which displays the list of back/forward folders in the * popup menu and allows to recall them by clicking on them. */ private class HistoryPopupButton extends PopupButton { private final TcAction action; private HistoryPopupButton(TcAction action) { super(action); this.action = action; } @Override public JPopupMenu getPopupMenu() { LocalLocationHistory locationHistory = mainFrame.getActivePanel().getFolderHistory(); FileURL[] history = action instanceof GoBackAction ? locationHistory.getBackFolders() : locationHistory.getForwardFolders(); // If no back/forward folder, do not display popup menu if (history.length == 0) { return null; } JPopupMenu popupMenu = new JPopupMenu(); for (FileURL url : history) { popupMenu.add(new OpenLocationAction(mainFrame, new HashMap<>(), url)); } return popupMenu; } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/toolbar/ToolBarAttributes.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.toolbar; import java.util.Map; import java.util.WeakHashMap; import com.mucommander.ui.action.impl.*; /** * This class is responsible to handle the attributes of ToolBars - their actions and separators. * Every ToolBar should get its attributes from this class, and register in it for receiving attributes modifications. * * @author Arik Hadas */ public class ToolBarAttributes { /** Command bar actions: Class instances or null to signify a separator */ private static String[] actionIds; private static boolean useDefaultActions = true; /** Contains all registered toolbar-attributes listeners, stored as weak references */ private static final Map listeners = new WeakHashMap<>(); /** Default command bar actions: Class instances or null to signify a separator */ private final static String[] DEFAULT_TOOLBAR_ACTIONS = new String[] { NewWindowAction.Descriptor.ACTION_ID, AddTabAction.Descriptor.ACTION_ID, null, GoBackAction.Descriptor.ACTION_ID, GoForwardAction.Descriptor.ACTION_ID, null, GoToParentAction.Descriptor.ACTION_ID, GoToHomeAction.Descriptor.ACTION_ID, null, StopAction.Descriptor.ACTION_ID, null, MarkGroupAction.Descriptor.ACTION_ID, UnmarkGroupAction.Descriptor.ACTION_ID, null, SwapFoldersAction.Descriptor.ACTION_ID, SetSameFolderAction.Descriptor.ACTION_ID, null, PackAction.Descriptor.ACTION_ID, UnpackAction.Descriptor.ACTION_ID, null, AddBookmarkAction.Descriptor.ACTION_ID, EditBookmarksAction.Descriptor.ACTION_ID, EditCredentialsAction.Descriptor.ACTION_ID, null, FindFileAction.Descriptor.ACTION_ID, null, ConnectToServerAction.Descriptor.ACTION_ID, ShowServerConnectionsAction.Descriptor.ACTION_ID, RunCommandAction.Descriptor.ACTION_ID, EmailAction.Descriptor.ACTION_ID, null, RevealInDesktopAction.Descriptor.ACTION_ID, ShowFilePropertiesAction.Descriptor.ACTION_ID, null, ShowPreferencesAction.Descriptor.ACTION_ID }; /** * Removes leading and trailing separators (null elements) from the given action Class array, and * returns the trimmed action array. * * @param actions the action Class array to trim. * @return the trimmed action Class array, free of leading and trailing separators. */ private static String[] trimActionsArray(String[] actions) { int start = 0; int end = actions.length; while (start < end && actions[start] == null) { start++; } if (start == end) { return new String[]{}; } while (end > start && actions[end-1] == null) { end--; } int newLen = end-start; String[] newActions = new String[newLen]; System.arraycopy(actions, start, newActions, 0, newLen); return newActions; } /** * Sets the toolbar actions to the given action classes. null elements are used to insert a separator * between buttons. * * @param actions the new toolbar actions classes */ public static void setActions(String[] actions) { ToolBarAttributes.actionIds = trimActionsArray(actions); useDefaultActions = false; fireActionsChanged(); } /** * Check whether the default attributes are used. * * @return true if the default attributes are used, false otherwise. */ static boolean areDefaultAttributes() { if (useDefaultActions) { return true; } int nbActions = actionIds.length; if (nbActions != DEFAULT_TOOLBAR_ACTIONS.length) { return false; } for (int i = 0; i < nbActions; ++i) { if (!equals(actionIds[i], DEFAULT_TOOLBAR_ACTIONS[i])) { return false; } } return true; } private static boolean equals(Object action1, Object action2) { if (action1 == null) { return action2 == null; } return action1.equals(action2); } /** * Returns the actions classes that constitute the toolbar. null elements are used to insert a separator * between buttons. * * @return the actions classes that constitute the toolbar. */ public static String[] getActions() { return useDefaultActions ? DEFAULT_TOOLBAR_ACTIONS : actionIds; } // - Listeners ------------------------------------------------------------- // ------------------------------------------------------------------------- static void addToolBarAttributesListener(ToolBarAttributesListener listener) { synchronized(listeners) {listeners.put(listener, null);} } public static void removeToolBarAttributesListener(ToolBarAttributesListener listener) { synchronized(listeners) { listeners.remove(listener); } } private static void fireActionsChanged() { synchronized(listeners) { for (ToolBarAttributesListener listener : listeners.keySet()) listener.toolBarActionsChanged(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/toolbar/ToolBarAttributesListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.toolbar; /** * This is an interface that each class that should listen to ToolBar's attributes modifications need to implement. * * @author Arik Hadas */ public interface ToolBarAttributesListener { /** * This method is invoked when toolbar's actions have been modified. */ void toolBarActionsChanged(); } ================================================ FILE: src/main/java/com/mucommander/ui/main/toolbar/ToolBarIO.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.toolbar; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.helpers.DefaultHandler; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; /** * * @author Arik Hadas */ public abstract class ToolBarIO extends DefaultHandler { private static Logger logger; /* Variables used for XML parsing */ /** Root element */ protected static final String ROOT_ELEMENT = "toolbar"; /** Attribute containing the last muCommander version that was used to create the file */ protected static final String VERSION_ATTRIBUTE = "version"; /** Element describing one of the button in the list */ static final String BUTTON_ELEMENT = "button"; /** Attribute containing the action class associated with the button */ static final String ACTION_ATTRIBUTE = "action"; /** Attribute containing the action id associated with the button */ static final String ACTION_ID_ATTRIBUTE = "action_id"; /** Element describing one of the separator in the list */ static final String SEPARATOR_ELEMENT = "separator"; /** Default toolbar descriptor filename */ final static String DEFAULT_TOOLBAR_FILE_NAME = "toolbar.xml"; /** Toolbar descriptor file used when calling {@link #loadDescriptionFile()} */ private static AbstractFile descriptionFile; /** ToolBarWriter instance */ private static ToolBarWriter toolBarWriter; /** Whether the command-bar has been modified and should be saved */ static boolean wasToolBarModified; /** * Parses the XML file describing the toolbar's buttons and associated actions. * If the file doesn't exist, default toolbar elements will be used. */ public static void loadDescriptionFile() throws Exception { AbstractFile descriptionFile = getDescriptionFile(); if (descriptionFile != null && descriptionFile.exists()) { ToolBarReader reader = new ToolBarReader(descriptionFile); ToolBarAttributes.setActions(reader.getActionsRead()); } else { getLogger().debug("User toolbar.xml was not found, using default toolbar"); } toolBarWriter = ToolBarWriter.create(); } /** * Writes the current tool bar to the user's toolbar file. */ public static void saveToolBar() throws IOException { if (ToolBarAttributes.areDefaultAttributes()) { AbstractFile toolBarFile = getDescriptionFile(); if (toolBarFile != null && toolBarFile.exists()) { getLogger().info("Toolbar use default settings, removing descriptor file"); toolBarFile.delete(); } else { getLogger().debug("Toolbar not modified, not saving"); } } else if (toolBarWriter != null) { if (wasToolBarModified) { toolBarWriter.write(); } else { getLogger().debug("Toolbar not modified, not saving"); } } else { getLogger().warn("Could not save toolbar. writer is null"); } } /** * Mark that actions were modified and therefore should be saved. */ static void setModified() { wasToolBarModified = true; } /** * Sets the path to the toolbar description file to be loaded when calling {@link #loadDescriptionFile()}. * By default, this file is {@link #DEFAULT_TOOLBAR_FILE_NAME} within the preferences folder. * @param file path to the toolbar descriptor file */ private static void setDescriptionFile(AbstractFile file) throws FileNotFoundException { if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file); } descriptionFile = file; } static AbstractFile getDescriptionFile() throws IOException { if (descriptionFile == null) { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_TOOLBAR_FILE_NAME); } return descriptionFile; } /** * Sets the path to the toolbar description file to be loaded when calling {@link #loadDescriptionFile()}. * By default, this file is {@link #DEFAULT_TOOLBAR_FILE_NAME} within the preferences folder. * @param path path to the toolbar descriptor file */ public static void setDescriptionFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setDescriptionFile(new File(path)); } else { setDescriptionFile(file); } } /** * Sets the path to the toolbar description file to be loaded when calling {@link #loadDescriptionFile()}. * By default, this file is {@link #DEFAULT_TOOLBAR_FILE_NAME} within the preferences folder. * @param file path to the toolbar descriptor file */ private static void setDescriptionFile(File file) throws FileNotFoundException { AbstractFile descriptionFile = FileFactory.getFile(file.getAbsolutePath()); if (descriptionFile != null) { setDescriptionFile(descriptionFile); } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ToolBarIO.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/toolbar/ToolBarReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.toolbar; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.AbstractFile; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.ui.action.ActionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import javax.xml.parsers.SAXParserFactory; import java.io.InputStream; import java.util.List; import java.util.Vector; /** * This class parses the XML file describing the toolbar's buttons and associated actions. * * @author Maxence Bernard, Arik Hadas */ public class ToolBarReader extends ToolBarIO { private static Logger logger; /** Temporarily used for XML parsing */ private List actionIdsV; /** * Starts parsing the XML description file. */ ToolBarReader(AbstractFile descriptionFile) throws Exception { try (InputStream in = new BackupInputStream(descriptionFile)) { SAXParserFactory.newInstance().newSAXParser().parse(in, this); } } String[] getActionsRead() { int nbActions = actionIdsV.size(); String[] actionIds = new String[nbActions]; actionIdsV.toArray(actionIds); return actionIds; } @Override public void startDocument() { actionIdsV = new Vector<>(); } @Override public void endDocument() {} @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { switch (qName) { case BUTTON_ELEMENT: // Resolve action id String actionIdAttribute = attributes.getValue(ACTION_ID_ATTRIBUTE); if (actionIdAttribute != null) { if (ActionManager.isActionExist(actionIdAttribute)) actionIdsV.add(actionIdAttribute); else getLogger().warn("Error in " + DEFAULT_TOOLBAR_FILE_NAME + ": action id \"" + actionIdAttribute + "\" not found"); } else { // Resolve action class String actionClassAttribute = attributes.getValue(ACTION_ATTRIBUTE); String actionId = ActionManager.extrapolateId(actionClassAttribute); if (ActionManager.isActionExist(actionId)) { actionIdsV.add(actionId); } else { getLogger().warn("Error in " + DEFAULT_TOOLBAR_FILE_NAME + ": action id for class " + actionClassAttribute + " was not found"); } } break; case SEPARATOR_ELEMENT: actionIdsV.add(null); break; case ROOT_ELEMENT: // Note: early 0.8 beta3 nightly builds did not have version attribute, so the attribute may be null String fileVersion = attributes.getValue(VERSION_ATTRIBUTE); // if the file's version is not up-to-date, update the file to the current version at quitting. if (!RuntimeConstants.VERSION.equals(fileVersion)) { setModified(); } break; } } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ToolBarReader.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/toolbar/ToolBarWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.toolbar; import java.io.IOException; import java.io.OutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.RuntimeConstants; import com.mucommander.io.backup.BackupOutputStream; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; /** * This class is responsible for writing the tool-bar attributes (buttons and separators). * * @author Arik Hadas */ public class ToolBarWriter extends ToolBarIO { private static final Logger LOGGER = LoggerFactory.getLogger(ToolBarWriter.class); private static ToolBarWriter instance; public static ToolBarWriter create() { if (instance == null) { instance = new ToolBarWriter(); } return instance; } private ToolBarWriter() {} void write() { String[] actionIds = ToolBarAttributes.getActions(); try (BackupOutputStream bos = new BackupOutputStream(getDescriptionFile())) { new Writer(bos).write(actionIds); wasToolBarModified = false; } catch (Exception e) { LOGGER.debug("Caught exception", e); } } private static class Writer { private final XmlWriter writer; private Writer(OutputStream stream) throws IOException { this.writer = new XmlWriter(stream); } private void write(String[] actionIds) throws IOException { try { writer.writeCommentLine("See http://trac.mucommander.com/wiki/ToolBar for information on how to customize this file"); XmlAttributes rootElementAttributes = new XmlAttributes(); rootElementAttributes.add(VERSION_ATTRIBUTE, RuntimeConstants.VERSION); writer.startElement(ROOT_ELEMENT, rootElementAttributes, true); for (String actionId : actionIds) { write(actionId); } } finally { writer.endElement(ROOT_ELEMENT); } } private void write(String actionId) throws IOException { if (actionId == null) { writer.writeStandAloneElement(SEPARATOR_ELEMENT); } else { XmlAttributes attributes = new XmlAttributes(); attributes.add(ACTION_ID_ATTRIBUTE, actionId); // AppLogger.finest("Writing button: action_id = " + attributes.getValue(ACTION_ATTRIBUTE_ID) + ", alt_action_id = " + attributes.getValue(ALT_ACTION_ATTRIBUTE_ID)); writer.writeStandAloneElement(BUTTON_ELEMENT, attributes); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/AbstractIOThreadManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A class that monitors IOThread if it is running or has been blocked. * This class maintains a list of tasks to execute and a thread that * executes these tasks. It checks periodically if the IOThread is running. * If IOThread has been blocked then it's killed and a new IOThread is * instantiated. Then the next getTask from the list will be executed. * @author Mariusz Jakubowski * */ public class AbstractIOThreadManager extends Thread { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractIOThreadManager.class); /** a queue with tasks to execute */ protected final List queue = Collections.synchronizedList(new ArrayList<>()); /** a thread that executes tasks */ protected IOThread ioThread; /** a time after i/o thread is marked as blocked */ protected long blockThreshold; /** * Creates a new monitoring thread. * @param name a name of this thread * @param blockThreshold a time after an i/o getTask is marked as blocked [ms] */ public AbstractIOThreadManager(String name, long blockThreshold) { super(name); this.blockThreshold = blockThreshold; ioThread = new IOThread(queue, blockThreshold); ioThread.start(); } /** * Adds new getTask to execute. A getTask is an instance of Runnable interface. * A proper exception handling within the Runnable instance have to be implemented. * If this getTask rises an exception, this exception is printed to stderr. * @param task a getTask to be executed */ public void addTask(Runnable task) { queue.add(task); synchronized(ioThread) { ioThread.notify(); } } @Override public void run() { while (!interrupted()) { synchronized (queue) { if (ioThread.isBlocked()) { LOGGER.debug("Killing IOThread " + ioThread); ioThread.interrupt(); ioThread = new IOThread(queue, blockThreshold); ioThread.start(); } } try { sleep(blockThreshold); } catch (InterruptedException e) { break; } } ioThread.interrupt(); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/CachedDirectory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import java.util.Arrays; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.ProxyFile; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; /** * A class that holds cached children of a directory. * * @author Mariusz Jakubowski * */ public class CachedDirectory extends ProxyFile { private static final Logger LOGGER = LoggerFactory.getLogger(CachedDirectory.class); private static final ImageIcon NOT_ACCESSIBLE_ICON = IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.NOT_ACCESSIBLE_FILE); /** an array of cached children */ private AbstractFile[] cachedChildren = null; /** a flag indicating that a thread is running, caching children */ private boolean readingChildren = false; /** a timestamp of last modification time of this directory */ private long lsTimeStamp = -1; /** a cache in which this object is stored */ private final DirectoryCache cache; /** a cached icon */ private Icon cachedIcon; /** * Creates a new instance. * * @param directory a directory to cache */ public CachedDirectory(AbstractFile directory, DirectoryCache cache) { super(directory); this.cache = cache; } /** * Checks if this directory is already cached. If it isn't cached then a new * cache thread is started. * @return true if directory is cached, false otherwise */ public synchronized boolean isCached() { // check if caching thread is running if (isReadingChildren()) { return false; } // check if directory contents changed if (lsTimeStamp != file.getLastModifiedDate()) { setReadingChildren(true); // read children in caching thread TreeIOThreadManager.getInstance().addTask(this::lsAsync); return false; } return true; } /** * Gets children of current directory. Files are filtered and then sorted. This * method is executed in caching thread. */ private void lsAsync() { if (getCachedIcon() == null || getCachedIcon() == NOT_ACCESSIBLE_ICON) { setCachedIcon(FileIcons.getFileIcon(getProxiedFile())); } AbstractFile[] children; try { children = file.ls(cache.getFilter()); } catch (Exception e) { LOGGER.debug("Caught exception", e); children = new AbstractFile[0]; setCachedIcon(NOT_ACCESSIBLE_ICON); } Arrays.sort(children, cache.getSort()); Icon[] icons = new Icon[children.length]; for (int i = 0; i < children.length; i++) { icons[i] = FileIcons.getFileIcon(children[i]); } synchronized (cache) { for (int i = 0; i < children.length; i++) { CachedDirectory cachedChild = cache.getOrAdd(children[i]); cachedChild.setCachedIcon(icons[i]); } } final AbstractFile[] children2 = children; try { /* * Set cache to new value. This is invoked in swing thread * so event listeners are called from right thread. */ SwingUtilities.invokeAndWait(() -> setLsCache(children2, file.getLastModifiedDate())); } catch (Exception e) { LOGGER.debug("Caught exception", e); } } /** * Sets cache information. * @param children array of children of this directory * @param lsTimeStamp timestamp of cache */ private synchronized void setLsCache(AbstractFile[] children, long lsTimeStamp) { this.lsTimeStamp = lsTimeStamp; this.cachedChildren = children; setReadingChildren(false); } /** * Returns true if caching thread is running. */ public synchronized boolean isReadingChildren() { return readingChildren; } /** * Sets a flag that indicates if caching thread is running. This method also * initializes spinning icon. * @param readingChildren */ private synchronized void setReadingChildren(boolean readingChildren) { this.readingChildren = readingChildren; cache.fireChildrenCached(this, readingChildren); } /** * Gets cached children. * @return cached children. */ public synchronized AbstractFile[] get() { return cachedChildren; } /** * Gets a cached icon for this folder. * @return a cached icon */ public Icon getCachedIcon() { return cachedIcon; } /** * Sets a cached icon for this folder. * @param cachedIcon a cached icon */ public void setCachedIcon(Icon cachedIcon) { this.cachedIcon = cachedIcon; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/CachedDirectoryListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import com.mucommander.commons.file.AbstractFile; import java.util.EventListener; /** * An interface that listeners to a directory cache must implement. * @author Mariusz Jakubowski * */ public interface CachedDirectoryListener extends EventListener { void cachingStarted(AbstractFile parent); void cachingEnded(AbstractFile parent); } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/DirectoryCache.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.util.FileComparator; import javax.swing.event.EventListenerList; import java.util.HashMap; import java.util.Map; /** * This class holds cached directories. * It maps AbstractFiles to DirectoryCache instances. * @author Mariusz Jakubowski * */ public class DirectoryCache { /** a map that holds cached folders */ private final Map cache = new HashMap<>(); /** Comparator used to sort folders */ private final FileComparator sort; /** A file IMAGE_FILTER */ private final FileFilter filter; /** Listeners. */ private final EventListenerList listenerList = new EventListenerList(); /** * Creates a new directory cache. * @param filter IMAGE_FILTER used to IMAGE_FILTER children directories. * @param sort a comparator used to sort children */ DirectoryCache(FileFilter filter, FileComparator sort) { //this.cache = Collections.synchronizedMap(new HashMap()); this.filter = filter; this.sort = sort; } /** * Returns current sort order. */ public FileComparator getSort() { return sort; } /** * Returns current IMAGE_FILTER. */ public FileFilter getFilter() { return filter; } /** * Fires a cachingStarted or cachingEnded event on all listeners. * @param cachedDirectory a directory those children has been cached * @param readingChildren */ void fireChildrenCached(CachedDirectory cachedDirectory, boolean readingChildren) { Object[] listeners = listenerList.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == CachedDirectoryListener.class) { if (readingChildren) { ((CachedDirectoryListener) listeners[i + 1]).cachingStarted(cachedDirectory); } else { ((CachedDirectoryListener) listeners[i + 1]).cachingEnded(cachedDirectory); } } } } void addCachedDirectoryListener(CachedDirectoryListener l) { listenerList.add(CachedDirectoryListener.class, l); } public void removeCachedDirectoryListener(CachedDirectoryListener l) { listenerList.remove(CachedDirectoryListener.class, l); } public synchronized void clear() { cache.clear(); } public synchronized CachedDirectory get(AbstractFile key) { return cache.get(key); } public synchronized void put(AbstractFile key, CachedDirectory value) { cache.put(key, value); } /** * Deletes entry and all children from the cache. */ synchronized void removeWithChildren(AbstractFile key) { CachedDirectory cachedDir = cache.get(key); if (cachedDir != null) { cache.remove(key); AbstractFile[] children = cachedDir.get(); if (children != null) { for (AbstractFile child : children) { removeWithChildren(child); } } } } /** * Gets a cached instance of a file. If the cached instance * of the file doesn't exists it's added to the cache. * @param key an AbstractFile instance * @return a cached file instance */ synchronized CachedDirectory getOrAdd(AbstractFile key) { CachedDirectory cachedDir = cache.get(key); if (cachedDir == null) { cachedDir = new CachedDirectory(key, this); cache.put(key, cachedDir); } return cachedDir; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/FilesTreeModel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.FileFilter; import com.mucommander.commons.file.util.FileComparator; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.SpinningDial; import javax.swing.*; import javax.swing.event.EventListenerList; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import java.util.Arrays; /** * A tree model for files. * This class contains a tree structure defined by AbstractFile objects. * @author Mariusz Jakubowski * */ public class FilesTreeModel implements TreeModel, CachedDirectoryListener { private final DirectoryCache cache; /** Comparator used to sort folders */ private final FileComparator sort; /** Listeners. */ private final EventListenerList listenerList = new EventListenerList(); /** Root of the directory tree. */ private AbstractFile root; /** number of caching children at the time, used to control spinning icon */ private int cachingNum = 0; /** icon used to show that a children of a directory are being cached */ private final SpinningDial spinningIcon = new SpinningDial(16, 16, false); FilesTreeModel(FileFilter filter, FileComparator sort) { super(); this.sort = sort; cache = new DirectoryCache(filter, sort); cache.addCachedDirectoryListener(this); } /** * Changes the current root of a tree * Fires 'tree structure changed' event. * @param newRoot the new root of a tree */ public void setRoot(AbstractFile newRoot) { final CachedDirectory cachedRoot = new CachedDirectory(newRoot, cache); cachedRoot.setCachedIcon(FileIcons.getFileIcon(newRoot)); SwingUtilities.invokeLater(() -> { root = cachedRoot.getProxiedFile(); cache.clear(); cache.put(root, cachedRoot); TreePath path = new TreePath(root); fireTreeStructureChanged(this, path); }); } public Object getRoot() { return root; } /** * Returns children folders of a parent folder sorted by name. * @param parent parent folder * @return children folders */ private AbstractFile[] getChildren(AbstractFile parent) { CachedDirectory cachedDir = cache.getOrAdd(parent); return cachedDir.isCached() ? cachedDir.get() : null; } public Object getChild(Object parent, int index) { AbstractFile[] children = getChildren((AbstractFile) parent); return children != null ? children[index] : null; } public int getChildCount(Object parent) { AbstractFile[] children = getChildren((AbstractFile) parent); return children != null ? children.length : 0; } public int getIndexOfChild(Object parent, Object child) { AbstractFile[] children = getChildren((AbstractFile) parent); return children != null ? Arrays.binarySearch(children, (AbstractFile)child, sort) : 0; } public boolean isLeaf(Object node) { return false; } public void valueForPathChanged(TreePath path, Object newValue) { } /** * Notifies all listeners that have registered interest for notification on this event type. * @param source the node where the tree model has changed * @param path the path to the root node * @see EventListenerList */ void fireTreeStructureChanged(Object source, TreePath path) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); TreeModelEvent e = null; // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == TreeModelListener.class) { // Lazily create the event: if (e == null) { e = new TreeModelEvent(source, path); } ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e); } } } /** * Builds the parents of node up to and including the root node, where the original node is the last element * in the returned array. The length of the returned array gives the node's depth in the tree. * @param aNode the TreeNode to get the path for */ AbstractFile[] getPathToRoot(AbstractFile aNode) { return getPathToRoot(aNode, 0); } /** * Builds the parents of node up to and including the root node, * where the original node is the last element in the returned array. * The length of the returned array gives the node's depth in the * tree. * * @param aNode the TreeNode to get the path for * @param depth an int giving the number of steps already taken towards * the root (on recursive calls), used to size the returned array * @return an array of TreeNodes giving the path from the root to the * specified node */ private AbstractFile[] getPathToRoot(AbstractFile aNode, int depth) { AbstractFile[] retNodes; // This method recurses, traversing towards the root in order // size the array. On the way back, it fills in the nodes, // starting from the root and working back to the original node. /* Check for null, in case someone passed in a null node, or they passed in an element that isn't rooted at root. */ if (aNode == null) { if (depth == 0) { return null; } else { retNodes = new AbstractFile[depth]; } } else { depth++; retNodes = aNode == root ? new AbstractFile[depth] : getPathToRoot(aNode.getParent(), depth); retNodes[retNodes.length - depth] = aNode; cache.getOrAdd(aNode).isCached(); // ensures that a path is in cache } return retNodes; } public void addTreeModelListener(TreeModelListener l) { listenerList.add(TreeModelListener.class, l); } public void removeTreeModelListener(TreeModelListener l) { listenerList.remove(TreeModelListener.class, l); } /** * Refreshes tree model from given path. * @param path a path to refresh */ public void refresh(TreePath path) { AbstractFile folder = (AbstractFile) path.getLastPathComponent(); CachedDirectory cached = cache.get(folder); Icon cachedIcon = cached.getCachedIcon(); cache.removeWithChildren(folder); cached = cache.getOrAdd(folder); cached.setCachedIcon(cachedIcon); fireTreeStructureChanged(this, path); } public void cachingStarted(AbstractFile parent) { cachingNum++; if (cachingNum == 1) { spinningIcon.setAnimated(true); } } public void cachingEnded(AbstractFile parent) { cachingNum--; if (cachingNum == 0) { spinningIcon.setAnimated(false); } TreePath path = new TreePath(getPathToRoot(parent)); fireTreeStructureChanged(this, path); } /** * Returns an icon of this directory or spinning icon if this directory is * being cached. * @return an icon of this directory or spinning icon if this directory is * being cached. */ Icon getCurrentIcon(AbstractFile file) { CachedDirectory cached = cache.get(file); if (cached != null) { return cached.isReadingChildren() ? spinningIcon : cached.getCachedIcon(); } return spinningIcon; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/FoldersTreePanel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import com.mucommander.ui.PreloadedJFrame; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.AndFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter; import com.mucommander.commons.file.filter.AttributeFileFilter.FileAttribute; import com.mucommander.commons.file.util.FileComparator; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.RefreshAction; import com.mucommander.ui.event.LocationEvent; import com.mucommander.ui.event.LocationListener; import com.mucommander.ui.main.ConfigurableFolderFilter; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.ui.theme.ThemeListener; /** * A panel which contains a directory tree. This panel is attached to the left * side of the files table. It allows for a quick navigation in a directory * tree. Selecting folder on the tree changes folder in files folder. * * @author Mariusz Jakubowski * */ @Slf4j public class FoldersTreePanel implements TreeSelectionListener, LocationListener, FocusListener, ThemeListener, TreeModelListener, ConfigurationListener { /** Directory tree * -- GETTER -- * Returns tree component. */ @Getter private final JTree tree; @Getter private final JPanel panel; /** Folder panel to which this tree is attached */ private final FolderPanel folderPanel; /** A model with a directory tree */ private final FilesTreeModel model; /** A timer that fires a directory change */ private final ChangeTimer changeTimer = new ChangeTimer(); static { TreeIOThreadManager.getInstance().start(); } /** * Creates a panel with directory tree attached to a specified folder panel. * @param folderPanel a folder panel to attach tree */ public FoldersTreePanel(FolderPanel folderPanel) { panel = PreloadedJFrame.getJPanel(new BorderLayout()); this.folderPanel = folderPanel; panel.setLayout(new BorderLayout()); // Filters out the files that should not be displayed in the tree view AndFileFilter treeFileFilter = new AndFileFilter( new AttributeFileFilter(FileAttribute.DIRECTORY), new ConfigurableFolderFilter() ); FileComparator sort = new FileComparator(FileComparator.NAME_CRITERION, true, true, false); model = new FilesTreeModel(treeFileFilter, sort); tree = new JTree(model); tree.setFont(ThemeCache.tableFont); tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setExpandsSelectedPaths(true); tree.getModel().addTreeModelListener(this); JScrollPane sp = new JScrollPane(tree); // JScrollPane usually comes with a tiny border, remove it sp.setBorder(null); panel.add(sp, BorderLayout.CENTER); // create tree renderer. We're not using default tree renderer, because // AbstractFile.toString method returns full path, and we want to // display only a file name. FoldersTreeRenderer renderer = new FoldersTreeRenderer(tree); tree.setCellRenderer(renderer); tree.addTreeSelectionListener(this); tree.addFocusListener(this); // add a popup menu final JPopupMenu popup = new JPopupMenu(); // refresh action JMenuItem item = new JMenuItem( ActionProperties.getActionLabel(RefreshAction.Descriptor.ACTION_ID), KeyEvent.VK_R); item.addActionListener(e -> { model.refresh(tree.getSelectionPath()); // model.fireTreeStructureChanged(tree, tree.getSelectionPath()); }); popup.add(item); tree.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { popup.show(e.getComponent(), e.getX(), e.getY()); } } }); ThemeCache.addThemeListener(this); TcConfigurations.addPreferencesListener(this); } /** * Listens to certain configuration variables. */ public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); if (var.equals(TcPreferences.SHOW_HIDDEN_FILES) || var.equals(TcPreferences.SHOW_DS_STORE_FILES) || var.equals(TcPreferences.SHOW_SYSTEM_FOLDERS)) { Object root = model.getRoot(); if (root != null) { TreePath path = new TreePath(root); model.refresh(path); } } } /** * Adds or removes location change listeners depending on the tree * visibility. */ public void setVisible(boolean flag) { panel.setVisible(flag); if (flag) { updateSelectedFolder(); folderPanel.getLocationManager().addLocationListener(this); // tree.requestFocus(); } else { folderPanel.getLocationManager().removeLocationListener(this); } } /** * Updates selection in a tree to the current folder. When necessary updates * the current root of a tree. Invoked when location on folder pane has changed or * when a tree has been updated (when directories have been loaded). */ private void updateSelectedFolder() { final AbstractFile currentFolder = folderPanel.getCurrentFolder(); // get selected directory (ignore archives - TODO make archives browsable (option)) final AbstractFile parentFolder = getParentFolder(currentFolder); // compare selection on tree and panel TreePath selectionPath = tree.getSelectionPath(); if (selectionPath != null) { if (selectionPath.getLastPathComponent() == currentFolder) { return; } } // check if root has changed final AbstractFile currentRoot = parentFolder.getRoot(); if (!currentRoot.equals(model.getRoot())) { model.setRoot(currentRoot); } // refresh selection on tree SwingUtilities.invokeLater(() -> { try { TreePath path = new TreePath(model.getPathToRoot(parentFolder)); tree.expandPath(path); tree.setSelectionPath(path); tree.scrollPathToVisible(path); } catch (Exception e) { log.debug("Caught exception", e); } }); } @NotNull private AbstractFile getParentFolder(AbstractFile currentFolder) { AbstractFile tempFolder = currentFolder; while (!tempFolder.isDirectory()) { AbstractFile tempParent = tempFolder.getParent(); if (tempParent == null) { break; } tempFolder = tempParent; } return tempFolder; } /** * Refreshes folder after a change (e.g. mkdir). * @param folder a folder to refresh on the tree */ public void refreshFolder(AbstractFile folder) { if (!panel.isVisible()) { return; } model.fireTreeStructureChanged(tree, new TreePath(model.getPathToRoot(folder))); } /** * Changes focus to tree. */ public void requestFocus() { tree.requestFocus(); } // - TreeSelectionListener code -------------------------------------------- // ------------------------------------------------------------------------- /** * This class is used to change folder after a user selects a folder in * tree. This change occurs after small delay (1 sec) to allow a user to * navigate a tree using keyboard. * * @author Mariusz Jakubowski * */ private class ChangeTimer extends Timer { private transient AbstractFile folder; ChangeTimer() { super(1000, null); setRepeats(false); } @Override public void fireActionPerformed(ActionEvent ae) { if (!folderPanel.getCurrentFolder().equals(folder)) { folderPanel.tryChangeCurrentFolder(folder); } } } /** * Changes the current folder in an associated folder panel, depending on * the current selection in tree. */ public void valueChanged(TreeSelectionEvent e) { TreePath path = e.getNewLeadSelectionPath(); if (path != null) { AbstractFile f = (AbstractFile) path.getLastPathComponent(); if (f != null && f.isBrowsable() && f != folderPanel.getCurrentFolder()) { changeTimer.folder = f; changeTimer.restart(); } } } @Override public void locationCancelled(LocationEvent locationEvent) { } @Override public void locationChanged(LocationEvent locationEvent) { updateSelectedFolder(); } @Override public void locationChanging(LocationEvent locationEvent) { } @Override public void locationFailed(LocationEvent locationEvent) { } @Override public void focusGained(FocusEvent e) { tree.setBackground(ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]); } @Override public void focusLost(FocusEvent e) { tree.setBackground(ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]); } @Override public void colorChanged(ColorChangedEvent event) { int type = tree.hasFocus() ? ThemeCache.ACTIVE : ThemeCache.INACTIVE; tree.setBackground(ThemeCache.backgroundColors[type][ThemeCache.NORMAL]); tree.repaint(); } @Override public void fontChanged(FontChangedEvent event) { tree.setFont(ThemeCache.tableFont); tree.repaint(); } @Override public void treeNodesChanged(TreeModelEvent e) { } @Override public void treeNodesInserted(TreeModelEvent e) { } @Override public void treeNodesRemoved(TreeModelEvent e) { } @Override public void treeStructureChanged(TreeModelEvent e) { // ensures that a selection is repainted correctly // after nodes have been inserted if (!changeTimer.isRunning()) { updateSelectedFolder(); tree.repaint(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/FoldersTreeRenderer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.theme.ThemeCache; import javax.swing.*; import javax.swing.tree.DefaultTreeCellRenderer; import java.awt.*; /** * A renderer for the directory tree. It renders model's items (which are * AbstractFiles), using file names. It also renders a correct icon for a folder. * * @author Mariusz Jakubowski * */ public class FoldersTreeRenderer extends DefaultTreeCellRenderer { private final JTree tree; private final FilesTreeModel model; FoldersTreeRenderer(JTree tree) { super(); this.tree = tree; this.model = (FilesTreeModel) tree.getModel(); } @Override public Color getBackgroundSelectionColor() { if (tree!=null && tree.hasFocus()) { return ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED]; } else { return ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.SELECTED]; } } @Override public Color getBackgroundNonSelectionColor() { if (tree!=null && tree.hasFocus()) { return ThemeCache.backgroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL]; } else { return ThemeCache.backgroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL]; } } @Override public Color getForeground() { if (tree!=null && tree.hasFocus()) { return selected ? ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.SELECTED][ThemeCache.FOLDER] : ThemeCache.foregroundColors[ThemeCache.ACTIVE][ThemeCache.NORMAL][ThemeCache.FOLDER]; } else { return selected ? ThemeCache.foregroundColors[ThemeCache.INACTIVE][ThemeCache.SELECTED][ThemeCache.FOLDER] : ThemeCache.foregroundColors[ThemeCache.INACTIVE][ThemeCache.NORMAL][ThemeCache.FOLDER]; } } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { // get file name and create default component (JLabel) to display it AbstractFile file = (AbstractFile) value; String name = file.isRoot()?file.getAbsolutePath():file.getName(); super.getTreeCellRendererComponent(tree, name, sel, expanded, leaf, row, hasFocus); setIcon(model.getCurrentIcon(file)); return this; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/IOThread.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A thread that executes i/o operations. * @author Mariusz Jakubowski * */ public class IOThread extends Thread { private static final Logger LOGGER = LoggerFactory.getLogger(IOThread.class); /** a queue with tasks to execute */ private final List queue; /** a time after this thread is marked as blocked */ private final long blockThreshold; /** a time when this thread signalled that is alive */ private volatile long lastActionTime = 0; /** * Creates a new instance of an IOThread. * @param queue a queue with tasks * @param blockThreshold a time after this thread is marked as blocked [ms] */ IOThread(List queue, long blockThreshold) { super("IOThread"); this.queue = queue; this.blockThreshold = blockThreshold; } @Override public void run() { while (!interrupted()) { lastActionTime = System.currentTimeMillis(); while (!queue.isEmpty()) { Runnable task = queue.remove(0); try { task.run(); } catch (Exception e) { LOGGER.debug("Caught exception", e); } lastActionTime = System.currentTimeMillis(); } try { synchronized (this) { wait(blockThreshold / 2); } } catch (InterruptedException e) { break; } } } /** * Checks if current thread is blocked. This is done by checking if * last action time is smaller than block threshold. * @return true if thread is running */ public boolean isBlocked() { return (lastActionTime != 0) && (System.currentTimeMillis() - lastActionTime > blockThreshold); } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/TreeIOThreadManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.main.tree; /** * Monitors thread that reads children and icons for the tree. * @author Mariusz Jakubowski * */ public class TreeIOThreadManager extends AbstractIOThreadManager { public final static TreeIOThreadManager instance = new TreeIOThreadManager(); private TreeIOThreadManager() { super("TreeIOThreadManager", 5000); } public static TreeIOThreadManager getInstance() { return instance; } } ================================================ FILE: src/main/java/com/mucommander/ui/main/tree/package.html ================================================ Contains classes used to display a directory tree. ================================================ FILE: src/main/java/com/mucommander/ui/menu/JScrollMenu.java ================================================ package com.mucommander.ui.menu; import ru.trolsoft.ui.TMenuSeparator; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.MenuElement; import javax.swing.UIManager; import javax.swing.plaf.MenuItemUI; import javax.swing.plaf.PopupMenuUI; import java.awt.Component; import java.awt.ComponentOrientation; public class JScrollMenu extends JMenu { // Covers the one in the JMenu because the method that creates it in JMenu is private /** * The popup menu portion of the menu. */ private JScrollPopupMenu popupMenu; /** * Constructs a new JMenu with no text. */ public JScrollMenu() { this(""); } /** * Constructs a new JMenu with the supplied string as its text. * * @param s the text for the menu label */ public JScrollMenu(String s) { super(s); } /** * Constructs a menu whose properties are taken from the Action supplied. * * @param a an Action */ public JScrollMenu(Action a) { this(); setAction(a); } /** * Lazily creates the popup menu. This method will create the popup using the JScrollPopupMenu class. */ protected void ensurePopupMenuCreated() { if (popupMenu == null) { popupMenu = new JScrollPopupMenu(); popupMenu.setInvoker(this); popupListener = createWinListener(popupMenu); } } ////////////////////////////// //// All of these methods are necessary because ensurePopupMenuCreated() is private in JMenu ////////////////////////////// @Override public void updateUI() { setUI((MenuItemUI) UIManager.getUI(this)); if (popupMenu != null) { popupMenu.setUI((PopupMenuUI) UIManager.getUI(popupMenu)); } } @Override public boolean isPopupMenuVisible() { ensurePopupMenuCreated(); return popupMenu.isVisible(); } @Override public void setMenuLocation(int x, int y) { super.setMenuLocation(x, y); if (popupMenu != null) { popupMenu.setLocation(x, y); } } @Override public JMenuItem add(JMenuItem menuItem) { ensurePopupMenuCreated(); return popupMenu.add(menuItem); } @Override public Component add(Component c) { ensurePopupMenuCreated(); popupMenu.add(c); return c; } @Override public Component add(Component c, int index) { ensurePopupMenuCreated(); popupMenu.add(c, index); return c; } @Override public void addSeparator() { ensurePopupMenuCreated(); popupMenu.add(new TMenuSeparator()); } @Override public void insert(String s, int pos) { ensurePopupMenuCreated(); popupMenu.insert(new JMenuItem(s), pos); } @Override public JMenuItem insert(JMenuItem mi, int pos) { ensurePopupMenuCreated(); popupMenu.insert(mi, pos); return mi; } @Override public JMenuItem insert(Action a, int pos) { ensurePopupMenuCreated(); JMenuItem mi = new JMenuItem(a); mi.setHorizontalTextPosition(JButton.TRAILING); mi.setVerticalTextPosition(JButton.CENTER); popupMenu.insert(mi, pos); return mi; } @Override public void insertSeparator(int index) { ensurePopupMenuCreated(); popupMenu.insert(new JPopupMenu.Separator(), index); } @Override public void remove(JMenuItem item) { if (popupMenu != null) { popupMenu.remove(item); } } @Override public void remove(int pos) { if (pos < 0) { throw new IllegalArgumentException("index less than zero."); } if (pos > getItemCount()) { throw new IllegalArgumentException("index greater than the number of items."); } if (popupMenu != null) { popupMenu.remove(pos); } } @Override public void remove(Component c) { if (popupMenu != null) { popupMenu.remove(c); } } @Override public void removeAll() { if (popupMenu != null) { for (int i = getMenuComponentCount() - 2; i >= 0; i--) { remove(i); } } } @Override public int getMenuComponentCount() { return (popupMenu == null) ? 0 : popupMenu.getComponentCount(); } @Override public Component getMenuComponent(int n) { return (popupMenu == null) ? null : popupMenu.getComponent(n); } @Override public Component[] getMenuComponents() { return (popupMenu == null) ? new Component[0] : popupMenu.getComponents(); } @Override public JScrollPopupMenu getPopupMenu() { ensurePopupMenuCreated(); return popupMenu; } @Override public MenuElement[] getSubElements() { return popupMenu == null ? new MenuElement[0] : new MenuElement[]{popupMenu}; } @Override public void applyComponentOrientation(ComponentOrientation o) { super.applyComponentOrientation(o); if (popupMenu != null) { int ncomponents = getMenuComponentCount(); for (int i = 0; i < ncomponents; ++i) { getMenuComponent(i).applyComponentOrientation(o); } popupMenu.setComponentOrientation(o); } } @Override public void setComponentOrientation(ComponentOrientation o) { super.setComponentOrientation(o); if (popupMenu != null) { popupMenu.setComponentOrientation(o); } } } ================================================ FILE: src/main/java/com/mucommander/ui/menu/JScrollPopupMenu.java ================================================ package com.mucommander.ui.menu; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.event.MouseWheelEvent; public class JScrollPopupMenu extends JPopupMenu { private int maximumVisibleRows = 10; public JScrollPopupMenu() { this(null); } public JScrollPopupMenu(String label) { super(label); setLayout(new ScrollPopupMenuLayout()); super.add(getScrollBar()); addMouseWheelListener(event -> { JScrollBar scrollBar = getScrollBar(); int amount = (event.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) ? event.getUnitsToScroll() * scrollBar.getUnitIncrement() : (event.getWheelRotation() < 0 ? -1 : 1) * scrollBar.getBlockIncrement(); scrollBar.setValue(scrollBar.getValue() + amount); event.consume(); }); } private JScrollBar popupScrollBar; protected JScrollBar getScrollBar() { if (popupScrollBar == null) { popupScrollBar = new JScrollBar(JScrollBar.VERTICAL); popupScrollBar.addAdjustmentListener(e -> { doLayout(); repaint(); }); popupScrollBar.setVisible(false); } return popupScrollBar; } public int getMaximumVisibleRows() { return maximumVisibleRows; } public void setMaximumVisibleRows(int maximumVisibleRows) { this.maximumVisibleRows = maximumVisibleRows; } @Override public void paintChildren(Graphics g) { Insets insets = getInsets(); g.clipRect(insets.left, insets.top, getWidth(), getHeight() - insets.top - insets.bottom); super.paintChildren(g); } @Override protected void addImpl(Component comp, Object constraints, int index) { super.addImpl(comp, constraints, index); if (maximumVisibleRows < getComponentCount() - 1) { getScrollBar().setVisible(true); } } @Override public void remove(int index) { // can't remove the scrollbar ++index; super.remove(index); if (maximumVisibleRows >= getComponentCount() - 1) { getScrollBar().setVisible(false); } } @Override public void show(Component invoker, int x, int y) { JScrollBar scrollBar = getScrollBar(); if (scrollBar.isVisible()) { int extent = 0; int max = 0; int i = 0; int unit = -1; int width = 0; for (Component comp : getComponents()) { if (!(comp instanceof JScrollBar)) { Dimension preferredSize = comp.getPreferredSize(); width = Math.max(width, preferredSize.width); if (unit < 0) { unit = preferredSize.height; } if (i++ < maximumVisibleRows) { extent += preferredSize.height; } max += preferredSize.height; } } Insets insets = getInsets(); int widthMargin = insets.left + insets.right; int heightMargin = insets.top + insets.bottom; scrollBar.setUnitIncrement(unit); scrollBar.setBlockIncrement(extent); scrollBar.setValues(0, heightMargin + extent, 0, heightMargin + max); width += scrollBar.getPreferredSize().width + widthMargin; int height = heightMargin + extent; setPopupSize(new Dimension(width, height)); } super.show(invoker, x, y); } protected static class ScrollPopupMenuLayout implements LayoutManager { @Override public void addLayoutComponent(String name, Component comp) { } @Override public void removeLayoutComponent(Component comp) { } @Override public Dimension preferredLayoutSize(Container parent) { int visibleAmount = Integer.MAX_VALUE; Dimension dim = new Dimension(); for (Component comp : parent.getComponents()) { if (comp.isVisible()) { if (comp instanceof JScrollBar) { JScrollBar scrollBar = (JScrollBar) comp; visibleAmount = scrollBar.getVisibleAmount(); } else { Dimension pref = comp.getPreferredSize(); dim.width = Math.max(dim.width, pref.width); dim.height += pref.height; } } } Insets insets = parent.getInsets(); dim.height = Math.min(dim.height + insets.top + insets.bottom, visibleAmount); return dim; } @Override public Dimension minimumLayoutSize(Container parent) { int visibleAmount = Integer.MAX_VALUE; Dimension dim = new Dimension(); for (Component comp : parent.getComponents()) { if (comp.isVisible()) { if (comp instanceof JScrollBar) { JScrollBar scrollBar = (JScrollBar) comp; visibleAmount = scrollBar.getVisibleAmount(); } else { Dimension min = comp.getMinimumSize(); dim.width = Math.max(dim.width, min.width); dim.height += min.height; } } } Insets insets = parent.getInsets(); dim.height = Math.min(dim.height + insets.top + insets.bottom, visibleAmount); return dim; } @Override public void layoutContainer(Container parent) { Insets insets = parent.getInsets(); int width = parent.getWidth() - insets.left - insets.right; int height = parent.getHeight() - insets.top - insets.bottom; int x = insets.left; int y = insets.top; int position = 0; for (Component comp : parent.getComponents()) { if ((comp instanceof JScrollBar) && comp.isVisible()) { JScrollBar scrollBar = (JScrollBar) comp; Dimension dim = scrollBar.getPreferredSize(); scrollBar.setBounds(x + width - dim.width, y, dim.width, height); width -= dim.width; position = scrollBar.getValue(); } } y -= position; for (Component comp : parent.getComponents()) { if (!(comp instanceof JScrollBar) && comp.isVisible()) { Dimension pref = comp.getPreferredSize(); comp.setBounds(x, y, width, pref.height); y += pref.height; } } } } } ================================================ FILE: src/main/java/com/mucommander/ui/notifier/AbstractNotifier.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.notifier; import java.awt.SystemTray; import javax.swing.SwingUtilities; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.main.WindowManager; /** * AbstractNotifier is a generic representation of a system notifier. It also provides factory methods to * retrieve the current platform's notifier instance, if there is one. *

    * A notifier serves the purpose of displaying notifications to the screen, to inform the user of an event when * the application is not visible (in the background). *

    * The notifier instance returned by {@link #getNotifier()} is platform-dependent. At this time, two notifier * implementations are available: *

      *
    • {@link GrowlNotifier}: for Mac OS X, requires Growl to be installed *
    • {@link SystemTrayNotifier}: for Java 1.6 and up, using the java.awt.SystemTray API *
    * * @author Maxence Bernard */ public abstract class AbstractNotifier { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractNotifier.class); /** AbstractNotifier instance, null if none is available on the current platform */ private static AbstractNotifier notifier; static { // Finds and creates a suitable AbstractNotifier instance for the platform, if there is one if (OsFamily.MAC_OS_X.isCurrent()) { notifier = new GrowlNotifier(); } else if (SystemTray.isSupported()) { notifier = new SystemTrayNotifier(); } } /** * Returns true if an AbstractNotifier instance is available. In other words, if true is * returned, {@link #getNotifier()} will return a non-null value. * * @return true if an AbstractNotifier instance is available */ public static boolean isAvailable() { return notifier != null; } /** * Returns an AbstractNotifier instance that can be used on the current platform, null if none * is available. * Note that the returned AbstractNotifier must be enabled before it can be used, which is not * guaranteed to succeed. * * @return an AbstractNotifier instance that can be used on the current platform, null if none is available */ public static AbstractNotifier getNotifier() { return notifier; } /** * Displays a notification with the specified type, title and description and returns true if the * notification could be displayed. The notification will not be displayed if the current muCommander window * (or one of its child windows) is presently in the foreground, so that the user doesn't get notified for things * that he/she can already see on the screen. * *

    * The notification will not be displayed if: *

      *
    • muCommander is in the foreground *
    • this notifier is not enabled *
    • the notification could not be delivered because of an error *
    * *

    * Note that this method is executed in a separate thread after all pending Swing events have been processed, * to ensure in the event of a window being made inactive that the notification will not be triggered. This method * immediately return s(i.e. does not wait for pending events) and thus is not be able to return if the notification * was displayed or not, unlike {@link #displayNotification(NotificationType, String, String)}. * * @param notificationType one of the available notification types, see {@link NotificationType} for possible values * @param title the title of the notification to display * @param description the description of the notification to display */ public void displayBackgroundNotification(final NotificationType notificationType, final String title, final String description) { SwingUtilities.invokeLater(() -> { if (WindowManager.getCurrentMainFrame().isAncestorOfActiveWindow()) { LOGGER.debug("Ignoring notification, application is in foreground"); return; } if (!displayNotification(notificationType, title, description)) { LOGGER.debug("Notification failed to be displayed"); } }); } ////////////////////// // Abstract methods // ////////////////////// /** * Enables/disables this notifier and returns true if the operation succeeded. A typical case * for returning false, is when the underlying notification system (e.g. Growl under Mac OS X) could not be reached. * * @param enabled true to enable this notifier, false to disable it * @return true if the operation succeeded */ public abstract boolean setEnabled(boolean enabled); /** * Returns true if this notifier is enabled and ready to display notifications. * * @return true if this notifier is enabled and ready to display notifications */ public abstract boolean isEnabled(); /** * Displays a notification with the specified type, title and description and returns true if the * notification could be displayed. Unlike {@link #displayBackgroundNotification(NotificationType, String, String)}, the * notification will be attempted for display even if muCommander is currently in the foreground. * *

    * Returns true if the notification could be displayed, false if: *

      *
    • this notifier is not enabled *
    • the notification could not be delivered because of an error *
    * * @param notificationType one of the available notification types, see {@link NotificationType} for possible values * @param title the title of the notification to display * @param description the description of the notification to display * @return true if the notification was properly sent, false otherwise */ public abstract boolean displayNotification(NotificationType notificationType, String title, String description); /** * Returns a pretty name for the underlying notification system that can be displayed to the end user. * * @return a pretty name for the underlying notification system */ public abstract String getPrettyName(); } ================================================ FILE: src/main/java/com/mucommander/ui/notifier/GrowlNotifier.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.notifier; import java.util.Hashtable; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.text.Translator; import com.mucommander.ui.macosx.AppleScript; /** * GrowlNotifier implements a notifier that uses the Growl notification system. * *

    Growl is a third party notification system for Mac OS X, which allows Growl-enabled applications to display small, * unintrusive popup notifications to inform the user of noteworthy events. Growl can be found at: * http://growl.info. * *

    This class communicates with Growl using {@link AppleScript}. More information about the AppleScript syntax can * be found here. * The Growl Java library part of the Growl SDK was previously used but it relied on the Cocoa-Java library which has * been deprecated by Apple since then. * * @author Maxence Bernard */ public class GrowlNotifier extends AbstractNotifier { private static final Logger LOGGER = LoggerFactory.getLogger(GrowlNotifier.class); /** Is this notifier enabled ? */ private static boolean isEnabled; /** Has muCommander been registered ? */ private static boolean isRegistered; /** Dictionary keys for the different notification types */ private final static Map NOTIFICATION_KEYS = new Hashtable<>(); /** Name of the application to be registered with Growl, as spelled in the .app */ private final static String APP_NAME = "muCommander"; /** This AppleScript returns "true" if Growl is currently running, "false" if it isn't */ private final static String IS_GROWL_RUNNING_APPLESCRIPT = "tell application \"System Events\"\n" + "\tset isRunning to (count of (every process whose name is \"GrowlHelperApp\")) > 0\n" + "end tell"; static { NOTIFICATION_KEYS.put(NotificationType.JOB_COMPLETED, "progress_dialog.job_finished"); NOTIFICATION_KEYS.put(NotificationType.JOB_ERROR, "progress_dialog.job_error"); } public GrowlNotifier() { } /** * Puts the given AppleScript bit inside a tell application / end tell block, executes the script * and returns true if it was successfully executed. * * @param appleScript the AppleScript bit to execute * @return true if the script was successfully executed */ private static boolean tellGrowl(String appleScript) { return AppleScript.execute( "tell application \"GrowlHelperApp\"\n" + "\t"+appleScript+"\n" + "end tell", null); } @Override public String getPrettyName() { return "Growl"; } @Override public boolean setEnabled(boolean enabled) { if (!enabled) { return (isEnabled = false); } // No need to bother if the OS is not Mac OS X if (!OsFamily.MAC_OS_X.isCurrent()) { return false; } // Nothing else to do if the application has already been registered if (isRegistered) { return (isEnabled = true); } // Test if Growl is currently running and abort if it is not StringBuilder outputBuffer = new StringBuilder(); if (!(AppleScript.execute(IS_GROWL_RUNNING_APPLESCRIPT, outputBuffer) && outputBuffer.toString().equals("true"))) { LOGGER.debug("Growl is not running, aborting"); return false; } // Register the application (muCommander) with Growl // The list of notification types muCommander uses String notificationTypes = "{"+ "\""+Translator.get(NOTIFICATION_KEYS.get(NotificationType.JOB_COMPLETED))+"\","+ "\""+Translator.get(NOTIFICATION_KEYS.get(NotificationType.JOB_ERROR))+"\""+ "}"; // Register muCommander with Growl, declare the notifications types and enable all of them by default isRegistered = tellGrowl( "register as application \""+APP_NAME+"\""+ " all notifications "+notificationTypes+ " default notifications "+notificationTypes+ " icon of application \""+APP_NAME+"\""); LOGGER.info(isRegistered ? "Successfully registered "+APP_NAME+" with Growl": "Error while registering "+APP_NAME+" with Growl"); return isEnabled = isRegistered; } public static boolean isGrowlRunning() { StringBuilder outputBuffer = new StringBuilder(); return AppleScript.execute(IS_GROWL_RUNNING_APPLESCRIPT, outputBuffer) && outputBuffer.toString().equals("true"); } @Override public boolean isEnabled() { return isEnabled; } @Override public boolean displayNotification(NotificationType notificationType, String title, String description) { LOGGER.debug("notificationType="+notificationType+" title="+title+" description="+description); if (!isEnabled()) { LOGGER.debug("Ignoring notification, this notifier is not enabled"); return false; } boolean success = tellGrowl( "notify with"+ " name \""+Translator.get(NOTIFICATION_KEYS.get(notificationType))+"\""+ " title \""+title+"\""+ " description \""+description+"\""+ " application name \""+APP_NAME+"\""); LOGGER.debug(success? "Notification sent successfully": "Error while sending notification"); return success; } // The following commented methods are implemented using the Growl Java library that comes with the Growl SDK. This // library relies on the Cocoa-Java library which has been deprecated by Apple, which is why we're not using it anymore. // The code has been kept for the record, in case the Growl Java library is ever used again. // public boolean setEnabled(boolean enabled) { // if(enabled) { // // No need to bother if the OS is not Mac OS X // if(PlatformManager.getOsFamily()!=PlatformManager.MAC_OS_X) // return false; // // // If Growl notifier has already been initialized // if(growl!=null) { // return (isEnabled = true); // } // // try { // // Register the application (muCommander) and its icon. Growl doesn't seem to be able to retrieve the // // application's icon by itself, so we have to use some Cocoa magic to get it and feed it to Growl. // growl = new Growl("muCommander", com.apple.cocoa.application.NSApplication.sharedApplication().applicationIconImage().TIFFRepresentation()); // // String notificationTypes[] = new String[]{ // Translator.get(NOTIFICATION_KEYS[NOTIFICATION_TYPE_JOB_COMPLETED]), // Translator.get(NOTIFICATION_KEYS[NOTIFICATION_TYPE_JOB_ERROR]) // }; // // // Declare a list of available notification types // growl.setAllowedNotifications(notificationTypes); // // Declare a list of notification types enabled by default // growl.setDefaultNotifications(notificationTypes); // // // Commit everything // growl.register(); // // AppLogger.fine("Application registered OK"); // // return (isEnabled = true); // } // catch(Exception e) { // AppLogger.fine("Exception thrown while initializing Growl support (Growl not running?)", e); // } // catch(Error e) { // AppLogger.fine("Error while initializing Growl support (cocoa-java not available?)", e); // } // // growl = null; // return (isEnabled = false); // } // else { // return (isEnabled = false); // } // } // // public boolean isEnabled() { // return growl!=null && isEnabled; // } // // public boolean displayNotification(int notificationType, String title, String description) { // AppLogger.finer("notificationType="+notificationType+" title="+title+" description="+description); // // if(!isEnabled()) { // AppLogger.fine("Ignoring notification, this notifier is not enabled"); // // return false; // } // // try { // growl.notifyGrowlOf(Translator.get(NOTIFICATION_KEYS[notificationType]), title, description); // AppLogger.finer("Notification sent OK"); // // return true; // } // catch(Exception e) { // AppLogger.fine("Exception thrown while sending notification", e); // // return false; // } // } } ================================================ FILE: src/main/java/com/mucommander/ui/notifier/NotificationType.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.notifier; /** * Defines the different kinds of notification. * * @author Maxence Bernard */ public enum NotificationType { /** Used to indicate that a job has finished */ JOB_COMPLETED, /** Used to indicate that a job has failed */ JOB_ERROR } ================================================ FILE: src/main/java/com/mucommander/ui/notifier/SystemTrayNotifier.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.notifier; import java.awt.Dimension; import java.awt.Image; import java.awt.Menu; import java.awt.MenuItem; import java.awt.PopupMenu; import java.awt.SystemTray; import java.awt.TrayIcon; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.util.Hashtable; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.ui.action.AwtActionProxy; import com.mucommander.ui.action.ActionManager; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.BringAllToFrontAction; import com.mucommander.ui.action.impl.NewWindowAction; import com.mucommander.ui.action.impl.QuitAction; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.WindowManager; /** * SystemTrayNotifier implements a notifier that uses the System Tray to display notifications. When enabled, this * notifier displays an icon in the system tray that recalls the current {@link com.mucommander.ui.main.MainFrame} * when double-clicked, or shows a popup menu with additional actions ('Bring all to front', 'Quit') when right-clicked. * *

    This notifier is available only with Java 1.6 and up. * * @author Maxence Bernard */ public class SystemTrayNotifier extends AbstractNotifier implements ActionListener { private static final Logger LOGGER = LoggerFactory.getLogger(SystemTrayNotifier.class); /** TrayIcon being displayed in the system tray, null when this notifier is not enabled */ private TrayIcon trayIcon; /** Is this notifier enabled ? */ private boolean isEnabled; /** Name of the tray icon image */ private final static String TRAY_ICON_NAME = "icon16_8.png"; /** Width of the muCommander tray icon */ private final static int TRAY_ICON_WIDTH = 16; /** Height of the muCommander tray icon */ private final static int TRAY_ICON_HEIGHT = 16; /** System tray message types for the different notification types */ private final static Map MESSAGE_TYPES; static { MESSAGE_TYPES = new Hashtable<>(); MESSAGE_TYPES.put(NotificationType.JOB_COMPLETED, TrayIcon.MessageType.INFO); MESSAGE_TYPES.put(NotificationType.JOB_ERROR, TrayIcon.MessageType.ERROR); } SystemTrayNotifier() { } /** * Creates and adds a menu item that triggers the MuAction denoted by the given Class. The menu item's label * is set to the value returned by {@link TcAction#getLabel()}. */ private void addMenuItem(Menu menu, String muActionId) { TcAction action = ActionManager.getActionInstance(muActionId, WindowManager.getCurrentMainFrame()); MenuItem menuItem = new MenuItem(action.getLabel()); menuItem.addActionListener(new AwtActionProxy(action)); menu.add(menuItem); } @Override public boolean setEnabled(boolean enabled) { if (enabled) { // No need to bother if the current Java runtime version is not 1.6 or up, or if SystemTray is not available if (!SystemTray.isSupported()) { return false; } // If System Tray has already been initialized if (trayIcon != null) { return (isEnabled = true); } SystemTray systemTray = SystemTray.getSystemTray(); // create the tray icon and disable image auto-size which shouldn't be used anyway but just in case trayIcon = new TrayIcon(createIconImage(systemTray.getTrayIconSize())); trayIcon.setImageAutoSize(false); trayIcon.setPopupMenu(createPopupMenu()); // Add the tray icon to the system tray. If an exception is caught, clean things up and leave this notifier // disabled. try { systemTray.add(trayIcon); // Tray icon was added OK, listen to action events trayIcon.addActionListener(this); return (isEnabled = true); } catch(java.awt.AWTException e) { trayIcon = null; return (isEnabled = false); } } else { if (trayIcon != null) { // Remove tray icon from the system tray SystemTray.getSystemTray().remove(trayIcon); trayIcon.removeActionListener(this); trayIcon = null; } return (isEnabled = false); } } private Image createIconImage(Dimension trayIconSize) { Image iconImage = IconManager.getIcon(IconManager.IconSet.TROLCOMMANDER, TRAY_ICON_NAME).getImage(); // If the system tray icon size is larger than the icon size, center the icon as the default is to display // the icon in the top left corner which is plain ugly if (trayIconSize.width > TRAY_ICON_WIDTH || trayIconSize.height > TRAY_ICON_HEIGHT) { // The buffered image uses ARGB for transparency BufferedImage bi = new BufferedImage(trayIconSize.width, trayIconSize.height, BufferedImage.TYPE_INT_ARGB); bi.getGraphics().drawImage(iconImage, (trayIconSize.width-TRAY_ICON_WIDTH)/2, (trayIconSize.height-TRAY_ICON_HEIGHT)/2, null); iconImage = bi; } return iconImage; } @NotNull private PopupMenu createPopupMenu() { // create the popup (AWT!) menu. Note there is no way with java.awt.Menu to know when the menu is selected // and thus it makes it hard to have contextual menu items such as the list of open windows. PopupMenu menu = new PopupMenu(); addMenuItem(menu, NewWindowAction.Descriptor.ACTION_ID); addMenuItem(menu, BringAllToFrontAction.Descriptor.ACTION_ID); menu.addSeparator(); addMenuItem(menu, QuitAction.Descriptor.ACTION_ID); return menu; } @Override public boolean isEnabled() { return trayIcon != null && isEnabled; } @Override public boolean displayNotification(NotificationType notificationType, String title, String description) { LOGGER.debug("notificationType={} title={} description={}", notificationType, title, description); if (!isEnabled()) { LOGGER.debug("Ignoring notification, this notifier is not enabled"); return false; } trayIcon.displayMessage(title, description, MESSAGE_TYPES.get(notificationType)); return true; } @Override public String getPrettyName() { return "System Tray"; } @Override public void actionPerformed(ActionEvent actionEvent) { LOGGER.trace("caught SystemTray ActionEvent"); WindowManager.getCurrentMainFrame().toFront(); } } ================================================ FILE: src/main/java/com/mucommander/ui/notifier/package.html ================================================ API for system-independent notification. ================================================ FILE: src/main/java/com/mucommander/ui/popup/TcActionsPopupMenu.kt ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.popup import com.mucommander.ui.action.ActionManager import com.mucommander.ui.helper.MenuToolkit import com.mucommander.ui.main.MainFrame import ru.trolsoft.ui.TMenuSeparator import javax.swing.Action import javax.swing.JMenuItem import javax.swing.JPopupMenu /** * Abstract class for popup menus that display MuActions. * * @author Maxence Bernard, Nicolas Rinaudo, Arik Hadas */ abstract class TcActionsPopupMenu( private val mainFrame: MainFrame ) : JPopupMenu() { /** * Adds the MuAction denoted by the given ID to this popup menu, as a `JMenuItem`. * @param actionId action ID */ protected fun addAction(actionId: String?): JMenuItem { return add(ActionManager.getActionInstance(actionId, mainFrame)) } override fun add(a: Action?): JMenuItem { val item = super.add(a) MenuToolkit.configureActionMenuItem(item) return item } override fun addSeparator() { add(TMenuSeparator()) } } ================================================ FILE: src/main/java/com/mucommander/ui/progress/ProgressTextField.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.progress; import javax.swing.*; import java.awt.*; /** * A text fields which can display progress information, a la Mac OS X Safari's location bar, filling a portion of the * text field's background with a specified color. * * @author Maxence Bernard */ public class ProgressTextField extends JTextField { /** Progress value, between 0 and 100 */ private int progressValue; /** Background color used to symbolize progress */ private Color progressColor; /** * Creates a new ProgressTextField, using the given initial progress value * and progress color which will be used as background color to show progress. * * @param initialProgressValue initial progress value, between 0 and 100 * @param progressColor background color used to symbolize progress */ public ProgressTextField(int initialProgressValue, Color progressColor) { this.progressValue = initialProgressValue; this.progressColor = progressColor; } /** * Sets current progress value and repaints this component. * * @param value current progress value, between 0 and 100. */ public void setProgressValue(int value) { this.progressValue = value; repaint(); } /** * Returns current progress value, as displayed on the component. * * @return current progress value */ public int getProgressValue() { return progressValue; } /** * Sets the color used to represent progress. * @param color new progress color. */ public void setProgressColor(Color color) { if (color != null && progressColor != null && !color.equals(progressColor)) { progressColor = color; repaint(); } } /** * Override JTextField's paint method to show progress information. */ @Override public void paint(Graphics g) { super.paint(g); if (progressValue > 0) { g.setColor(progressColor); g.fillRect(0, 0, (int)(getWidth()*progressValue/(float)100), getHeight()); } } } ================================================ FILE: src/main/java/com/mucommander/ui/progress/package.html ================================================ Various task progress related components. ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/QuickList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.Point; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.util.ArrayList; import java.util.List; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.border.LineBorder; import com.mucommander.ui.quicklist.item.QuickListHeaderItem; import com.mucommander.utils.text.Translator; /** * This abstract class contains some common features to all file table's popups: * 1. add HeaderMenuItem as the first item. * 2. set custom line border. * 3. does the calculations needed in order to show the popup in the center of the * invoker FolderPanel. * * @author Arik Hadas */ public abstract class QuickList extends JPopupMenu implements FocusListener { private static final int PADDING = 2; private QuickListHeaderItem headerMenuItem; private final List items = new ArrayList<>(); private final QuickListContainer container; QuickList(QuickListContainer container, String header) { super(); this.container = container; setBorder(new PopupsBorder()); add(headerMenuItem = new QuickListHeaderItem(header)); setFocusTraversalKeysEnabled(false); } Component nextFocusableComponent() { return container.nextFocusableComponent(); } /** * This function is called before showing quick-list. * If the return value is true, the quick list will be shown. Otherwise, it won't be shown. */ protected abstract boolean prepareForShowing(QuickListContainer container); @Override @SuppressWarnings("deprecated") public void show() { // public void setVisible(boolean visible) { // if (!visible) { // super.setVisible(false); // return; // } if (prepareForShowing(container)) { // Note: the actual popup menu's size is not known at this stage so we use the component's preferred size Dimension dim = getPreferredSize(); Point location = container.calcQuickListPosition(dim); show(container.containerComponent(), location.x, location.y); getFocus(); } } @Override public Component add(Component comp) { items.add(comp); return super.add(comp); } @Override public JMenuItem add(JMenuItem comp) { items.add(comp); return super.add(comp); } @Override public Dimension getPreferredSize() { double width = PADDING, height = PADDING; for (Component item : items) { width = Math.max(width, item.getPreferredSize().getWidth()); height += item.getPreferredSize().getHeight(); } width = Math.ceil(Math.max(container == null ? 0 : container.getWidth() / 2.0, width * 1.05)); height = Math.ceil(height); return new Dimension((int)width, (int)height); } static protected String i18n(String key, String ...paramValues) { return Translator.get(key, paramValues); } @Override public void focusGained(FocusEvent arg0) {} @Override public void focusLost(FocusEvent arg0) { setVisible(false); } /** * Get focus for the desired subcomponent. Only subclasses know which * component to focus, so they must implement it. */ protected abstract void getFocus(); public static class PopupsBorder extends LineBorder { public PopupsBorder() { super(Color.gray); } @Override public Insets getBorderInsets(Component c) { return new Insets(1,1,1,1); } @Override public Insets getBorderInsets(Component c, Insets i) { return new Insets(1,1,1,1); } } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/QuickListContainer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; /** * * @author Arik Hadas */ public interface QuickListContainer { Point calcQuickListPosition(Dimension dim); Component containerComponent(); Component nextFocusableComponent(); int getWidth(); } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/QuickListWithDataList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist; import com.mucommander.ui.main.FolderPanel; import com.mucommander.ui.quicklist.item.QuickListDataList; import com.mucommander.ui.quicklist.item.QuickListDataModel; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import java.awt.Dimension; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * FileTablePopupWithDataList is a FileTablePopup which contains FileTablePopupDataList. * * @author Arik Hadas */ public abstract class QuickListWithDataList extends QuickList implements KeyListener { protected QuickListDataList dataList; private QuickListWithEmptyMsg emptyPopup; private boolean supportDeleteItem; public QuickListWithDataList(QuickListContainer container, String header, String emptyPopupHeader) { super(container, header); // get the TablePopupDataList. dataList = getList(); // add JScrollPane that contains the TablePopupDataList to the popup. JScrollPane scroll = new JScrollPane(dataList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER) { @Override public Dimension getPreferredSize() { // default we have quicklist height for 10 lines maximum // calculate new height to better filling if (container instanceof FolderPanel folderPanel) { Dimension parentSize = folderPanel.getPanel().getSize(); Dimension preferredSize = dataList.getPreferredSize(); return new Dimension(super.getPreferredSize().width, Math.min(preferredSize.height, parentSize.height * 8 / 10)); } return super.getPreferredSize(); } }; scroll.setBorder(null); scroll.getVerticalScrollBar().setFocusable(false); scroll.getHorizontalScrollBar().setFocusable(false); add(scroll); dataList.addFocusListener(this); // create TablePopupWithEmptyMsg that will be shown instead of this popup, if this // popup's data list won't have any elements. emptyPopup = new QuickListWithEmptyMsg(container, header, emptyPopupHeader); dataList.addKeyListener(this); } protected abstract T[] getData(); /** * This function will be called when an element from the data list will be selected. * * @param item - The selected item from the data list. */ public void itemSelected(T item) { setVisible(false); acceptListItem(item); } @Override protected boolean prepareForShowing(QuickListContainer container) { boolean toShow = false; // if data list contains at least 1 element, show this popup. T[] data = getData(); if (data.length > 0) { dataList.setListData(data); toShow = true; } else { // else, show popup with a "no elements" message. emptyPopup.show(); } return toShow; } @Override protected void getFocus() { // to overcome #552 (right recentQL not focused) both must be used: // invokeLater and requestFocus (requestFocusInWindow is not sufficient) SwingUtilities.invokeLater(dataList::requestFocus); } /** * This function defines what should be done with a selected item from the data list. * * @param item - The selected item from the data list. */ protected abstract void acceptListItem(T item); protected abstract QuickListDataList getList(); protected void deleteListItem(int index, T item) { if (!supportDeleteItem) { return; } onDeleteItem(index, item); QuickListDataModel model = (QuickListDataModel)dataList.getModel(); model.remove(index); int selectedIndex = index; if (selectedIndex >= model.getSize()) { selectedIndex--; } dataList.setSelectedIndex(selectedIndex); dataList.repaint(); } protected void onDeleteItem(int index, T item) { } @Override public void keyTyped(KeyEvent e) { } @Override public void keyPressed(KeyEvent e) { if (dataList == null) { return; } int selectedIndex = dataList.getSelectedIndex(); int lastIndex = dataList.getModel().getSize() - 1; if (e.getExtendedKeyCode() == KeyEvent.VK_UP && selectedIndex == 0) { dataList.setSelectedIndex(lastIndex); dataList.ensureIndexIsVisible(lastIndex); e.consume(); } else if (e.getExtendedKeyCode() == KeyEvent.VK_DOWN && dataList.getSelectedIndex() == lastIndex) { dataList.setSelectedIndex(0); dataList.ensureIndexIsVisible(0); e.consume(); } } @Override public void keyReleased(KeyEvent e) { if (supportDeleteItem && e.getKeyCode() == KeyEvent.VK_DELETE) { int index = dataList.getSelectedIndex(); if (index >= 0 && dataList.getModel().getSize() > 0) { deleteListItem(index, dataList.getSelectedValue()); } } } protected boolean isSupportDeleteItem() { return supportDeleteItem; } protected void setSupportDeleteItem(boolean supportDeleteItem) { this.supportDeleteItem = supportDeleteItem; } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/QuickListWithEmptyMsg.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.SwingUtilities; import com.mucommander.ui.quicklist.item.QuickListEmptyMessageItem; /** * FileTablePopupWithEmptyMsg is a FileTablePopup which contains EmptyMessageItem. * * @author Arik Hadas */ class QuickListWithEmptyMsg extends QuickList { protected QuickListEmptyMessageItem emptyMenuItem; public QuickListWithEmptyMsg(QuickListContainer container, String header, String emptyPopupHeader) { super(container, header); add(emptyMenuItem = new QuickListEmptyMessageItem(emptyPopupHeader)); addKeyListenerToList(); addFocusListener(this); } @Override protected boolean prepareForShowing(QuickListContainer container) { getFocus(); return true; } @Override public void getFocus(){ SwingUtilities.invokeLater(this::requestFocus); } private void addKeyListenerToList() { addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { switch(e.getKeyCode()) { default: nextFocusableComponent().requestFocus(); break; } } }); } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/QuickListWithIcons.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist; import java.awt.Dimension; import java.awt.Image; import java.util.HashMap; import java.util.Map; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.icon.CustomFileIconProvider; import com.mucommander.ui.icon.FileIcons; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.quicklist.item.QuickListDataList; import com.mucommander.ui.quicklist.item.QuickListDataListWithIcons; /** * FileTablePopupWithIcons is a FileTablePopupWithDataList in which the data list * contains icons. * * @author Arik Hadas */ public abstract class QuickListWithIcons extends QuickListWithDataList { /** * This Map's keys are items and its objects are the corresponding icon. */ private final Map itemToIconCacheMap = new HashMap<>(); /** * This SpinningDial will appear until the icon fetching of an item is over. */ private static final SpinningDial WAITING_ICON = new SpinningDial(); /** * If the icon fetching fails for some item, the following icon will appear for it. */ private static final Icon NOT_AVAILABLE_ICON = IconManager.getIcon(IconManager.IconSet.FILE, CustomFileIconProvider.NOT_ACCESSIBLE_FILE); /** * Saves the number of waiting-icons (SpinningDials) appearing in the list. */ private int numOfWaitingIconInList; public QuickListWithIcons(QuickListContainer container, String header, String emptyPopupHeader) { super(container, header, emptyPopupHeader); numOfWaitingIconInList = 0; addPopupMenuListener(new PopupMenuListener() { public void popupMenuCanceled(PopupMenuEvent e) {} public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { onHide(); } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { onShow(); } }); } protected void onShow() { // Clear icon-caching before opening popup-list in order to let the icons be fetched again. itemToIconCacheMap.clear(); } protected void onHide() {} /** * Called when waitingIcon is added to the list. */ private synchronized void waitingIconAddedToList() { // If there was no other waitingIcon in the list before current addition - start the spinning dial. if (numOfWaitingIconInList++ == 0) { WAITING_ICON.setAnimated(true); } } /** * Called when waitingIcon is removed from the list. */ private synchronized void waitingIconRemovedFromList() { // If after current remove operation, there will be no waitingIcon in the list - stop the spinning dial. if (--numOfWaitingIconInList == 0) { WAITING_ICON.setAnimated(false); } } @Override protected QuickListDataList getList() { return new QuickListDataListWithIcons(nextFocusableComponent()) { @Override public Icon getImageIconOfItem(T item, final Dimension preferredSize) { return getImageIconOfItemImp(item, preferredSize); } }; } /** * This function gets an item from the data list and return its icon. * * @param item a list item * @return an icon for the specified item */ protected abstract Icon itemToIcon(T item); /** * This function return an icon for the specified file. * * @param file the file for which to return an icon * @return the specified file's icon. null is returned if the file does not exist */ protected Icon getIconOfFile(AbstractFile file) { return (file != null && file.exists()) ? IconManager.getImageIcon(FileIcons.getFileIcon(file)) : null; } protected Icon getImageIconOfItemImp(final T item, final Dimension preferredSize) { boolean found; synchronized(itemToIconCacheMap) { if (!(found = itemToIconCacheMap.containsKey(item))) { itemToIconCacheMap.put(item, WAITING_ICON); waitingIconAddedToList(); } } Icon result = itemToIconCacheMap.get(item); if (!found) new Thread(() -> { Icon icon = itemToIcon(item); // If the item does not exist or is not accessible, show notAvailableIcon for it. itemToIconCacheMap.put(item, icon != null ? icon : NOT_AVAILABLE_ICON); waitingIconRemovedFromList(); repaint(); }).start(); return resizeIcon(result, preferredSize); } private Icon resizeIcon(Icon icon, final Dimension preferredSize) { if (icon instanceof ImageIcon) { Image image = ((ImageIcon) icon).getImage(); final double height = preferredSize.getHeight(); final double width = (height / icon.getIconHeight()) * icon.getIconWidth(); Image scaledImage = image.getScaledInstance((int)width, (int)height, Image.SCALE_SMOOTH); return new ImageIcon(scaledImage); } return icon; } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/QuickListWithoutIcons.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist; import com.mucommander.ui.quicklist.item.QuickListDataList; /** * FileTablePopupWithoutIcons is a FileTablePopupWithDataList in which the data list * doesn't contain icons. * * @author Arik Hadas */ public abstract class QuickListWithoutIcons extends QuickListWithDataList { public QuickListWithoutIcons(QuickListContainer container, String header, String emptyPopupHeader) { super(container, header, emptyPopupHeader); } @Override protected QuickListDataList getList() { return new QuickListDataList<>(nextFocusableComponent()); } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/item/QuickListDataList.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist.item; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.swing.DefaultListCellRenderer; import javax.swing.JList; import javax.swing.text.Position; import lombok.Getter; import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.ui.icon.IconManager; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.main.table.CellLabel; import com.mucommander.ui.quicklist.QuickListWithDataList; import com.mucommander.ui.quicksearch.QuickSearch; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.ThemeCache; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeListener; import com.mucommander.ui.theme.ThemeManager; /** * This class represent a data list for FileTablePopupWithDataList. * * @author Arik Hadas */ public class QuickListDataList extends JList { private static final Logger LOGGER = LoggerFactory.getLogger(QuickListDataList.class); private final static int VISIBLE_ROWS_COUNT = 10; @Getter private final QuickSearch quickSearch = new QuickListQuickSearch(); private final Component nextFocusableComponent; public QuickListDataList(Component nextFocusableComponent){ this.nextFocusableComponent = nextFocusableComponent; setFocusTraversalKeysEnabled(false); addMouseListenerToList(); DataListItemRenderer itemRenderer = getItemRenderer(); ThemeManager.addCurrentThemeListener(itemRenderer); setCellRenderer(itemRenderer); } public QuickListDataList(T[] data) { this(new Component() {}); setListData(data); } protected DataListItemRenderer getItemRenderer() { return new DataListItemRenderer(); } /** * This function is called before showing TablePopupWithDataList. * It does the required steps before the popup is shown. */ @Override public void setListData(final T[] data) { //super.setListData(data); setModel(new QuickListDataModel<>(data)); int numOfRowsInList = getModel().getSize(); if (numOfRowsInList > 0) { setVisibleRowCount(Math.min(numOfRowsInList, VISIBLE_ROWS_COUNT)); setSelectedIndex(0); ensureIndexIsVisible(0); } } protected T getListItem(int index) { if (index > getModel().getSize() || index < 0) { return null; } return getModel().getElementAt(index); } protected void addMouseListenerToList() { addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { // If there was double click on item of the popup's list, // select it, and update the text component. if (e.getClickCount() == 2) { int index = locationToIndex(e.getPoint()); setSelectedIndex(index); ((QuickListWithDataList)(getParent().getParent().getParent())).itemSelected(getSelectedValue()); } } }); } public void setForegroundColors(Color foreground, Color selectedForeground) { @SuppressWarnings("unchecked") DataListItemRenderer cellRenderer = (DataListItemRenderer) getCellRenderer(); cellRenderer.setItemForeground(foreground); cellRenderer.setSelectedItemForeground(selectedForeground); } public void setBackgroundColors(Color background, Color selectedBackground) { @SuppressWarnings("unchecked") DataListItemRenderer cellRenderer = (DataListItemRenderer) getCellRenderer(); cellRenderer.setItemBackground(background); cellRenderer.setSelectedItemBackground(selectedBackground); } protected class DataListItemRenderer extends DefaultListCellRenderer implements ThemeListener { @Setter private Color selectedItemBackground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR); @Setter private Color selectedItemForeground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR); @Setter private Color itemBackground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_ITEM_BACKGROUND_COLOR); @Setter private Color itemForeground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_ITEM_FOREGROUND_COLOR); private Font itemFont = ThemeManager.getCurrentFont(ThemeData.QUICK_LIST_ITEM_FONT); DataListItemRenderer() { } @Override public Component getListCellRendererComponent(JList list, Object value, int rowIndex, boolean isSelected, boolean cellHasFocus) { // Let superclass deal with most of it... super.getListCellRendererComponent(list, value, rowIndex, isSelected, cellHasFocus); T item = getListItem(rowIndex); CellLabel label = new CellLabel(); if (item == null) { LOGGER.debug("tableModel.getCachedFileAtRow({}) RETURNED NULL !", rowIndex); return label; } QuickSearch search = QuickListDataList.this.getQuickSearch(); boolean matches = !search.isActive() || search.matches(item.toString()); label.setFont(itemFont); label.setText(item.toString()); //label.setToolTipText(""+item); // Set background color depending on whether the row is selected or not, and whether the table has focus or not if (isSelected) { label.setBackground(selectedItemBackground); label.setForeground(selectedItemForeground); } else { label.setBackground(matches ? itemBackground : ThemeCache.unmatchedBackground); label.setForeground(matches ? itemForeground : ThemeCache.unmatchedForeground); } return label; } @Override public void colorChanged(ColorChangedEvent event) { if (event.getColorId() == ThemeData.QUICK_LIST_ITEM_BACKGROUND_COLOR) { itemBackground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_ITEM_BACKGROUND_COLOR); } else if (event.getColorId() == ThemeData.QUICK_LIST_ITEM_FOREGROUND_COLOR) { itemForeground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_ITEM_FOREGROUND_COLOR); } else if (event.getColorId() == ThemeData.QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR) { selectedItemBackground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR); } else if (event.getColorId() == ThemeData.QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR) { selectedItemForeground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR); } } public void fontChanged(FontChangedEvent event) { itemFont = ThemeManager.getCurrentFont(ThemeData.QUICK_LIST_ITEM_FONT); } } public class QuickListQuickSearch extends QuickSearch { public QuickListQuickSearch() { super(QuickListDataList.this); } @Override protected void searchStarted() { } @Override protected void searchStopped() { WindowManager.getCurrentMainFrame().getStatusBar().updateSelectedFilesInfo(); QuickListDataList.this.repaint(); } @Override protected int getNumOfItems() { return QuickListDataList.this.getModel().getSize(); } @Override protected String getItemString(int index) { return getListItem(index).toString(); } @Override protected void searchStringBecameEmpty(String searchString) { WindowManager.getCurrentMainFrame().getStatusBar().setStatusInfo(searchString); // TODO: is needed? } @Override protected void matchFound(int row, String searchString, boolean itsBestMatch) { if (row != getSelectedIndex()) { setSelectedIndex(row); ensureIndexIsVisible(row); } // Display the new search string in the status bar // that indicates that the search has yielded a match WindowManager.getCurrentMainFrame().getStatusBar().setStatusInfo(searchString, IconManager.getIcon(IconManager.IconSet.STATUS_BAR, QUICK_SEARCH_OK_ICON), false); } @Override protected void matchNotFound(String searchString) { // No file matching the search string, display the new search string with an icon // that indicates that the search has failed WindowManager.getCurrentMainFrame().getStatusBar().setStatusInfo(searchString, IconManager.getIcon(IconManager.IconSet.STATUS_BAR, QUICK_SEARCH_KO_ICON), false); } @Override public synchronized void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); // If quick search is not active... if (!isActive()) { // Return (do not start quick search) if the key is not a valid quick search input if (!isValidQuickSearchInput(e)) { if (keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_ENTER) { tryToTransferFocusToTheNextComponent(); } if (keyCode == KeyEvent.VK_ENTER) { Container c = getParent().getParent().getParent(); ((QuickListWithDataList)(c)).itemSelected(getSelectedValue()); } return; } // Start the quick search and continue to process the current key event start(); } // At this point, quick search is active boolean keyHasModifiers = (e.getModifiersEx()&(KeyEvent.SHIFT_DOWN_MASK|KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.META_DOWN_MASK))!=0; // Backspace removes the last character of the search string if (keyCode == KeyEvent.VK_BACK_SPACE && !keyHasModifiers) { // Search string is empty already if (isSearchStringEmpty()) { return; } removeLastCharacterFromSearchString(); // Find the row that best matches the new search string and select it findMatch(0, true, true); } // Escape immediately cancels the quick search else if (keyCode == KeyEvent.VK_ESCAPE && !keyHasModifiers) { stop(); } // Up/Down jumps to previous/next match // Shift+Up/Shift+Down marks currently selected file and jumps to previous/next match else if ((keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_DOWN) && !keyHasModifiers) { // Find the first row before/after the current row that matches the search string boolean down = keyCode == KeyEvent.VK_DOWN; findMatch(getSelectedIndex() + (down ? 1 : -1), down, false); } // If no modifier other than Shift is pressed and the typed character is not a control character (space is ok) // and a valid Unicode character, add it to the current search string else if (isValidQuickSearchInput(e)) { appendCharacterToSearchString(e.getKeyChar()); // Find the row that best matches the new search string and select it findMatch(0, true, true); } else { switch(e.getKeyCode()) { case KeyEvent.VK_UP: { int numOfItems = getModel().getSize(); if (numOfItems > 0 && getSelectedIndex() == 0) { setSelectedIndex(numOfItems - 1); ensureIndexIsVisible(numOfItems - 1); e.consume(); } } break; case KeyEvent.VK_DOWN: { int numOfItems = getModel().getSize(); if (numOfItems > 0 && getSelectedIndex() == numOfItems - 1) { setSelectedIndex(0); ensureIndexIsVisible(0); e.consume(); } } break; case KeyEvent.VK_ENTER: tryToTransferFocusToTheNextComponent(); ((QuickListWithDataList)(getParent().getParent().getParent())).itemSelected(getSelectedValue()); stop(); break; case KeyEvent.VK_TAB: tryToTransferFocusToTheNextComponent(); stop(); break; } // Do not update last search string's change timestamp return; } // Update last search string's change timestamp setLastSearchStringChange(System.currentTimeMillis()); e.consume(); } private void tryToTransferFocusToTheNextComponent() { if (nextFocusableComponent != null) nextFocusableComponent.requestFocus(); } } @Override public int getNextMatch(String prefix, int startIndex, Position.Bias bias) { // This method is overridden to prevent the swing native search return -1; } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/item/QuickListDataListWithIcons.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist.item; import java.awt.Component; import java.awt.Dimension; import javax.swing.Icon; import javax.swing.JList; import com.mucommander.ui.main.table.CellLabel; /** * * @author Arik Hadas */ public abstract class QuickListDataListWithIcons extends QuickListDataList { public QuickListDataListWithIcons(Component nextFocusableComponent) { super(nextFocusableComponent); } public QuickListDataListWithIcons(T[] data) { super(data); } @Override protected DataListItemRenderer getItemRenderer() { return new DataListItemWithIconRenderer(); } ////////////////////// // Abstract methods // ////////////////////// public abstract Icon getImageIconOfItem(final T item, final Dimension preferredSize); protected class DataListItemWithIconRenderer extends DataListItemRenderer { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { // Let superclass deal with most of it... CellLabel label = (CellLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); // Add its icon T item = getListItem(index); Icon icon = getImageIconOfItem(item, this.getPreferredSize()); label.setIcon(icon); return label; } } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/item/QuickListDataModel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist.item; import javax.swing.AbstractListModel; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * @author Oleg Trifonov * Created on 17/10/14. */ public class QuickListDataModel extends AbstractListModel { private List data = new ArrayList<>(); public QuickListDataModel(T[] data) { super(); Collections.addAll(this.data, data); } @Override public int getSize() { return data.size(); } @Override public T getElementAt(int index) { return data.get(index); } public void remove(int index) { data.remove(index); } public void remove(T item) { data.remove(item); } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/item/QuickListEmptyMessageItem.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist.item; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeManager; import java.awt.*; /** * This class represent an item that will be shown in a QuickList which doesn't * contain any elements, with a relevant message. * * @author Arik Hadas */ public class QuickListEmptyMessageItem extends QuickListItem { protected Color foreground; protected Color background; public QuickListEmptyMessageItem(String text) { super(text); foreground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_ITEM_FOREGROUND_COLOR); background = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_ITEM_BACKGROUND_COLOR); setFont(ThemeManager.getCurrentFont(ThemeData.QUICK_LIST_ITEM_FONT)); } @Override protected final void paintComponent(Graphics g) { Graphics2D graphics = (Graphics2D) g; // paint the background of this item with lightGray color graphics.setColor(background); graphics.fillRect(0, 0, getWidth(), getHeight()); // draw message: graphics.setFont(mFont); graphics.setColor(foreground); graphics.drawString(getText(), X_AXIS_OFFSET, (int) graphics.getFontMetrics().getLineMetrics(this.getText(), graphics).getHeight()); } @Override public void colorChanged(ColorChangedEvent event) { if (event.getColorId() == ThemeData.QUICK_LIST_ITEM_BACKGROUND_COLOR) background = event.getColor(); else if (event.getColorId() == ThemeData.QUICK_LIST_ITEM_FOREGROUND_COLOR) foreground = event.getColor(); } @Override public void fontChanged(FontChangedEvent event) { setFont(ThemeManager.getCurrentFont(ThemeData.QUICK_LIST_ITEM_FONT)); } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/item/QuickListHeaderItem.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist.item; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.ThemeData; import com.mucommander.ui.theme.ThemeManager; import java.awt.*; import java.awt.image.BufferedImage; /** * HeaderMenuItem is a custom MenuItem that shown as the first item in every QuickList. * * @author Arik Hadas */ public class QuickListHeaderItem extends QuickListItem { protected Color foreground; protected Color background; protected Color secondaryBackground; public QuickListHeaderItem(String text) { super(text); foreground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_HEADER_FOREGROUND_COLOR); background = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_HEADER_BACKGROUND_COLOR); secondaryBackground = ThemeManager.getCurrentColor(ThemeData.QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR); setFont(ThemeManager.getCurrentFont(ThemeData.QUICK_LIST_HEADER_FONT)); } @Override protected final void paintComponent(Graphics g) { Graphics2D graphics = (Graphics2D) g; graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); // paint background image graphics.drawImage(getBackgroundImage(getWidth(), getHeight(), graphics, background, secondaryBackground), 0, 0, null); // draw text: graphics.setFont(mFont); graphics.setColor(foreground); graphics.drawString(getText(), X_AXIS_OFFSET, (int) graphics.getFontMetrics().getLineMetrics(this.getText(), graphics).getHeight()); } private BufferedImage getBackgroundImage(int width, int height, Graphics2D graphics, Color leftColor, Color rightColor) { //clear previous painting: graphics.setColor(Color.white); graphics.fillRect(0, 0, width, height); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // paint transition color GradientPaint gradient = new GradientPaint(0, 0, leftColor, width, 0, rightColor); graphics.setPaint(gradient); graphics.fillRect(0, 0, width, height); return image; } public void setForegroundColor(Color foreground) { this.foreground = foreground; repaint(); } public void setBackgroundColors(Color background, Color secondaryBackground) { this.background = background; this.secondaryBackground = secondaryBackground; repaint(); } @Override public void colorChanged(ColorChangedEvent event) { switch (event.getColorId()) { case ThemeData.QUICK_LIST_HEADER_BACKGROUND_COLOR: background = event.getColor(); break; case ThemeData.QUICK_LIST_HEADER_FOREGROUND_COLOR: foreground = event.getColor(); break; case ThemeData.QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR: secondaryBackground = event.getColor(); break; } } @Override public void fontChanged(FontChangedEvent event) { setFont(ThemeManager.getCurrentFont(ThemeData.QUICK_LIST_HEADER_FONT)); } } ================================================ FILE: src/main/java/com/mucommander/ui/quicklist/item/QuickListItem.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicklist.item; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.ThemeListener; import com.mucommander.ui.theme.ThemeManager; import javax.swing.*; import java.awt.*; /** * This abstract class represent menu item of QuickList. * * @author Arik Hadas */ abstract class QuickListItem extends JMenuItem implements ThemeListener { protected static final int X_AXIS_OFFSET = 5; protected Font mFont; protected Dimension dimension; public QuickListItem(String text) { super(text); setEnabled(false); ThemeManager.addCurrentThemeListener(this); } @Override public void setFont(Font font) { mFont = font; dimension = new Dimension((int) Math.ceil(getFontMetrics(font).stringWidth(getText()) * 1.1), (int) (font.getSize() * 1.5)); setPreferredSize(dimension); setSize(dimension); } /** * This function returns the item's dimension which is based on the item's font. */ @Override public Dimension getPreferredSize() { return dimension; } ///////////////////////////// /// ThemeListener methods /// ///////////////////////////// abstract public void colorChanged(ColorChangedEvent event); abstract public void fontChanged(FontChangedEvent event); } ================================================ FILE: src/main/java/com/mucommander/ui/quicksearch/QuickSearch.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.quicksearch; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.JComponent; import com.mucommander.commons.file.AbstractFile; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class contains 'quick search' common functionality - selection of rows that match * the user's keyboard input. * This class is abstract, and should be inherited by subclasses that define 'quick search' * functionality for specific components. * * @author Arik Hadas */ public abstract class QuickSearch extends KeyAdapter implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(QuickSearch.class); /** Icon that is used to indicate in the status bar that quick search has failed */ protected final static String QUICK_SEARCH_KO_ICON = "quick_search_ko.png"; /** Icon that is used to indicate in the status bar that quick search has found a match */ protected final static String QUICK_SEARCH_OK_ICON = "quick_search_ok.png"; /** Quick search string */ private String searchString; /** Timestamp of the last search string change, used when quick search is active */ private long lastSearchStringChange; /** Thread that's responsible for canceling the quick search on timeout, * has a null value when quick search is not active */ private Thread timeoutThread; /** Quick search timeout in ms. No timeout if <= 0 */ private int quickSearchTimeout; private final JComponent component; private volatile boolean active; protected QuickSearch(JComponent component) { this.component = component; // Listener to key events to start quick search or update search string when it is active component.addKeyListener(this); } /** * Turns on quick search mode. This method has no effect if the quick search is already active. * {@link #isActive() isActive()} will return true after this call, and until the quick search has * timed out or has been cancelled by user. */ protected synchronized void start() { if (!isActive()) { // Reset search string searchString = ""; // Start the thread that's responsible for canceling the quick search on timeout quickSearchTimeout = TcConfigurations.getPreferences().getVariable(TcPreference.QUICK_SEARCH_TIMEOUT, TcPreferences.DEFAULT_QUICK_SEARCH_TIMEOUT); if (quickSearchTimeout > 0) { timeoutThread = new Thread(this, "QuickSearch timeout thread"); timeoutThread.start(); } active = true; lastSearchStringChange = System.currentTimeMillis(); searchStarted(); } } /** * Stops the current quick search. This method has no effect if the quick search is not currently active. */ public synchronized void stop() { if (isActive()) { timeoutThread = null; active = false; searchStopped(); } } /** * Returns true if a quick search is being performed. * * @return true if a quick search is being performed */ public boolean isActive() { return active; } /** * Returns true if the current quick search string matches the given string. * Always returns false when the quick search is inactive. * * @param string the string to test against the quick search string * @return true if the current quick search string matches the given string */ public boolean matches(String string) { return isActive() && string.toLowerCase().contains(searchString.toLowerCase()); } public boolean matches(AbstractFile file) { return matches(file.getName()); } /** * Returns true if the given KeyEvent corresponds to a valid quick search input, * false in any of the following cases: * *

      *
    • has any of the Alt, Ctrl or Meta modifier keys down (Shift is OK)
    • *
    • is an ASCII control character (<32 or ==127)
    • *
    • is not a valid Unicode character
    • *
    * * @param e the KeyEvent to test * @return true if the given KeyEvent corresponds to a valid quick search input */ protected boolean isValidQuickSearchInput(KeyEvent e) { if ((e.getModifiersEx()&(KeyEvent.ALT_DOWN_MASK|KeyEvent.CTRL_DOWN_MASK|KeyEvent.META_DOWN_MASK))!=0) return false; char keyChar = e.getKeyChar(); return keyChar >= 32 && keyChar != 127 && Character.isDefined(keyChar); } /** * Setter for the last search string change time * * @param lastSearchStringChange - the time of the last change made to the search string */ protected void setLastSearchStringChange(long lastSearchStringChange) { this.lastSearchStringChange = lastSearchStringChange; } protected boolean isSearchStringEmpty() { return searchString.isEmpty(); } protected void removeLastCharacterFromSearchString() { // Remove last character from the search string // Since the search string has been updated, match information has changed as well // and we need to repaint the table. // Note that we only repaint if the search string is not empty: if it's empty, // the cancel() method will be called, and repainting twice would result in an // unpleasant graphical artifact. searchString = searchString.substring(0, searchString.length()-1); if (!searchString.isEmpty()) { component.repaint(); } } protected void appendCharacterToSearchString(char keyChar) { // Update search string with the key that has just been typed // Since the search string has been updated, match information has changed as well // and we need to repaint the table. searchString += keyChar; component.repaint(); } /** * Finds a match (if any) for the current quick search string and selects the corresponding row. * * @param startIndex first row to be tested * @param descending specifies whether rows should be tested in ascending or descending order * @param findBestMatch if true, all rows will be tested in the specified order, looking for the best match. If not, it will stop to the first match (not necessarily the best). */ protected void findMatch(int startIndex, boolean descending, boolean findBestMatch) { LOGGER.trace("startRow="+startIndex+" descending="+descending+" findMatch="+findBestMatch); // If search string is empty, update status bar without any icon and return if (searchString.isEmpty()) { searchStringBecameEmpty(searchString); } else { int bestMatch = getBestMatch(startIndex, descending, findBestMatch); if (bestMatch >= 0) { matchFound(bestMatch, searchString, findBestMatch); } else { matchNotFound(searchString); } } } private int getBestMatch(int startIndex, boolean descending, boolean findBestMatch) { String searchStringLC = searchString.toLowerCase(); int searchStringLen = searchString.length(); int startsWithCaseMatch = -1; int startsWithNoCaseMatch = -1; int containsCaseMatch = -1; int containsNoCaseMatch = -1; int nbFiles = getNumOfItems(); // Iterate on rows and look the first strings to match one of the following tests, // in the following order of importance : // - search string matches the beginning of the string with the same case // - search string matches the beginning of the string with a different case // - string contains search string with the same case // - string contains search string with a different case for (int i = startIndex; descending ? i < nbFiles : i >= 0; i = descending ? i+1 : i-1) { // if findBestMatch was not specified, stop to the first match if (!findBestMatch && (startsWithCaseMatch != -1 || startsWithNoCaseMatch != -1 || containsCaseMatch != -1 || containsNoCaseMatch != -1)) { break; } String item = getItemString(i); int itemLen = item.length(); // No need to compare strings if quick search string is longer than compared string, // they won't match if (itemLen < searchStringLen) { continue; } // Compare quick search string against if (item.startsWith(searchString)) { // We've got the best match we could ever have, let's get out of this loop! startsWithCaseMatch = i; break; } // If we already have a match on this test case, let's skip to the next string if (startsWithNoCaseMatch >= 0) { continue; } String itemLC = item.toLowerCase(); if (itemLC.startsWith(searchStringLC)) { // We've got a match, let's see if we can find a better match on the next string startsWithNoCaseMatch = i; } // No need to check if the compared string contains search string if both size are equal, // in the case startsWith test yields the same result if (itemLen == searchStringLen) { continue; } // If we already have a match on this test case, let's skip to the next string if (containsCaseMatch != -1) { continue; } if (item.contains(searchString)) { // We've got a match, let's see if we can find a better match on the next string containsCaseMatch = i; continue; } // If we already have a match on this test case, let's skip to the next string if (containsNoCaseMatch != -1) { continue; } if (itemLC.contains(searchStringLC)) { // We've got a match, let's see if we can find a better match on the next string containsNoCaseMatch = i; //continue; } } // Determines what the best match is, based on all the matches we found int bestMatch = startsWithCaseMatch != -1 ? startsWithCaseMatch : startsWithNoCaseMatch != -1 ? startsWithNoCaseMatch : containsCaseMatch !=-1 ? containsCaseMatch : containsNoCaseMatch; LOGGER.trace("startsWithCaseMatch="+startsWithCaseMatch+" containsCaseMatch="+containsCaseMatch+" startsWithNoCaseMatch="+startsWithNoCaseMatch+" containsNoCaseMatch="+containsNoCaseMatch); LOGGER.trace("bestMatch="+bestMatch); return bestMatch; } /** * Hook that is called after the search is started */ protected abstract void searchStarted(); /** * Hook that is called after the search is stopped */ protected abstract void searchStopped(); /** * Return number of items to be searched in * * @return number of items */ protected abstract int getNumOfItems(); /** * Return item at a given index as String * * @param index - index of item * @return item at index as String */ protected abstract String getItemString(int index); /** * Hook that is called after a search was done for an empty string * * @param searchString - the string that was being searched */ protected abstract void searchStringBecameEmpty(String searchString); /** * Hook that is called after a search was done and an item was found * * @param row - the row of the item that was found * @param searchString - the string that was being searched * @param itsBestMatch - if found best match (true it it's a new search, elsewhere if we navigate between matced files) */ protected abstract void matchFound(int row, String searchString, boolean itsBestMatch); /** * Hood that is called after a search was done and no item was found * * @param searchString - the string that was being searched */ protected abstract void matchNotFound(String searchString); public void run() { do { try { Thread.sleep(100); } catch(InterruptedException ignore) { } synchronized(this) { if (timeoutThread != null && System.currentTimeMillis()-lastSearchStringChange >= quickSearchTimeout) { stop(); } } } while(timeoutThread != null); } @Override public synchronized void keyReleased(KeyEvent e) { // Cancel quick search if backspace key has been pressed and search string is empty. // This check is done on key release, so that if backspace key is maintained pressed to remove all the search // string, it does not trigger the JComponent's back action which is mapped on backspace too if (isActive() && e.getKeyCode() == KeyEvent.VK_BACK_SPACE && searchString.isEmpty()) { e.consume(); stop(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/table/CenteredTableHeaderRenderer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.table; import javax.swing.*; import javax.swing.table.TableCellRenderer; import java.awt.*; /** * A TableCellRenderer which can be used to center the text in table's headers. * * @author Arik Hadas */ public class CenteredTableHeaderRenderer extends JLabel implements TableCellRenderer { public CenteredTableHeaderRenderer() { setHorizontalAlignment(JLabel.CENTER); setBorder(UIManager.getBorder("TableHeader.cellBorder")); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int rowIndex, int vColIndex) { // Configure the component with the specified value setText(value.toString()); return this; } // The following methods override the defaults for performance reasons @Override public void validate() {} @Override public void revalidate() {} @Override protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {} @Override public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {} } ================================================ FILE: src/main/java/com/mucommander/ui/table/EditableHeader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.table; import javax.swing.event.CellEditorListener; import javax.swing.event.ChangeEvent; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellEditor; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import java.awt.*; import java.util.EventObject; public class EditableHeader extends JTableHeader implements CellEditorListener { private final int HEADER_ROW = -10; transient protected int editingColumn; transient protected TableCellEditor cellEditor; transient protected Component editorComp; public EditableHeader(TableColumnModel columnModel) { super(columnModel); setReorderingAllowed(false); cellEditor = null; recreateTableColumn(columnModel); } @Override public void updateUI() { setUI(new EditableHeaderUI()); resizeAndRepaint(); invalidate(); } protected void recreateTableColumn(TableColumnModel columnModel) { int n = columnModel.getColumnCount(); EditableHeaderTableColumn[] newCols = new EditableHeaderTableColumn[n]; TableColumn[] oldCols = new TableColumn[n]; for (int i = 0; i < n; i++) { oldCols[i] = columnModel.getColumn(i); newCols[i] = new EditableHeaderTableColumn(); newCols[i].copyValues(oldCols[i]); } for (int i = 0; i < n; i++) { columnModel.removeColumn(oldCols[i]); } for (int i = 0; i < n; i++) { columnModel.addColumn(newCols[i]); } } public boolean editCellAt(int index, EventObject e) { if (cellEditor != null && !cellEditor.stopCellEditing()) { return false; } /*if (!isCellEditable(index)) { return false; } */ TableCellEditor editor = getCellEditor(index); if (editor != null && editor.isCellEditable(e)) { editorComp = prepareEditor(editor, index); editorComp.setBounds(getHeaderRect(index)); add(editorComp); editorComp.validate(); setCellEditor(editor); setEditingColumn(index); editor.addCellEditorListener(this); return true; } return false; } /*public boolean isCellEditable(int index) { return false; }*/ public TableCellEditor getCellEditor(int index) { int columnIndex = columnModel.getColumn(index).getModelIndex(); EditableHeaderTableColumn col = (EditableHeaderTableColumn) columnModel.getColumn(columnIndex); return col.getHeaderEditor(); } public void setCellEditor(TableCellEditor newEditor) { TableCellEditor oldEditor = cellEditor; cellEditor = newEditor; // firePropertyChange if (oldEditor != null) { oldEditor.removeCellEditorListener(this); } if (newEditor != null) { newEditor.addCellEditorListener(this); } } public Component prepareEditor(TableCellEditor editor, int index) { Object value = columnModel.getColumn(index).getHeaderValue(); return editor.getTableCellEditorComponent(getTable(), value, true, HEADER_ROW, index); } public TableCellEditor getCellEditor() { return cellEditor; } public Component getEditorComponent() { return editorComp; } public void setEditingColumn(int aColumn) { editingColumn = aColumn; } public int getEditingColumn() { return editingColumn; } public void removeEditor() { TableCellEditor editor = getCellEditor(); if (editor != null) { editor.removeCellEditorListener(this); requestFocus(); remove(editorComp); int index = getEditingColumn(); Rectangle cellRect = getHeaderRect(index); setCellEditor(null); setEditingColumn(-1); editorComp = null; repaint(cellRect); } } public boolean isEditing() { return cellEditor != null; } // // CellEditorListener // public void editingStopped(ChangeEvent e) { TableCellEditor editor = getCellEditor(); if (editor != null) { Object value = editor.getCellEditorValue(); int index = getEditingColumn(); columnModel.getColumn(index).setHeaderValue(value); removeEditor(); } } public void editingCanceled(ChangeEvent e) { removeEditor(); } } ================================================ FILE: src/main/java/com/mucommander/ui/table/EditableHeaderTableColumn.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.table; import javax.swing.table.TableCellEditor; import javax.swing.table.TableColumn; public class EditableHeaderTableColumn extends TableColumn { protected TableCellEditor headerEditor; protected boolean isHeaderEditable; public EditableHeaderTableColumn() { isHeaderEditable = false; } public void setHeaderEditor(TableCellEditor headerEditor) { this.headerEditor = headerEditor; } public TableCellEditor getHeaderEditor() { return headerEditor; } public void setHeaderEditable(boolean isEditable) { isHeaderEditable = isEditable; } public boolean isHeaderEditable() { return headerEditor != null && isHeaderEditable; } public void copyValues(TableColumn base) { modelIndex = base.getModelIndex(); identifier = base.getIdentifier(); width = base.getWidth(); minWidth = base.getMinWidth(); setPreferredWidth(base.getPreferredWidth()); maxWidth = base.getMaxWidth(); headerRenderer = base.getHeaderRenderer(); headerValue = base.getHeaderValue(); cellRenderer = base.getCellRenderer(); cellEditor = base.getCellEditor(); isResizable = base.getResizable(); } } ================================================ FILE: src/main/java/com/mucommander/ui/table/EditableHeaderUI.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.table; import javax.swing.*; import javax.swing.event.MouseInputListener; import javax.swing.plaf.basic.BasicTableHeaderUI; import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.event.MouseEvent; class EditableHeaderUI extends BasicTableHeaderUI { @Override protected MouseInputListener createMouseInputListener() { return new MouseInputHandler((EditableHeader) header); } public class MouseInputHandler extends BasicTableHeaderUI.MouseInputHandler { private Component dispatchComponent; protected final EditableHeader header; MouseInputHandler(EditableHeader header) { this.header = header; } private void setDispatchComponent(MouseEvent e) { Component editorComponent = header.getEditorComponent(); Point p = e.getPoint(); Point p2 = SwingUtilities.convertPoint(header, p, editorComponent); dispatchComponent = SwingUtilities.getDeepestComponentAt( editorComponent, p2.x, p2.y); } private boolean repostEvent(MouseEvent e) { if (dispatchComponent == null) { return false; } MouseEvent e2 = SwingUtilities.convertMouseEvent(header, e, dispatchComponent); dispatchComponent.dispatchEvent(e2); return true; } @Override public void mousePressed(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e)) { return; } super.mousePressed(e); if (header.getResizingColumn() == null) { Point p = e.getPoint(); TableColumnModel columnModel = header.getColumnModel(); int index = columnModel.getColumnIndexAtX(p.x); if (index != -1) { if (header.editCellAt(index, e)) { setDispatchComponent(e); repostEvent(e); } } } } @Override public void mouseReleased(MouseEvent e) { super.mouseReleased(e); if (!SwingUtilities.isLeftMouseButton(e)) { return; } repostEvent(e); dispatchComponent = null; } } } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/ActiveTabListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; /** * Interface to be implemented by classes that wish to be notified of tabs switching * or when properties of the active tab changed * * @author Arik Hadas */ public interface ActiveTabListener { void activeTabChanged(); } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/HideableTabbedPane.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import java.awt.*; import java.util.Iterator; import java.util.WeakHashMap; import javax.swing.JComponent; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; /** * This component acts like a tabbedpane in which multiple tabs are presented in a JTabbedPane layout * and single tab is presented without the JTabbedPane layout, only the tab's data. * * When a single tab is presented and new tab is added this component makes a switch to JTabbedPane layout, * and when two tabs are presented and there is a removal of one of the tabs this component makes a switch * to JPanel layout that contains the data of the tab that is left. * * This component also provides an interface for the other parts of the application to make operations * that influence the tabs layout. * * @author Arik Hadas */ public class HideableTabbedPane extends JComponent implements TabsEventListener, ConfigurationListener, ChangeListener { private static final Logger LOGGER = LoggerFactory.getLogger(HideableTabbedPane.class); /* The tabs which are being displayed */ private final TabsCollection tabsCollection = new TabsCollection<>(); /* The tabs display type (with/without tabs headers) * It is initialize as nullable so that it can be destroyed when it's replaced for the first time (see @{link tabAdded()})*/ private TabsViewer tabsViewer = new NullableTabsViewer<>(); /* The factory that will be used to create the viewers for tabs with no headers */ private final TabsViewerFactory tabsWithoutHeadersViewerFactory; /* The factory that will be used to create the viewers for tabs with headers */ private final TabsViewerFactory tabsWithHeadersViewerFactory; /* Contains all registered active tab change listeners, stored as weak references */ private final WeakHashMap activeTabChangedListener = new WeakHashMap<>(); /** * Constructor * * @param tabsWithoutHeadersViewerFactory - factory of tabs-display * @param tabsWithHeadersViewerFactory - factory of tabs-display */ public HideableTabbedPane(TabsViewerFactory tabsWithoutHeadersViewerFactory, TabsViewerFactory tabsWithHeadersViewerFactory) { setLayout(new BorderLayout()); this.tabsWithoutHeadersViewerFactory = tabsWithoutHeadersViewerFactory; this.tabsWithHeadersViewerFactory = tabsWithHeadersViewerFactory; // Register for tabs changes tabsCollection.addTabsListener(this); TcConfigurations.addPreferencesListener(this); } /** */ public synchronized void addActiveTabListener(ActiveTabListener listener) { activeTabChangedListener.put(listener, null); } /** */ public synchronized void removeActiveTabChangedListener(ActiveTabListener listener) { activeTabChangedListener.remove(listener); } /** */ protected synchronized void fireActiveTabChanged() { for (ActiveTabListener listener : activeTabChangedListener.keySet()) { listener.activeTabChanged(); } } /** * This function returns an iterator that points to the current Tabs contained in the TabbedPane * * @return Iterator that points to current Tabs */ public Iterator iterator() { return tabsCollection.iterator(); } /** * Select the given tab * * @param tab the tab to be selected */ public void selectTab(T tab) { int index = tabsCollection.indexOf(tab); if (index != -1) { selectTab(index); } else { LOGGER.error("Was requested to change to non-existing tab, ignoring"); } } /** * Select the tab at the given index * An exception will be thrown if no tab exists in the given index * * @param index of the tab to be selected */ public void selectTab(int index) { tabsViewer.setSelectedTabIndex(index); } /** * Return the index of the selected tab * * @return index of the selected tab */ public int getSelectedIndex() { return tabsViewer.getSelectedTabIndex(); } /** * Return how many tabs the panel contains * * @return number of tabs contained in the panel */ public int getTabsCount() { return tabsCollection.count(); } protected TabsCollection getTabs() { return tabsCollection; } /* Actions which are not depended on the display type (single/multiple tabs) */ /** * Add new tab * * @param tab - new tab's data */ protected void addTab(T tab) { tabsCollection.add(tab); } /** * Add new tab and select it * * @param tab - new tab's data */ protected void addAndSelectTab(T tab) { addTab(tab); tabsViewer.setSelectedTabIndex(tabsCollection.count()-1); } /** * Update the current displayed tab's data with the given {@link TabUpdater} * * @param updater - object that will be used to update the tab */ protected void updateCurrentTab(TabUpdater updater) { tabsCollection.updateTab(getSelectedIndex(), updater); } /* Actions that depended on the display type (single/multiple tabs) */ /** * Remove tab with the given header */ protected void removeTab(Component header) { tabsViewer.removeTab(header); } /** * Remove current displayed tab */ protected T removeTab() { return tabsCollection.remove(getSelectedIndex()); } /** * Remove duplicate tabs */ protected void removeDuplicateTabs() { tabsViewer.removeDuplicateTabs(); } /** * Remove all tabs except the current displayed tab */ protected void removeOtherTabs() { tabsViewer.removeOtherTabs(); } /** * Change the current displayed tab to the tab which is located to the right of the * current displayed tab. * If the current displayed tab is the rightmost tab, the leftmost tab will be displayed. */ public void nextTab() { tabsViewer.nextTab(); } /** * Change the current displayed tab to the tab which is located to the left of the * current displayed tab. * If the current displayed tab is the leftmost tab, the rightmost tab will be displayed. */ public void previousTab() { tabsViewer.previousTab(); } /****************** * Private Methods ******************/ private void switchToTabsWithHeaders() { setTabsViewer(tabsWithHeadersViewerFactory); } private void switchToTabWithoutHeader() { setTabsViewer(tabsWithoutHeadersViewerFactory); } private void setTabsViewer(TabsViewerFactory tabsViewerFactory) { tabsViewer.removeChangeListener(this); tabsViewer = tabsViewerFactory.create(tabsCollection); tabsViewer.addChangeListener(this); removeAll(); add(tabsViewer); validate(); tabsViewer.requestFocus(); } /** * Returns the tab at the given index * An exception will be thrown if no tab exists in the given index * * @param index of the requested tab * @return tab in the given index */ protected T getTab(int index) { return tabsCollection.get(index); } private boolean refreshViewer() { int nbTabs = tabsCollection.count(); return switch (nbTabs) { case 2 -> { switchToTabsWithHeaders(); yield true; } case 1 -> { if (showSingleTabHeader()) { switchToTabsWithHeaders(); } else { switchToTabWithoutHeader(); } yield true; } default -> false; }; } protected boolean showSingleTabHeader() { return TcConfigurations.getPreferences().getVariable(TcPreference.SHOW_TAB_HEADER, TcPreferences.DEFAULT_SHOW_TAB_HEADER); } protected void show(int tabIndex) { } @Override public void tabAdded(int index) { if (!refreshViewer()) tabsViewer.add(tabsCollection.get(index), index); if (isDisplayable()) { tabsViewer.setSelectedTabIndex(index); } } @Override public void tabRemoved(int index) { int previouslySelectedIndex = tabsViewer.getSelectedTabIndex(); if (!refreshViewer()) { tabsViewer.removeTab(index); } else { selectTab(Math.max(previouslySelectedIndex - 1, 0)); } } @Override public void tabUpdated(int index) { tabsViewer.update(tabsCollection.get(index), index); fireActiveTabChanged(); } @Override public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); // Update the button's icon if the system file icons policy has changed if (var.equals(TcPreferences.SHOW_SINGLE_TAB_HEADER)) refreshViewer(); } @Override public void stateChanged(ChangeEvent e) { final int selectedIndex = tabsViewer.getSelectedTabIndex(); if (selectedIndex != -1) show(selectedIndex); SwingUtilities.invokeLater(tabsViewer::requestFocus); } } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/NullableTabsViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import java.awt.Component; import javax.swing.JLabel; /** * Initial value for TabsDisplay object. * * @author Arik Hadas */ class NullableTabsViewer extends TabsViewer { public NullableTabsViewer() { super(new JLabel(), null); } @Override public void add(T tab) { } @Override public void add(T tab, int index) { } @Override public void update(T tab, int index) { } @Override public void remove(int index) { } @Override public int getSelectedTabIndex() { return 0; } @Override public void setSelectedTabIndex(int index) { } @Override public T removeCurrentTab() { return null; } @Override public void removeOtherTabs() { } @Override public void removeTab(Component header) { } @Override public void removeDuplicateTabs() { } @Override public void removeTab(int index) { } } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/Tab.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; /** * Interface for tabs. * * @author Arik Hadas */ public interface Tab { } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; /** * interface for factory of {@link com.mucommander.ui.tabs.Tab} * * @author Arik Hadas * * @param kind-of Tab * @param parameter for initiating the new tab */ public interface TabFactory { /** * This method returns new {@link com.mucommander.ui.tabs.Tab} based on the given parameter * * @param k a parameter that the new tab is based on * @return instance of subclass of {@link com.mucommander.ui.tabs.Tab} */ T createTab(K k); } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabUpdater.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; /** * This interface declare objects that are used to update properties of Tabs in a generic way * * @author Arik Hadas */ public interface TabUpdater { void update(T tab); } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabWithoutHeaderViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import java.awt.Component; import javax.swing.JComponent; /** * Component that presents tab with no header * * @author Arik Hadas */ public class TabWithoutHeaderViewer extends TabsViewer { /** The component to be displayed in the tab */ private final JComponent component; public TabWithoutHeaderViewer(TabsCollection tabs, JComponent component) { super(component, tabs); this.component = component; } @Override public void requestFocus() { component.requestFocusInWindow(); } @Override public int getSelectedTabIndex() { return 0; } @Override public void add(T tab, int index) { if (index > 0) { throw new IllegalArgumentException("Unable to add tab at index > 0 to single tab display"); } add(tab); } @Override public void update(T tab, int index) { } @Override public void setSelectedTabIndex(int index) { } @Override public void add(T tab) { } @Override public T removeCurrentTab() { return null; } @Override public void removeOtherTabs() { } @Override public void removeTab(Component header) { } @Override public void removeDuplicateTabs() { } @Override public void removeTab(int index) { } } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabbedPane.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import javax.swing.JTabbedPane; /** * TabbedPane that contains implementations of Tab interface * * @author Arik Hadas */ public abstract class TabbedPane extends JTabbedPane { /** * Add tab to the end of the tabbed pane * * @param tab - implementation of Tab interface */ public abstract void add(T tab); /** * Add tab in a given index * * @param tab - implementation of Tab interface * @param index - the index in which the tab would be added */ public abstract void add(T tab, int index); /** * Update tab in a given index * The updated tab would be selected in the end of the operation * * @param tab - implementation of Tab interface * @param index - the index of the tab to be updated */ public abstract void update(T tab, int index); } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabsCollection.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.WeakHashMap; /** * Collection of tabs * The collection is iterable, and can notify listeners about added/removed/updated tabs * * @author Arik Hadas */ public class TabsCollection implements java.lang.Iterable { /** List of tabs */ private final List collection = new ArrayList<>(); /** Listeners that were registered to be notified when tabs are added/removed/updated */ private final WeakHashMap tabsListeners = new WeakHashMap<>(); /** * Empty constructor */ TabsCollection() { } /** * Constructor that creates the collection with a single given tab * * @param tab - a tab */ public TabsCollection(T tab) { collection.add(tab); } /** * Constructor that creates the collection with multiple given tabs * * @param tabs - list of tabs */ public TabsCollection(List tabs) { collection.addAll(tabs); } /** * Add the given tab to the collection * The tab would be inserted in the last index in the collection * * @param tab - tab */ public void add(T tab) { add(tab, count()); } /** * Add the given tab to the collection in a given index * * @param tab - tab * @param index - the index in which the tab would be inserted in the collection */ public void add(T tab, int index) { collection.add(index, tab); fireTabAdded(count() - 1); } /** * Update the tab in the given index with the given {@link TabUpdater} * * @param index - the index of the tab to be updated * @param updater - the object that will be used to update the tab */ void updateTab(int index, TabUpdater updater) { updater.update(get(index)); fireTabUpdated(index); } /** * Remove the tab in the given index * If there are less than two tabs in the collection, the tab * won't be removed in order to prevent a situation in which * we remain with no tabs at all * * @param index - the index of the tab to be removed */ public T remove(int index) { if (collection.size() > 1) { T tab = collection.remove(index); fireTabRemoved(index); return tab; } return null; } /** * Return the tab in the given index * * @param index - the index of the tab to be returned * @return the tab in the given index */ public T get(int index) { if (index < 0 || index >= collection.size()) { return null; } return collection.get(index); } /** * Return the number of tabs contained in the collection * * @return the number of tabs contained in the collection */ public int count() { return collection.size(); } /** * Return the index of the given tab in the collection * * @return the index of the given tab or -1 if the tab is not exist in the collection */ public int indexOf(T tab) { return collection.indexOf(tab); } /** * Add a given listener to the listeners to be notified about tabs-changes * * @param listener - object that implements TabsChangeListener interface */ synchronized void addTabsListener(TabsEventListener listener) { tabsListeners.put(listener, null); } /** * Remove a given listener from the listeners to be notifies about tabs-changes * * @param listener - object that implements TabsChangeListener interface */ public synchronized void removeTabsListener(TabsEventListener listener) { tabsListeners.remove(listener); } /** * Notify the registered listeners about addition of tab in the given index * * @param index - the index of the added tab */ private synchronized void fireTabAdded(int index) { Set listeners = new HashSet<>(tabsListeners.keySet()); for (TabsEventListener listener : listeners) { listener.tabAdded(index); } } /** * Notify the registered listeners about removal of tab in the given index * * @param index - the index in which the removed tab was located */ private synchronized void fireTabRemoved(int index) { Set listeners = tabsListeners.keySet(); for (TabsEventListener listener : listeners) { listener.tabRemoved(index); } } /** * Notify the registered listeners about tab that was updated in the given index * * @param index - the index of the updated tab */ private synchronized void fireTabUpdated(int index) { Set listeners = tabsListeners.keySet(); for (TabsEventListener listener : listeners) { listener.tabUpdated(index); } } @NotNull @Override public Iterator iterator() { return collection.iterator(); } } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabsEventListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; /** * Interface to be implemented by classes that wish to be notified of tabs changes on a particular * HideableTabbedPane. Those classes need to be registered to receive those events, this can be done by calling * {@link TabsCollection#addTabsListener(TabsEventListener)}. * * @see com.mucommander.ui.tabs.TabsCollection * @author Arik Hadas */ public interface TabsEventListener { /** * This method is invoked when a tab was added. * * @param index - the index in which the tab was added */ void tabAdded(int index); /** * This method is invoked when a tab was removed. * * @param index - the index in which the tab was added */ void tabRemoved(int index); /** * This method is invoked when a tab data was updated. * * @param index - the index of the updated tab */ void tabUpdated(int index); } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabsViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import java.awt.Component; import java.awt.GridLayout; import javax.swing.JComponent; import javax.swing.event.ChangeListener; /** * Abstract class for components that display tabs. * * @author Arik Hadas */ public abstract class TabsViewer extends JComponent { /** Collection of the displayed tabs */ private final TabsCollection tabs; TabsViewer(JComponent component, TabsCollection tabs) { this.tabs = tabs; setLayout(new GridLayout(1, 1)); add(component); } public void addChangeListener(ChangeListener listener) { } public void removeChangeListener(ChangeListener listener) { } /*************** * Tabs Actions ***************/ public abstract void add(T tab); public abstract void add(T tab, int index); public abstract void update(T tab, int index); public abstract int getSelectedTabIndex(); public abstract void setSelectedTabIndex(int index); public abstract T removeCurrentTab(); public abstract void removeDuplicateTabs(); public abstract void removeOtherTabs(); public abstract void removeTab(Component header); public abstract void removeTab(int index); public void nextTab() { setSelectedTabIndex((getSelectedTabIndex()+1) % tabs.count()); } public void previousTab() { int numOfTabs = tabs.count(); setSelectedTabIndex((getSelectedTabIndex()-1+numOfTabs) % numOfTabs); } } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabsViewerFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; /** * Factory that creates tabs viewer. * * @author Arik Hadas */ public interface TabsViewerFactory { TabsViewer create(TabsCollection tabs); } ================================================ FILE: src/main/java/com/mucommander/ui/tabs/TabsWithHeaderViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tabs; import java.awt.Component; import java.util.HashSet; import java.util.Set; import javax.swing.event.ChangeListener; /** * Component that presents tabs with headers * * @author Arik Hadas */ public class TabsWithHeaderViewer extends TabsViewer { private final TabsCollection tabsCollection; private final TabbedPane tabbedPane; public TabsWithHeaderViewer(TabsCollection tabs, TabbedPane tabbedpane) { super(tabbedpane, tabs); this.tabsCollection = tabs; this.tabbedPane = tabbedpane; int index = 0; for (T tab : tabs) { tabbedpane.add(tab, index++); } } /************** * TabsViewer **************/ @Override public int getSelectedTabIndex() { return tabbedPane.getSelectedIndex(); } @Override public void removeTab(int index) { tabbedPane.remove(index); } @Override public void addChangeListener(ChangeListener listener) { tabbedPane.addChangeListener(listener); } @Override public void removeChangeListener(ChangeListener listener) { tabbedPane.removeChangeListener(listener); } @Override public void add(T tab) { add(tab, tabsCollection.count()); } @Override public void add(T tab, int index) { tabbedPane.add(tab, index); } @Override public void update(T tab, int index) { tabbedPane.update(tab, index); } @Override public void setSelectedTabIndex(int index) { tabbedPane.setSelectedIndex(index); } @Override public void requestFocus() { tabbedPane.requestFocusInWindow(); } @Override public T removeCurrentTab() { T tab = tabsCollection.get(getSelectedTabIndex()); tabsCollection.remove(getSelectedTabIndex()); return tab; } @Override public void removeDuplicateTabs() { // a Set that will contain the tabs we've seen Set visitedTabs = new HashSet<>(); // a Set that will contain the tabs which are duplicated Set duplicatedTabs = new HashSet<>(); // The index of the selected tab int selectedTabIndex = getSelectedTabIndex(); // add all duplicated tabs to the duplicatedTab Set for (T tab : tabsCollection) { if (!visitedTabs.add(tab)) duplicatedTabs.add(tab); } // remove all duplicated tabs which are identical to the selected tab without // changing the tab selection T selectedTab = tabsCollection.get(selectedTabIndex); if (duplicatedTabs.remove(selectedTab)) { int removedTabsCount = 0; int tabsCount = tabsCollection.count(); for (int i=0; i0; --i) { tabsCollection.remove(1); } } @Override public void removeTab(Component header) { tabsCollection.remove(tabbedPane.indexOfTabComponent(header)); } } ================================================ FILE: src/main/java/com/mucommander/ui/terminal/JediTerminalPanelEx.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.terminal import com.jediterm.terminal.CursorShape import com.jediterm.terminal.model.StyleState import com.jediterm.terminal.model.TerminalTextBuffer import com.jediterm.terminal.ui.TerminalPanel import com.jediterm.terminal.ui.settings.SettingsProvider import com.mucommander.commons.runtime.OsFamily import com.mucommander.ui.action.ActionKeymap import com.mucommander.ui.action.impl.TerminalPanelAction import com.mucommander.ui.main.MainFrame import ru.trolsoft.calculator.CalculatorDialog import java.awt.event.InputEvent import java.awt.event.KeyEvent import javax.swing.KeyStroke /** * @author Oleg Trifonov * Created on 27/10/14. */ class JediTerminalPanelEx internal constructor( settingsProvider: SettingsProvider, terminalTextBuffer: TerminalTextBuffer, styleState: StyleState, private val mainFrame: MainFrame ) : TerminalPanel(settingsProvider, terminalTextBuffer, styleState) { private val keyModifier: Int = if (OsFamily.MAC_OS_X.isCurrent) KeyEvent.META_DOWN_MASK else KeyEvent.CTRL_DOWN_MASK private var lineHeight = 0 init { setDefaultCursorShape(CursorShape.BLINK_VERTICAL_BAR) } override fun processKeyEvent(e: KeyEvent) { val id = e.getID() if (id == KeyEvent.KEY_PRESSED) { val actionId = ActionKeymap.getRegisteredActionIdForKeystroke( KeyStroke.getKeyStroke( e.getKeyCode(), e.modifiersEx, false ) ) if (TerminalPanelAction.Descriptor.ACTION_ID == actionId) { mainFrame.showTerminalPanel(false) return } if (e.modifiersEx == keyModifier) { when (e.getKeyCode()) { KeyEvent.VK_UP -> { resizePanel(1) return } KeyEvent.VK_DOWN -> { resizePanel(-1) return } KeyEvent.VK_LEFT -> { resizePanel(-10) return } KeyEvent.VK_RIGHT -> { resizePanel(10) return } KeyEvent.VK_1 -> { mainFrame.leftPanel.fileTable.requestFocus() return } KeyEvent.VK_2 -> { mainFrame.rightPanel.fileTable.requestFocus() return } } } super.processKeyEvent(e) } else if (id == KeyEvent.KEY_TYPED) { super.processKeyEvent(e) } if (e.modifiersEx == InputEvent.ALT_DOWN_MASK && e.getKeyCode() == KeyEvent.VK_C) { if (id == KeyEvent.KEY_RELEASED) { CalculatorDialog(mainFrame.jFrame).showDialog() } e.consume() return } e.consume() } private fun resizePanel(delta: Int) { var lineHeight = getHeight() / terminalTextBuffer.height if (lineHeight > 0 && (lineHeight < this.lineHeight || this.lineHeight == 0)) { this.lineHeight = lineHeight } lineHeight = this.lineHeight var height = mainFrame.terminalPanelHeight + delta * lineHeight val minHeight = 2 * lineHeight val maxHeight = mainFrame.jFrame.getHeight() - minHeight if (delta < -2) { height = minHeight } else if (delta > 2) { height = maxHeight mainFrame.setTerminalPanelHeight(height) } else if (height !in minHeight..maxHeight) { return } mainFrame.setTerminalPanelHeight(height) } } ================================================ FILE: src/main/java/com/mucommander/ui/terminal/TcTerminal.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.terminal import com.jediterm.terminal.TtyConnector import com.jediterm.terminal.model.StyleState import com.jediterm.terminal.model.TerminalTextBuffer import com.jediterm.terminal.ui.JediTermWidget import com.jediterm.terminal.ui.TerminalPanel import com.jediterm.terminal.ui.TerminalSession import com.jediterm.terminal.ui.TerminalWidget import com.jediterm.terminal.ui.settings.SettingsProvider import com.mucommander.cache.WindowsStorage import com.mucommander.ui.main.MainFrame import java.awt.event.FocusEvent import java.awt.event.FocusListener import java.io.IOException import javax.swing.JComponent /** * @author Oleg Trifonov * Created on 24/10/14. */ class TcTerminal(private val mainFrame: MainFrame) { private val termWidget: TerminalWidget init { val settingsProvider: SettingsProvider = TerminalSettingsProvider() val ttyConnector = createTtyConnector(getCurrentFolder()) termWidget = object : JediTermWidget(settingsProvider) { override fun createTerminalPanel( settingsProvider: SettingsProvider, styleState: StyleState, textBuffer: TerminalTextBuffer ): TerminalPanel = JediTerminalPanelEx(settingsProvider, textBuffer, styleState, mainFrame) } termWidget.component.addFocusListener(object : FocusListener { override fun focusGained(e: FocusEvent?) { updateTitle() } override fun focusLost(e: FocusEvent?) { mainFrame.updateWindowTitle() } }) if (termWidget.canOpenSession()) { openSession(termWidget, ttyConnector) } } private fun createTtyConnector(directory: String?): TcTerminalTtyConnector? { try { return object : TcTerminalTtyConnector(directory) { override fun close() { super.close() mainFrame.closeTerminalSession() } } } catch (e: IOException) { e.printStackTrace() return null } } fun openSession(terminal: TerminalWidget, ttyConnector: TtyConnector?) { val session: TerminalSession = terminal.createTerminalSession(ttyConnector) session.start() } fun storeHeight(height: Int) { WindowsStorage.getInstance().put(STORAGE_KEY, WindowsStorage.Record(0, 0, 0, height)) } fun loadHeight(): Int = WindowsStorage.getInstance().get(STORAGE_KEY)?.height ?: -1 fun getComponent(): JComponent = termWidget.component fun show(show: Boolean) { termWidget.component.isVisible = show } private fun getCurrentFolder(): String? { val currentFolder = mainFrame.activePanel.currentFolder.absolutePath return if (currentFolder.contains("://")) null else currentFolder } fun updateTitle() { mainFrame.jFrame.setTitle(termWidget.terminalDisplay.windowTitle) } companion object { private const val STORAGE_KEY = "TerminalPanel" } } ================================================ FILE: src/main/java/com/mucommander/ui/terminal/TcTerminalTtyConnector.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.terminal import com.jediterm.terminal.ProcessTtyConnector import com.mucommander.conf.TcConfigurations import com.mucommander.conf.TcPreference import com.mucommander.conf.TcPreferences import com.mucommander.desktop.DesktopManager import com.pty4j.PtyProcess import com.pty4j.PtyProcessBuilder import java.io.IOException import java.nio.charset.StandardCharsets import java.util.* /** * @author Oleg Trifonov * Created on 28/10/14. */ open class TcTerminalTtyConnector private constructor(process: PtyProcess) : ProcessTtyConnector(process, StandardCharsets.UTF_8) { private val myDataChunks: MutableList = ArrayList() internal constructor(directory: String?) : this(createPtyProcess(directory)) override fun getName(): String = "" @Throws(IOException::class) override fun read(buf: CharArray, offset: Int, length: Int): Int { val len = super.read(buf, offset, length) if (len > 0) { val arr = buf.copyOfRange(offset, len) myDataChunks.add(arr) } return len } fun getChunks(): MutableList = ArrayList(myDataChunks) companion object { @Throws(IOException::class) private fun createPtyProcess(directory: String?): PtyProcess { val envs: MutableMap = HashMap(System.getenv()).apply { put("TERM", "xterm-256color") } val pref = TcConfigurations.getPreferences() var cmd = if (pref.getVariable(TcPreference.TERMINAL_USE_CUSTOM_SHELL,TcPreferences.DEFAULT_TERMINAL_USE_CUSTOM_SHELL)) { pref.getVariable(TcPreference.TERMINAL_SHELL) } else { DesktopManager.getDefaultTerminalShellCommand() } cmd = cmd.replace("\t", " ").replace(" +".toRegex(), " ") val command: Array = cmd.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() return PtyProcessBuilder() .setCommand(command) .setEnvironment(envs) .setDirectory(directory) .setRedirectErrorStream(true) .setWindowsAnsiColorEnabled(true) .setUnixOpenTtyToPreserveOutputAfterTermination(true) .setSpawnProcessUsingJdkOnMacIntel(true) .setConsole(false) // Windows only ? .start() } } } ================================================ FILE: src/main/java/com/mucommander/ui/terminal/TerminalSettingsProvider.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.terminal import com.jediterm.terminal.HyperlinkStyle.HighlightMode import com.jediterm.terminal.TerminalColor import com.jediterm.terminal.TerminalColor.rgb import com.jediterm.terminal.TextStyle import com.jediterm.terminal.emulator.ColorPalette import com.jediterm.terminal.emulator.ColorPaletteImpl import com.jediterm.terminal.model.TerminalTypeAheadSettings import com.jediterm.terminal.ui.settings.DefaultSettingsProvider import com.mucommander.commons.runtime.OsFamily import com.mucommander.ui.theme.Theme import com.mucommander.ui.theme.ThemeManager import org.slf4j.LoggerFactory import java.awt.Color import java.awt.Font /** * Provides terminal settings synchronized with application theme. * Extends DefaultSettingsProvider to integrate with JediTerm terminal emulator. * * @author Oleg Trifonov * Created on 27/10/14. */ class TerminalSettingsProvider : DefaultSettingsProvider() { // Cached color palette to avoid recreation on each call private var colorPalette: ColorPalette? = null private val log = LoggerFactory.getLogger(TerminalSettingsProvider::class.java) override fun getDefaultForeground(): TerminalColor = getThemeColor(Theme.TERMINAL_FOREGROUND_COLOR, Color.GREEN).toTerm() override fun getDefaultBackground(): TerminalColor = getThemeColor(Theme.TERMINAL_BACKGROUND_COLOR, Color.BLACK).toTerm() override fun getSelectionColor(): TextStyle { val fg = getThemeColor(Theme.TERMINAL_SELECTED_FOREGROUND_COLOR, Color.WHITE) val bg = getThemeColor(Theme.TERMINAL_SELECTED_BACKGROUND_COLOR, Color(0x6666ff)) return TextStyle(fg.toTerm(), bg.toTerm()) } override fun getFoundPatternColor(): TextStyle = // Use selection colors for found pattern highlighting getSelectionColor() override fun getHyperlinkColor(): TextStyle { val hyperlinkColor = getThemeColor(Theme.TERMINAL_FOREGROUND_COLOR, Color.BLUE) return TextStyle(hyperlinkColor.toTerm(), null) } override fun getHyperlinkHighlightingMode(): HighlightMode = HighlightMode.HOVER override fun getTerminalFont(): Font = ThemeManager.getCurrentFont(Theme.TERMINAL_FONT) ?: super.getTerminalFont() override fun getTerminalFontSize(): Float = getTerminalFont().getSize2D() override fun getTerminalColorPalette(): ColorPalette? { if (colorPalette == null) { colorPalette = createColorPalette() } return colorPalette } override fun useInverseSelectionColor(): Boolean = false override fun copyOnSelect(): Boolean = true // Enable copy on select for better UX override fun pasteOnMiddleMouseClick(): Boolean = true override fun emulateX11CopyPaste(): Boolean = false override fun useAntialiasing(): Boolean = true override fun audibleBell(): Boolean = false override fun enableMouseReporting(): Boolean = true override fun caretBlinkingMs(): Int = 500 override fun scrollToBottomOnTyping(): Boolean = true override fun maxRefreshRate(): Int = 50 override fun DECCompatibilityMode(): Boolean = true override fun forceActionOnMouseReporting(): Boolean = false override fun getBufferMaxLinesCount(): Int = 5000 override fun altSendsEscape(): Boolean = true override fun ambiguousCharsAreDoubleWidth(): Boolean = false override fun getTypeAheadSettings() = TerminalTypeAheadSettings.DEFAULT /** * Safely retrieves color from ThemeManager with fallback value. * * @param themeId theme color identifier * @param defaultColor fallback color if theme manager returns null * @return color from theme or default color */ private fun getThemeColor(themeId: Int, defaultColor: Color): Color = try { ThemeManager.getCurrentColor(themeId) ?: defaultColor } catch (e: Exception) { log.warn("Failed to get theme color for id={}, using default", themeId, e) defaultColor } /** * Creates color palette from current theme. * * @return color palette with ANSI colors from theme */ private fun createColorPalette(): ColorPalette = if (OsFamily.getCurrent() == OsFamily.WINDOWS) { ColorPaletteImpl.WINDOWS_PALETTE } else { ColorPaletteImpl.XTERM_PALETTE } private fun Color.toTerm(): TerminalColor = rgb(red, green, blue) } ================================================ FILE: src/main/java/com/mucommander/ui/text/FileLabel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.icon.FileIcons; import javax.swing.*; /** * A simple JLabel that displays information about a file: *
      *
    • the label's text is set to the file's name or canonical path (specified in the constructor)
    • *
    • the label's icon is set to the file's icon, as returned by {@link FileIcons#getFileIcon(com.mucommander.commons.file.AbstractFile)}
    • *
    • the label's tooltip is set to the file's canonical path, only if the label's text is the file's name
    • *
    * * @author Maxence Bernard */ public class FileLabel extends JLabel { /** * Creates a new FileLabel, showing the file's name or full canonical path depending on the value of * showFullPath. * * @param file the file to show * @param showFullPath if true, the file's canonical path will be displayed, if false its filename. */ public FileLabel(AbstractFile file, boolean showFullPath) { String path = file.getCanonicalPath(); if (showFullPath) { setText(path); } else { setText(file.getName()); setToolTipText(path); } setIcon(FileIcons.getFileIcon(file)); } } ================================================ FILE: src/main/java/com/mucommander/ui/text/FilePathField.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.autocomplete.BasicAutocompleterTextComponent; import com.mucommander.ui.autocomplete.CompleterFactory; import com.mucommander.ui.autocomplete.TextFieldCompletion; import com.mucommander.ui.autocomplete.completers.PathCompleter; import javax.swing.*; import javax.swing.text.Document; /** * FilePathField is a text field that is made to receive a file path. It provides auto-completion * capabilities, suggesting files/folders to the user as the path is being entered. * * @author Maxence Bernard */ public class FilePathField extends JTextField { private PathCompleter pathCompleter; public FilePathField() { super(); init(); } public FilePathField(String text) { super(text); init(); } public FilePathField(int columns) { super(columns); init(); } public FilePathField(String text, int columns) { super(text, columns); init(); } public FilePathField(Document doc, String text, int columns) { super(doc, text, columns); init(); } /** * Adds auto-completion capabilities to this text field. */ private void init() { pathCompleter = (PathCompleter)CompleterFactory.getPathCompleter(); new TextFieldCompletion(new BasicAutocompleterTextComponent(this), pathCompleter); new FilePathFieldKeyListener(this, true); } public void setDefaultLocation(AbstractFile dir) { pathCompleter.setCurrentLocation(dir); } } ================================================ FILE: src/main/java/com/mucommander/ui/text/FilePathFieldKeyListener.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import javax.swing.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * @author Oleg Trifonov * Created on 25/01/17. */ public class FilePathFieldKeyListener implements KeyListener { private final JTextField textField; private final boolean deleteOnFirstAction; protected FilePathFieldKeyListener(JTextField textField, boolean deleteOnFirstAction) { super(); this.textField = textField; this.deleteOnFirstAction = deleteOnFirstAction; textField.addKeyListener(this); } @Override public void keyPressed(KeyEvent e) { // Key listener to detect first left/right arrow pressed // if user press the left button then move cursor to the start of file name // if user press the right button then move cursor to the end of file name if (e.getKeyCode() == KeyEvent.VK_LEFT) { if (deleteOnFirstAction) { textField.removeKeyListener(this); } if (e.getModifiersEx() != 0) { return; } int len = textField.getText().length(); int pos = textField.getCaretPosition(); String selected = textField.getSelectedText(); String text = textField.getText(); if (selected != null && ((len > pos && text.charAt(pos) == '.') || len == pos)) { int newPos = pos - selected.length(); if (newPos >= 0) { textField.setCaretPosition(newPos); } e.consume(); } } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { if (deleteOnFirstAction) { textField.removeKeyListener(this); } if (e.getModifiersEx() != 0) { return; } int pos = textField.getCaretPosition(); String selected = textField.getSelectedText(); String text = textField.getText(); if (selected != null && text.length() > pos && text.charAt(pos) == '.') { textField.setCaretPosition(pos); e.consume(); } } } @Override public void keyTyped(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { } } ================================================ FILE: src/main/java/com/mucommander/ui/text/FontUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import com.mucommander.RuntimeConstants; import com.mucommander.commons.runtime.OsFamily; import javax.swing.*; import java.awt.*; /** * This class contains a set of helper methods that allow to easily change the font of a JComponent * * @author Maxence Bernard */ public class FontUtils { public static void setup() { if (OsFamily.getCurrent() == OsFamily.LINUX && RuntimeConstants.DISPLAY_4K) { scaleFont("Menu.font", 18, 24); scaleFont("MenuItem.font", 18, 24); scaleFont("CheckBoxMenuItem.font", 18, 24); scaleFont("TabbedPane.font", 18, 22); scaleFont("CheckBox.font", 18, 24); scaleFont("ComboBox.font", 18, 24); scaleFont("RadioButton.font", 18, 24); scaleFont("Button.font", 18, 22); scaleFont("Label.font", 18, 24); scaleFont("List.font", 18, 22); scaleFont("TextField.font", 18, 22); scaleFont("ToolTip.font", 18, 24); scaleFont("Table.font", 18, 22); scaleFont("TableHeader.font", 18, 24); scaleFont("TitledBorder.font", 18, 24); scaleFont("ProgressBar.font", 18, 22); scaleFont("JideSplitButton.font", 18, 22); } } private static void scaleFont(String uiManagerName, int minSize, int size) { Font font = UIManager.getFont(uiManagerName); if (font != null && font.getSize() <= minSize) { Font newFont = new Font(font.getFontName(), font.getStyle(), size); UIManager.put(uiManagerName, newFont); } } public static Font scaleFont(Font font, int minSize, int size) { if (font.getSize() <= minSize) { return new Font(font.getFontName(), font.getStyle(), size); } else { return font; } } public static void scaleFont(JComponent component, int minSize, int size) { component.setFont(scaleFont(component.getFont(), minSize, size)); } /** * Changes the style of the given component's font. Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @param newStyle the new Font style to use, see java.awt.Font for allowed values * @return the component that was passed, for convenience only */ public static JComponent changeStyle(JComponent comp, int newStyle) { comp.setFont(comp.getFont().deriveFont(newStyle)); return comp; } /** * Changes the size of the given component's font. Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @param newSize the new Font size to use, see java.awt.Font for allowed values * @return the component that was passed, for convenience only */ public static JComponent changeSize(JComponent comp, float newSize) { comp.setFont(comp.getFont().deriveFont(newSize)); return comp; } /** * Changes the style and size of the given component's font. Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @param newStyle the new Font style to use, see java.awt.Font for allowed values * @param newSize the new Font size to use, see java.awt.Font for allowed values * @return the component that was passed, for convenience only */ public static JComponent changeStyleAndSize(JComponent comp, int newStyle, float newSize) { comp.setFont(comp.getFont().deriveFont(newStyle, newSize)); return comp; } /** * Changes the style of the given component's font to {@link java.awt.Font#BOLD}. * Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @return the component that was passed, for convenience only */ public static JComponent makeBold(JComponent comp) { changeStyle(comp, Font.BOLD); return comp; } /** * Changes the style of the given component's font to {@link java.awt.Font#ITALIC}. * Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @return the component that was passed, for convenience only */ public static JComponent makeItalic(JComponent comp) { changeStyle(comp, Font.BOLD); return comp; } /** * Changes the style of the given component's font to {@link java.awt.Font#BOLD}|{@link java.awt.Font#ITALIC}. * Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @return the component that was passed, for convenience only */ public static JComponent makeBoldItalic(JComponent comp) { changeStyle(comp, Font.BOLD|Font.ITALIC); return comp; } /** * Changes the style of the given component's font to {@link java.awt.Font#PLAIN}. * Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @return the component that was passed, for convenience only */ public static JComponent makePlain(JComponent comp) { changeStyle(comp, Font.PLAIN); return comp; } /** * Decreases the size of the given component's font by 2 units. Other attributes of the font are left unchanged. * * @param comp the component for which to change the font * @return the component that was passed, for convenience only */ public static JComponent makeMini(JComponent comp) { changeSize(comp, comp.getFont().getSize()-2); return comp; } } ================================================ FILE: src/main/java/com/mucommander/ui/text/KeyStrokeUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import javax.swing.KeyStroke; import com.mucommander.commons.runtime.OsFamily; /** * This class offers utility methods for converting KeyStrokes to texts. * * @author Arik Hadas, Maxence Bernard */ public class KeyStrokeUtils { private final static String SHIFT_MODIFIER_STRING = InputEvent.getModifiersExText(KeyEvent.SHIFT_DOWN_MASK); private final static String CTRL_MODIFIER_STRING = InputEvent.getModifiersExText(KeyEvent.CTRL_DOWN_MASK); private final static String ALT_MODIFIER_STRING = InputEvent.getModifiersExText(KeyEvent.ALT_DOWN_MASK); private final static String META_MODIFIER_STRING = InputEvent.getModifiersExText(KeyEvent.META_DOWN_MASK); /** * Returns a String representation for the given KeyStroke for display, in the following format:
    * modifier+modifier+...+key * *

    For example, KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK|InputEvent.ALT_MASK) * will return Ctrl+Alt+C. * * @param ks the KeyStroke for which to return a String representation * @return a String representation of the given KeyStroke for display, in the [modifier]+[modifier]+...+key format */ public static String getKeyStrokeRepresentation(KeyStroke ks) { return ks.toString().replaceFirst("(released )|(pressed )|(typed )", ""); } /** * Returns a String representation for the given KeyStroke for display, in the following format:
    * modifier+modifier+...+key * *

    For example, KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK|InputEvent.ALT_MASK) * will return Ctrl+Alt+C. * * @param ks the KeyStroke for which to return a String representation * @return a String representation of the given KeyStroke for display, in the [modifier]+[modifier]+...+key format */ public static String getKeyStrokeDisplayableRepresentation(KeyStroke ks) { if (ks == null) { return null; } int modifiers = ks.getModifiers(); String keyText = KeyEvent.getKeyText(ks.getKeyCode()); if (modifiers != 0) { return getModifiersDisplayableRepresentation(modifiers)+"+"+keyText; } return keyText; } /** * Returns a String representations of the given modifiers bitwise mask, in the following format:
    * modifier+...+modifier * *

    The modifiers' order in the returned String tries to mimick the keyboard layout of the current platform as * much as possible: * *

      *
    • Under Mac OS X, the order is: Shift, Ctrl, Alt, Meta *
    • Under other platforms, the order is Shift, Ctrl, Meta, Alt *

    * * @param modifiers a modifiers bitwise mask * @return a String representations of the given modifiers bitwise mask */ public static String getModifiersDisplayableRepresentation(int modifiers) { StringBuilder result = new StringBuilder(); if ((modifiers&KeyEvent.SHIFT_DOWN_MASK) != 0) { result.append(SHIFT_MODIFIER_STRING); } if ((modifiers&KeyEvent.CTRL_DOWN_MASK) != 0) { appendModifier(result, CTRL_MODIFIER_STRING); } if (OsFamily.MAC_OS_X.isCurrent()) { if ((modifiers&KeyEvent.ALT_DOWN_MASK) != 0) { appendModifier(result, ALT_MODIFIER_STRING); } if ((modifiers&KeyEvent.META_DOWN_MASK) != 0) { appendModifier(result, META_MODIFIER_STRING); } } else { if ((modifiers&KeyEvent.META_DOWN_MASK) != 0) { appendModifier(result, META_MODIFIER_STRING); } if ((modifiers&KeyEvent.ALT_DOWN_MASK) != 0) { appendModifier(result, ALT_MODIFIER_STRING); } } return result.toString(); } private static void appendModifier(StringBuilder sb, String s) { if (!sb.isEmpty()) { sb.append('+'); } sb.append(s); } } ================================================ FILE: src/main/java/com/mucommander/ui/text/MultiLineLabel.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; /** * MultiLineLabel is a line-wrapping label that spawns the text over multiple lines as necessary. Unlike what its name * suggests, it is derived from JTextArea and not JLabel, but it looks just like a label. * When added to a container, this component takes just the amount of space it needs, without the need to set a fixed * number of rows or columns. * * @author Maxence Bernard */ public class MultiLineLabel extends JTextArea { /** * Equivalent to calling {@link #MultiLineLabel(String, boolean)} with auto-repack enabled. * * @param text the initial label's text */ public MultiLineLabel(String text) { this(text, true); } /** * Creates a new MultiLineLabel, spawning over multiple lines as necessary. By default, lines are * wrapped at word boundaries, i.e. words are not split over multiple lines. This behavior can be changed by * calling {@link #setWrapStyleWord(boolean)}. *

    * The autoRepack parameter allows to automatically issue an extra call to the pack() * method of the Window that contains this component, for the window to be layed out properly. This works around a * well-known bug that affects line-wrapping text components which report an incorrect preferred size, causing * layout issues. This parameter should be always enabled unless a fixed number of rows or columns is set using * {@link #setRows(int)} or {@link #setColumns(int)}.
    * For reference, here are links to the afore-mentionned issue: *

      *
    • http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4924163
    • *
    • http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4446522
    • *
    * * @param text the initial label's text * @param autoRepack if true, an extra call to the pack() method of the Window that * contains this component will be automatically issued after this component has first been layed out. */ public MultiLineLabel(String text, boolean autoRepack) { super(text); setEditable(false); setLineWrap(true); setWrapStyleWord(true); // Make this text area look like a label setOpaque(false); setBackground((Color) UIManager.get("Label.background")); setForeground((Color) UIManager.get("Label.foreground")); setFont((Font) UIManager.get("Label.font")); if (autoRepack) { addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { Container tla = getTopLevelAncestor(); if(tla instanceof Window) { ((Window)tla).pack(); } removeComponentListener(this); } }); } } } ================================================ FILE: src/main/java/com/mucommander/ui/text/RecordingKeyStrokeTextField.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; import javax.swing.*; import javax.swing.border.Border; import java.awt.*; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * RecordingKeyStrokeTextField is a text field that record a KeyStroke entered by the user. * * @author Arik Hadas */ public class RecordingKeyStrokeTextField extends JTextField implements FocusListener, KeyListener { /** The last KeyStroke that was entered to the field. */ private KeyStroke lastKeyStroke; /** The default border of JTextField */ private final Border defaultTextFieldBorder = getBorder(); protected RecordingKeyStrokeTextField(int columns, KeyStroke keyStroke) { // set text field's length setColumns(columns); // The text will be shown at the center of the text field setHorizontalAlignment(JTextField.CENTER); // The text field should not be editable setEditable(false); // Change colors to prevent the user from marking the field's text setSelectionColor(UIManager.getColor("jtextfield.background")); setSelectedTextColor(getForeground()); // Use JTextField's "setText" method to set the initial KeyStroke in the text field super.setText(KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(lastKeyStroke = keyStroke)); // Add listeners: addFocusListener(this); addKeyListener(this); } /** * This method is used to fetch the KeyStroke in the text-field. * The returned KeyStrole is the last KeyStroke entered to the field by the user, or * the initial KeyStroke that was loaded to the field if the user didn't entered anything. * * @return the KeyStroke in the text-field */ public KeyStroke getKeyStroke() { return lastKeyStroke; } ////////////////////////////////// ///// FocusListener methods //// ///////////////////////////////// public void focusGained(FocusEvent e) { // change border to indicate this field gained the focus // and the user can type setBorder(BorderFactory.createLineBorder(Color.orange, 2)); } public void focusLost(FocusEvent e) { // change border to indicate this field lost the focus setBorder(defaultTextFieldBorder); } //////////////////////////////// ///// KeyListener methods //// //////////////////////////////// public void keyPressed(KeyEvent e) { if (e.getKeyCode() != KeyEvent.VK_ESCAPE) { lastKeyStroke = KeyStroke.getKeyStroke(e.getKeyCode(), 0); setText(KeyStrokeUtils.getKeyStrokeDisplayableRepresentation(lastKeyStroke)); } e.consume(); } public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) {} } ================================================ FILE: src/main/java/com/mucommander/ui/text/SizeConstrainedDocument.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.text; /** * Document that can be used with java.swing.JTextField and javax.swing.JTextArea to limit * the number of characters that can be entered by the user. * * @author Maxence Bernard */ public class SizeConstrainedDocument extends javax.swing.text.PlainDocument { /** Maximum number of characters allowed */ private final int maxLen; /** * Creates a new instance of SizeConstrainedDocument, using the specified length * to limit the number of characters allowed. * * @param maxLen maximum number of characters allowed */ public SizeConstrainedDocument(int maxLen) { this.maxLen = maxLen; } @Override public void insertString(int offset, String str, javax.swing.text.AttributeSet attributeSet) throws javax.swing.text.BadLocationException { if (str != null && maxLen > 0 && this.getLength() + str.length() > maxLen) { return; } super.insertString(offset, str, attributeSet); } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ColorChangedEvent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.*; public class ColorChangedEvent { private final Theme source; private final int colorId; private final Color color; ColorChangedEvent(Theme source, int colorId, Color color) { this.source = source; this.colorId = colorId; this.color = color; } public boolean isDefaultColor() { return source == null; } public Theme getSource() { return source; } public int getColorId() { return colorId; } public Color getColor() { return color; } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ComponentMapper.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import javax.swing.*; /** * Used to provide instances of {@link JComponent} to {@link SystemDefaultColor} and {@link SystemDefaultFont}. * @author Nicolas Rinaudo */ public abstract class ComponentMapper { /** * Returns a new instance of the {@link JComponent} this instance maps. * @return a new instance of the {@link JComponent} this instance maps. */ public abstract JComponent getComponent(); } ================================================ FILE: src/main/java/com/mucommander/ui/theme/DefaultColor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.Color; import java.util.ArrayList; import java.util.List; /** * Represents a default value for a theme color. *

    * Instances of this class are used to provide default values for theme colors and notify the current theme when they * are modified. *

    * If, for example, a color should default to the look and feel defined TextArea foreground color and the look and feel * is changed, the corresponding {@link DefaultColor} instance will catch that event and notify the current theme of the * change. * * @author Nicolas Rinaudo */ public abstract class DefaultColor { /** List of colors linked to this default value. */ private final List linkedColors = new ArrayList<>(); /** * Creates a new instance of {@link DefaultColor}. */ DefaultColor() { } /** * Notifies the current theme of a default value change to all linked colors. * @param color new default color value. */ void notifyChange(Color color) { for(int i : linkedColors) { ThemeData.triggerColorEvent(i, color); } } /** * Registers a theme color as defaulting to the current instance. *

    * If the default color's value were to change, the current theme will automatically be notified of the change and * ultimately propagate to all registered theme listeners if necessary. * * @param colorId identifier of the color that uses this instance as a default value. */ public void link(Integer colorId) { linkedColors.add(colorId); } /** * Returns the color this default value represents. * @param data contains all the current theme values. * @return the color this default value represents. */ public abstract Color getColor(ThemeData data); } ================================================ FILE: src/main/java/com/mucommander/ui/theme/DefaultFont.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.Font; import java.util.ArrayList; import java.util.List; /** * Represents a default value for a theme font. *

    * Instances of this class are used to provide default values for theme fonts and notify the current theme when they * are modified. * *

    * If, for example, a font should default to the look and feel defined TextArea font and the look and feel is changed, * the corresponding {@link DefaultFont} instance will catch that event and notify the current theme of the change. * @author Nicolas Rinaudo */ public abstract class DefaultFont { // - Instance fields ----------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** List of fonts linked to this default value. */ private List linkedFonts = new ArrayList<>(); // - Event propagation --------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Registers a theme font as defaulting to the current instance. *

    * If the default font's value were to change, the current theme will automatically be notified of the change and * ultimately propagate to all registered theme listeners if necessary. * * @param fontId identifier of the font that uses this instance as a default value. */ public void link(Integer fontId) { linkedFonts.add(fontId); } /** * Notifies the current theme of a default value change to all linked fonts. * @param font new default font value. */ protected void notifyChange(Font font) { for (int i : linkedFonts) { ThemeData.triggerFontEvent(i, font); } } // - Abstract methods ---------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Returns the font this default value represents. * @param data contains all the current theme values. * @return the font this default value represents. */ public abstract Font getFont(ThemeData data); } ================================================ FILE: src/main/java/com/mucommander/ui/theme/EditorTheme.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import org.fife.ui.rsyntaxtextarea.*; import org.fife.ui.rsyntaxtextarea.Theme; import java.awt.Font; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Created on 04/01/14. */ public class EditorTheme { private org.fife.ui.rsyntaxtextarea.Theme theme; private EditorTheme() { } private EditorTheme(org.fife.ui.rsyntaxtextarea.Theme theme) { this.theme = theme; } public EditorTheme(RSyntaxTextArea textArea) { theme = new Theme(textArea); } public static EditorTheme load(InputStream in) throws IOException { return new EditorTheme(org.fife.ui.rsyntaxtextarea.Theme.load(in)); } public static EditorTheme load(InputStream in, Font baseFont) throws IOException { return new EditorTheme(org.fife.ui.rsyntaxtextarea.Theme.load(in, baseFont)); } public void apply(RSyntaxTextArea textArea) { theme.apply(textArea); } public void save(OutputStream out) throws IOException { theme.save(out); } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/FixedDefaultColor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.*; /** * {@link DefaultColor} implementation that maps to a fixed value. * @author Nicolas Rinaudo */ public class FixedDefaultColor extends DefaultColor { /** Color to default to. */ private final Color color; /** * Creates a new instance of {@link FixedDefaultColor}. * @param color color to default to. */ public FixedDefaultColor(Color color) { this.color = color; } @Override public Color getColor(ThemeData data) { return color; } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/FixedDefaultFont.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.*; /** * {@link DefaultFont} implementation that maps to a fixed value. * @author Nicolas Rinaudo */ public class FixedDefaultFont extends DefaultFont { /** Font to default to. */ private final Font font; /** * Creates a new instance of {@link FixedDefaultFont}. * @param font font to default to. */ FixedDefaultFont(Font font) { this.font = font; } @Override public Font getFont(ThemeData data) { return font; } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/FontChangedEvent.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.*; public class FontChangedEvent { private final Theme source; private final int fontId; private final Font font; FontChangedEvent(Theme source, int fontId, Font font) { this.source = source; this.fontId = fontId; this.font = font; } public boolean isDefaultFont() { return source == null; } public Theme getSource() { return source; } public int getFontId() { return fontId; } public Font getFont() { return font; } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/LinkedDefaultColor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.*; /** * {@link DefaultColor} implementation that maps to a value in the current theme. *

    * This is typically useful to make sure that a color always defaults to the same value as another theme property. * Care should be exercised when using this class, however: it doesn't check for infinite recursion, meaning it's * entirely possible to freeze muCommander by linking a color to itself as a default. * * @author Nicolas Rinaudo */ public class LinkedDefaultColor extends DefaultColor implements ThemeListener { /** Identifier of the current theme color to default to. */ private final int colorId; /** * Creates a new instance of {@link LinkedDefaultColor}. * @param colorId identifier of the current theme color to default to. */ LinkedDefaultColor(int colorId) { this.colorId = colorId; ThemeData.addDefaultValuesListener(this); } @Override public Color getColor(ThemeData data) { return data.getColor(colorId); } public void colorChanged(ColorChangedEvent event) { if (event.getColorId() == colorId) { notifyChange(event.getColor()); } } public void fontChanged(FontChangedEvent event) { } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/LinkedDefaultFont.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.*; /** * {@link DefaultFont} implementation that maps to a value in the current theme. *

    * This is typically useful to make sure that a font always defaults to the same value as another theme property. * Care should be exercised when using this class, however: it doesn't check for infinite recursion, meaning it's * entirely possible to freeze muCommander by linking a font to itself as a default. * * @author Nicolas Rinaudo */ public class LinkedDefaultFont extends DefaultFont implements ThemeListener { /** Identifier of the current theme font to default to. */ private int id; /** * Creates a new instance of {@link LinkedDefaultFont}. * @param id identifier of the current theme font to default to. */ public LinkedDefaultFont(int id) { this.id = id; } @Override public Font getFont(ThemeData data) { return data.getFont(id); } public void colorChanged(ColorChangedEvent event) { } public void fontChanged(FontChangedEvent event) { if (event.getFontId() == id) { notifyChange(event.getFont()); } } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/SystemDefaultColor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import javax.swing.*; import javax.swing.text.JTextComponent; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * {@link DefaultColor} implementation that maps to a system value. *

    * The purpose of this class is to create default colors that map, for example, to the default text area foreground * color for the current look and feel. * *

    * The mechanism used to identify the default color goes through three different stages: *

      *
    • Look for a specific property in {@link UIManager}.
    • *
    • * If this isn't found, rely on a {@link ComponentMapper} to get an instance of the target and retrieve the relevant * color. *
    • *
    • * If this is null, return a hard-coded default value. *
    • *
    * * @author Nicolas Rinaudo */ public class SystemDefaultColor extends DefaultColor implements PropertyChangeListener { // - Fallbacks ----------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** Foreground color used in case no system default could be identified. */ private static final Color DEFAULT_FOREGROUND = Color.BLACK; /** Background color used in case no system default could be identified. */ private static final Color DEFAULT_BACKGROUND = Color.WHITE; /** Selection foreground color used in case no system default could be identified. */ private static final Color DEFAULT_SELECTION_FOREGROUND = Color.WHITE; /** Selection background color used in case no system default could be identified. */ private static final Color DEFAULT_SELECTION_BACKGROUND = Color.BLUE; /** Identifies a foreground color (linked to {@link JComponent#getForeground()}). */ static final int FOREGROUND = 1; /** Identifies a background color (linked to {@link JComponent#getBackground()}). */ static final int BACKGROUND = 2; /** Identifies a selection foreground color (linked to {@link JTextComponent#getSelectedTextColor()}). */ static final int SELECTION_FOREGROUND = 3; /** Identifies a selection background color (linked to {@link JTextComponent#getSelectionColor()}). */ static final int SELECTION_BACKGROUND = 4; /** Identifies a current line background color. */ static final int CURRENT_LINE_BACKGROUND = 5; /** {@link UIManager} property to look for. */ private final String property; /** * Type of the default color (can be one of {@link #FOREGROUND}, {@link #BACKGROUND}, {@link #SELECTION_FOREGROUND} * or {@link #SELECTION_BACKGROUND}). */ private final int type; /** Current default color value. */ private Color color; /** Used to create instance of the component whose color will be retrieved (in case {@link #property} isn't set). */ private final ComponentMapper mapper; /** * Creates a new instance of {@link SystemDefaultColor}. * @param type type of the color being described (can be one of {@link #FOREGROUND}, {@link #BACKGROUND}, * {@link #SELECTION_FOREGROUND} or {@link #SELECTION_BACKGROUND}). * @param property name of the {@link UIManager} property to look for. * @param mapper component mapper to use when the {@link UIManager} property isn't set. */ SystemDefaultColor(int type, String property, ComponentMapper mapper) { UIManager.addPropertyChangeListener(this); this.property = property; this.mapper = mapper; this.type = type; } /** * Returns the color of the right {@link #type type} used by the specified component. * @param component component to analyse. * @return the color of the right {@link #type type} used by the specified component. */ private Color getColor(JComponent component) { if (type == FOREGROUND) { return component.getForeground(); } else if(type == BACKGROUND) { return component.getBackground(); } // Text component specific colors. else if (component instanceof JTextComponent comp) { if (type == SELECTION_FOREGROUND) { return comp.getSelectedTextColor(); } else if(type == SELECTION_BACKGROUND) { return comp.getSelectionColor(); } } return null; } /** * Returns the fallback color of the right {@link #type type}. * @return the fallback color of the right {@link #type type}. */ private Color getColor() { return switch (type) { case FOREGROUND -> DEFAULT_FOREGROUND; case SELECTION_FOREGROUND -> DEFAULT_SELECTION_FOREGROUND; case SELECTION_BACKGROUND -> DEFAULT_SELECTION_BACKGROUND; default -> DEFAULT_BACKGROUND; }; } @Override public Color getColor(ThemeData data) { if (color == null) { if ((color = UIManager.getColor(property)) == null) { if ((color = getColor(mapper.getComponent())) == null) { color = getColor(); } } color = new Color(color.getRGB(), (color.getRGB() & 0xFF000000) != 0xFF000000); } return color; } // - PropertyChangeListener implementation ------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName().toLowerCase(); if ("lookandfeel".equals(name) || name.equalsIgnoreCase(property)) { //color = null; Color oldColor = color; color = getColor((ThemeData)null); if (!color.equals(oldColor)) { notifyChange(color); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/SystemDefaultFont.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import javax.swing.*; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * {@link DefaultFont} implementation that maps to a system value. *

    * The purpose of this class is to create default fonts that map, for example, to the default text area font for the * current look and feel. * *

    * The mechanism used to identify the default font goes through three different stages: *

      *
    • Look for a specific property in {@link UIManager}.
    • *
    • * If this isn't found, rely on a {@link ComponentMapper} to get an instance of the target and retrieve its font. *
    • *
    • * If this is null, return a default SansSerif font. *
    • *
    * * @author Nicolas Rinaudo */ public class SystemDefaultFont extends DefaultFont implements PropertyChangeListener { /** Name of the {@link UIManager#getFont(Object)} font property} to query. */ private final String property; /** Current value of the default font. */ private Font font; /** Used to create instance of the component whose font will be retrieved (in case {@link #property} isn't set). */ private final ComponentMapper mapper; /** * Creates a new instance of {@link SystemDefaultFont}. * @param property {@link UIManager} property to query for the default font. * @param mapper component mapper to use when the {@link UIManager} property isn't set. */ SystemDefaultFont(String property, ComponentMapper mapper) { UIManager.addPropertyChangeListener(this); this.property = property; this.mapper = mapper; } @Override public Font getFont(ThemeData data) { // If the font hasn't been identified yet... if (font == null) { // ... try to retrieve it from the UIManager. font = UIManager.getFont(property); if (font == null) // If the current l&f didn't set the right property, attempt to retrieve it from a component of the desired type. font = mapper.getComponent().getFont(); if (font == null) { // If that failed, defaults to SansSerif (guaranteed to be supported by the VM). font = Font.decode("SansSerif"); } } return font; } @Override public void propertyChange(PropertyChangeEvent evt) { // Monitors changes to both the global look & feel and the target property and react to them if necessary. String name = evt.getPropertyName().toLowerCase(); if (name.equals("lookandfeel") || name.equalsIgnoreCase(property)) { Font oldFont = font; // We first set font to null to ensure that the value is refreshed. font = null; font = getFont(null); if (!font.equals(oldFont)) { notifyChange(font); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/Theme.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import com.mucommander.utils.text.Translator; import java.awt.*; import java.util.WeakHashMap; /** * @author Nicolas Rinaudo */ public class Theme extends ThemeData { // - Theme types --------------------------------------------------------------------- // ----------------------------------------------------------------------------------- public enum Type { /** Describes the user defined theme. */ USER, /** Describes predefined muCommander themes. */ PREDEFINED, /** Describes custom muCommander themes. */ CUSTOM } // - Theme listeners ----------------------------------------------------------------- // ----------------------------------------------------------------------------------- private static final WeakHashMap listeners = new WeakHashMap<>(); // - Instance variables -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Theme name. */ private String name; /** Theme type. */ private Type type; // While this field might look useless, it's actually critical for proper event notification: // ThemeData uses a weak hashmap to store its listeners, meaning that each listener must be 'linked' // somewhere or be garbage collected. Simply put, if we do not store the instance here, we might // as well not bother registering it. private DefaultValuesListener defaultValuesListener; /** * Creates a new empty user theme. */ Theme(ThemeListener listener) { super(); init(listener, Type.USER, null); } Theme(ThemeListener listener, Type type, String name) { super(); init(listener, type, name); } Theme(ThemeListener listener, ThemeData template) { super(template); init(listener, Type.USER, null); } Theme(ThemeListener listener, ThemeData template, Type type, String name) { super(template); init(listener, type, name); } private void init(ThemeListener listener, Type type, String name) { // This might seem like a roundabout way of doing things, but it's actually necessary. // If we didn't explicitly call a defaultValuesListener method, proGuard would 'optimise' // the instance out with catastrophic results (the listener would become a weak reference, // be removed by the garbage collector, and all our carefully crafted event system would // crumble). // While Theme.addDefaultValuesListener(defaultValuesListener = new DefaultValuesListener(this)); // might seem like a more compact way of doing things, it wouldn't actually work. defaultValuesListener = new DefaultValuesListener(); defaultValuesListener.setTheme(this); ThemeData.addDefaultValuesListener(defaultValuesListener); addThemeListener(listener); setType(type); if (name != null) { setName(name); } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Theme other = (Theme) obj; if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return type == other.type; } // - Data retrieval ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Checks whether this theme is modifiable. *

    * A theme is modifiable if and only if it's not a predefined theme. * * @return true if the theme is modifiable, false otherwise. */ public boolean canModify() { return type != Type.PREDEFINED; } /** * Returns the theme's type. * @return the theme's type. */ public Type getType() {return type;} /** * Returns the theme's name. * @return the theme's name. */ public String getName() { // Lazy loading for Launcher speedup if (name == null && type == Type.USER) { name = Translator.get("theme.custom_theme"); } return name; } // - Data modification --------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Sets one of the theme's fonts. *

    * Note that this method will only work if the theme is not a predifined one. Any other theme type will throw an * exception. * * @see ThemeManager#setCurrentFont(int,Font) * @param id * identifier of the font to set. * @param font * value for the specified font. * @throws IllegalStateException * thrown if the theme is a predifined one. */ @Override public boolean setFont(int id, Font font) { // Makes sure we're not trying to modify a non-user theme. if (type == Type.PREDEFINED) { throw new IllegalStateException("Trying to modify a predefined theme."); } if (super.setFont(id, font)) { // We're using getFont here to make sure that no event is propagated with a null value. triggerFontEvent(new FontChangedEvent(this, id, getFont(id))); return true; } return false; } /** * Sets one of the theme's colors. *

    * Note that this method will not work if the theme is a predefined one. Any other theme type will throw an * exception. * * @see ThemeManager#setCurrentColor(int,Color) * @param id * identifier of the color to set. * @param color * value for the specified color. * @throws IllegalStateException * thrown if the theme is a predefined one. */ @Override public boolean setColor(int id, Color color) { // Makes sure we're not trying to modify a non-user theme. if (type == Type.PREDEFINED) { throw new IllegalStateException("Trying to modify a predefined theme."); } if (super.setColor(id, color)) { // We're using getColor here to make sure that no event is propagated with a null value. triggerColorEvent(new ColorChangedEvent(this, id, getColor(id))); return true; } return false; } /** * Sets this theme's type. *

    * If type is set to {@link Type#USER}, this method will also set the * theme's name to the proper value taken from the dictionary. * * @param type theme's type. */ void setType(Type type) { checkType(type); this.type = type; if (type == Type.USER) { setName(null); // the name will be lazy loaded later after dictionaly loading } } /** * Sets this theme's name. * @param name theme's name. */ void setName(String name) { this.name = name; } static void checkType(Type type) { if (type != Type.USER && type != Type.PREDEFINED && type != Type.CUSTOM) { throw new IllegalArgumentException("Illegal theme type: " + type); } } /** * Returns the theme's name. * @return the theme's name. */ public String toString() { return getName(); } private static void addThemeListener(ThemeListener listener) { listeners.put(listener, null); } private static void removeThemeListener(ThemeListener listener) { listeners.remove(listener); } private static void triggerFontEvent(FontChangedEvent event) { for (ThemeListener listener : listeners.keySet()) { listener.fontChanged(event); } } private static void triggerColorEvent(ColorChangedEvent event) { for (ThemeListener listener : listeners.keySet()) { listener.colorChanged(event); } } private class DefaultValuesListener implements ThemeListener { private Theme theme; DefaultValuesListener() {} public void setTheme(Theme theme) {this.theme = theme;} public void colorChanged(ColorChangedEvent event) { if (!theme.isColorSet(event.getColorId())) { int colorId = event.getColorId(); Theme.triggerColorEvent(new ColorChangedEvent(theme, colorId, getColor(colorId))); } } public void fontChanged(FontChangedEvent event) { if (!theme.isFontSet(event.getFontId())) { int fontId = event.getFontId(); Theme.triggerFontEvent(new FontChangedEvent(theme, fontId, getFont(fontId))); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeCache.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import com.mucommander.ui.main.table.FileGroupResolver; import java.awt.*; import java.util.WeakHashMap; /** * Contains cached colors and fonts for current theme. * * @author Mariusz Jakubowski */ public class ThemeCache implements ThemeListener { public static final int NORMAL = 0; public static final int SELECTED = 1; public static final int ALTERNATE = 2; public static final int SECONDARY = 3; public static final int INACTIVE = 0; public static final int ACTIVE = 1; public static final int HIDDEN_FOLDER = 0; public static final int HIDDEN_FILE = 1; public static final int FOLDER = 2; public static final int ARCHIVE = 3; public static final int SYMLINK = 4; public static final int MARKED = 5; public static final int EXECUTABLE = 6; public static final int PLAIN_FILE = 7; public static Color[][][] foregroundColors; public static Color[][] backgroundColors; public static Color[] groupColors; public static Color unmatchedForeground; public static Color unmatchedBackground; public static Color activeOutlineColor; public static Color inactiveOutlineColor; public static Font tableFont; /** Theme cache instance */ public static final ThemeCache instance = new ThemeCache(); static { foregroundColors = new Color[2][2][8]; backgroundColors = new Color[2][4]; groupColors = new Color[FileGroupResolver.MAX_GROUPS]; // Active background colors. backgroundColors[ACTIVE][NORMAL] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_BACKGROUND_COLOR); backgroundColors[ACTIVE][SELECTED] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_SELECTED_BACKGROUND_COLOR); backgroundColors[ACTIVE][ALTERNATE] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_ALTERNATE_BACKGROUND_COLOR); backgroundColors[ACTIVE][SECONDARY] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR); // Inactive background colors. backgroundColors[INACTIVE][NORMAL] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_INACTIVE_BACKGROUND_COLOR); backgroundColors[INACTIVE][SELECTED] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR); backgroundColors[INACTIVE][ALTERNATE] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR); backgroundColors[INACTIVE][SECONDARY] = ThemeManager.getCurrentColor(Theme.FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR); // Normal foreground foregroundColors. foregroundColors[ACTIVE][NORMAL][HIDDEN_FOLDER] = ThemeManager.getCurrentColor(Theme.HIDDEN_FOLDER_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][HIDDEN_FILE] = ThemeManager.getCurrentColor(Theme.HIDDEN_FILE_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][FOLDER] = ThemeManager.getCurrentColor(Theme.FOLDER_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][ARCHIVE] = ThemeManager.getCurrentColor(Theme.ARCHIVE_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][SYMLINK] = ThemeManager.getCurrentColor(Theme.SYMLINK_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][MARKED] = ThemeManager.getCurrentColor(Theme.MARKED_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][EXECUTABLE] = ThemeManager.getCurrentColor(Theme.EXECUTABLE_FOREGROUND_COLOR); foregroundColors[ACTIVE][NORMAL][PLAIN_FILE] = ThemeManager.getCurrentColor(Theme.FILE_FOREGROUND_COLOR); // Normal unfocused foreground foregroundColors. foregroundColors[INACTIVE][NORMAL][HIDDEN_FOLDER] = ThemeManager.getCurrentColor(Theme.HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][HIDDEN_FILE] = ThemeManager.getCurrentColor(Theme.HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][FOLDER] = ThemeManager.getCurrentColor(Theme.FOLDER_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][ARCHIVE] = ThemeManager.getCurrentColor(Theme.ARCHIVE_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][SYMLINK] = ThemeManager.getCurrentColor(Theme.SYMLINK_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][MARKED] = ThemeManager.getCurrentColor(Theme.MARKED_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][EXECUTABLE] = ThemeManager.getCurrentColor(Theme.EXECUTABLE_INACTIVE_FOREGROUND_COLOR); foregroundColors[INACTIVE][NORMAL][PLAIN_FILE] = ThemeManager.getCurrentColor(Theme.FILE_INACTIVE_FOREGROUND_COLOR); // Selected foreground foregroundColors. foregroundColors[ACTIVE][SELECTED][HIDDEN_FOLDER] = ThemeManager.getCurrentColor(Theme.HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][HIDDEN_FILE] = ThemeManager.getCurrentColor(Theme.HIDDEN_FILE_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][FOLDER] = ThemeManager.getCurrentColor(Theme.FOLDER_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][ARCHIVE] = ThemeManager.getCurrentColor(Theme.ARCHIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][SYMLINK] = ThemeManager.getCurrentColor(Theme.SYMLINK_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][MARKED] = ThemeManager.getCurrentColor(Theme.MARKED_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][EXECUTABLE] = ThemeManager.getCurrentColor(Theme.EXECUTABLE_SELECTED_FOREGROUND_COLOR); foregroundColors[ACTIVE][SELECTED][PLAIN_FILE] = ThemeManager.getCurrentColor(Theme.FILE_SELECTED_FOREGROUND_COLOR); // Selected unfocused foreground foregroundColors. foregroundColors[INACTIVE][SELECTED][HIDDEN_FOLDER]= ThemeManager.getCurrentColor(Theme.HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][HIDDEN_FILE] = ThemeManager.getCurrentColor(Theme.HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][FOLDER] = ThemeManager.getCurrentColor(Theme.FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][ARCHIVE] = ThemeManager.getCurrentColor(Theme.ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][SYMLINK] = ThemeManager.getCurrentColor(Theme.SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][MARKED] = ThemeManager.getCurrentColor(Theme.MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][EXECUTABLE] = ThemeManager.getCurrentColor(Theme.EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR); foregroundColors[INACTIVE][SELECTED][PLAIN_FILE] = ThemeManager.getCurrentColor(Theme.FILE_INACTIVE_SELECTED_FOREGROUND_COLOR); unmatchedForeground = ThemeManager.getCurrentColor(Theme.FILE_TABLE_UNMATCHED_FOREGROUND_COLOR); unmatchedBackground = ThemeManager.getCurrentColor(Theme.FILE_TABLE_UNMATCHED_BACKGROUND_COLOR); tableFont = ThemeManager.getCurrentFont(Theme.FILE_TABLE_FONT); activeOutlineColor = ThemeManager.getCurrentColor(Theme.FILE_TABLE_SELECTED_OUTLINE_COLOR); inactiveOutlineColor = ThemeManager.getCurrentColor(Theme.FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR); for (int i = 0; i < 10; i++) { groupColors[i] = ThemeManager.getCurrentColor(Theme.FILE_GROUP_1_FOREGROUND_COLOR + i); } ThemeManager.addCurrentThemeListener(instance); } /** Listeners. */ private static final WeakHashMap listeners = new WeakHashMap<>(); private ThemeCache() { } public static void addThemeListener(ThemeListener listener) { listeners.put(listener, null); } public static void removeThemeListener(ThemeListener listener) { listeners.remove(listener); } private static void fireColorChanged(ColorChangedEvent event) { for (ThemeListener listener : listeners.keySet()) { listener.colorChanged(event); } } private static void fireFontChanged(FontChangedEvent event) { for (ThemeListener listener : listeners.keySet()) { listener.fontChanged(event); } } /** * Receives theme color changes notifications. */ @Override public void colorChanged(ColorChangedEvent event) { int colorId = event.getColorId(); if (colorId >= Theme.FILE_GROUP_1_FOREGROUND_COLOR && colorId <= Theme.FILE_GROUP_10_FOREGROUND_COLOR) { groupColors[colorId - Theme.FILE_GROUP_1_FOREGROUND_COLOR] = event.getColor(); } else { switch(colorId) { // Plain file color. case Theme.FILE_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][PLAIN_FILE] = event.getColor(); break; // Selected file color. case Theme.FILE_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][PLAIN_FILE] = event.getColor(); break; // Hidden folders. case Theme.HIDDEN_FOLDER_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][HIDDEN_FOLDER] = event.getColor(); break; // Selected hidden folders. case Theme.HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][HIDDEN_FOLDER] = event.getColor(); break; // Hidden files. case Theme.HIDDEN_FILE_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][HIDDEN_FILE] = event.getColor(); break; // Selected hidden files. case Theme.HIDDEN_FILE_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][HIDDEN_FILE] = event.getColor(); break; // Folders. case Theme.FOLDER_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][FOLDER] = event.getColor(); break; // Selected folders. case Theme.FOLDER_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][FOLDER] = event.getColor(); break; // Archives. case Theme.ARCHIVE_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][ARCHIVE] = event.getColor(); break; // Selected archives. case Theme.ARCHIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][ARCHIVE] = event.getColor(); break; // Symlinks. case Theme.SYMLINK_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][SYMLINK] = event.getColor(); break; // Selected symlinks. case Theme.SYMLINK_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][SYMLINK] = event.getColor(); break; // Marked files. case Theme.MARKED_FOREGROUND_COLOR: foregroundColors[ACTIVE][NORMAL][MARKED] = event.getColor(); break; // Selected marked files. case Theme.MARKED_SELECTED_FOREGROUND_COLOR: foregroundColors[ACTIVE][SELECTED][MARKED] = event.getColor(); break; // Plain file color. case Theme.FILE_INACTIVE_FOREGROUND_COLOR: foregroundColors[INACTIVE][NORMAL][PLAIN_FILE] = event.getColor(); break; // Selected file color. case Theme.FILE_INACTIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[INACTIVE][SELECTED][PLAIN_FILE] = event.getColor(); break; // Hidden files. case Theme.HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR: foregroundColors[INACTIVE][NORMAL][HIDDEN_FILE] = event.getColor(); break; // Selected hidden files. case Theme.HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[INACTIVE][SELECTED][HIDDEN_FILE] = event.getColor(); break; // Folders. case Theme.FOLDER_INACTIVE_FOREGROUND_COLOR: foregroundColors[INACTIVE][NORMAL][FOLDER] = event.getColor(); break; // Selected folders. case Theme.FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[INACTIVE][SELECTED][FOLDER] = event.getColor(); break; // Archives. case Theme.ARCHIVE_INACTIVE_FOREGROUND_COLOR: foregroundColors[INACTIVE][NORMAL][ARCHIVE] = event.getColor(); break; // Selected archives. case Theme.ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[INACTIVE][SELECTED][ARCHIVE] = event.getColor(); break; // Symlinks. case Theme.SYMLINK_INACTIVE_FOREGROUND_COLOR: foregroundColors[INACTIVE][NORMAL][SYMLINK] = event.getColor(); break; // Selected symlinks. case Theme.SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[INACTIVE][SELECTED][SYMLINK] = event.getColor(); break; // Marked files. case Theme.MARKED_INACTIVE_FOREGROUND_COLOR: foregroundColors[INACTIVE][NORMAL][MARKED] = event.getColor(); break; // Selected marked files. case Theme.MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR: foregroundColors[INACTIVE][SELECTED][MARKED] = event.getColor(); break; // Unmatched foreground case Theme.FILE_TABLE_UNMATCHED_FOREGROUND_COLOR: unmatchedForeground = event.getColor(); break; // Unmached background case Theme.FILE_TABLE_UNMATCHED_BACKGROUND_COLOR: unmatchedBackground = event.getColor(); break; // Active normal background. case Theme.FILE_TABLE_BACKGROUND_COLOR: backgroundColors[ACTIVE][NORMAL] = event.getColor(); break; // Active selected background. case Theme.FILE_TABLE_SELECTED_BACKGROUND_COLOR: backgroundColors[ACTIVE][SELECTED] = event.getColor(); break; // Active alternate background. case Theme.FILE_TABLE_ALTERNATE_BACKGROUND_COLOR: backgroundColors[ACTIVE][ALTERNATE] = event.getColor(); break; // Inactive normal background. case Theme.FILE_TABLE_INACTIVE_BACKGROUND_COLOR: backgroundColors[INACTIVE][NORMAL] = event.getColor(); break; // Inactive selected background. case Theme.FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR: backgroundColors[INACTIVE][SELECTED] = event.getColor(); break; // Inactive alternate background. case Theme.FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR: backgroundColors[INACTIVE][ALTERNATE] = event.getColor(); break; // Active selection outline. case Theme.FILE_TABLE_SELECTED_OUTLINE_COLOR: activeOutlineColor = event.getColor(); break; // Inactive selection outline. case Theme.FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR: inactiveOutlineColor = event.getColor(); break; // Secondary background color. case Theme.FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR: backgroundColors[ACTIVE][SECONDARY] = event.getColor(); break; // Inactive secondary background color. case Theme.FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR: backgroundColors[INACTIVE][SECONDARY] = event.getColor(); break; default: return; } } fireColorChanged(event); } /** * Receives theme font changes notifications. */ public void fontChanged(FontChangedEvent event) { if (event.getFontId() == Theme.FILE_TABLE_FONT) { tableFont = event.getFont(); } else { return; } fireFontChanged(event); } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeData.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import com.mucommander.RuntimeConstants; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.text.FontUtils; import org.fife.ui.rtextarea.RTextArea; import javax.swing.*; import java.awt.*; import java.util.Hashtable; import java.util.Map; import java.util.WeakHashMap; /** * Base class for all things Theme. *

    * The role of ThemeData is twofold:
    * - theme data storage.
    * - default values retrievals and notification.
    * *

    * In the current version, theme data is solely composed of assorted colors and fonts. ThemeData * offers methods to {@link #setColor(int,Color) set}, {@link #getColor(int) retrieve}, {@link #isIdentical(ThemeData,boolean) compare} * and {@link #cloneData() clone} these values. * *

    * One of its major constraints is that it can never return null values for the items it contains. Whenever a specific * value hasn't been set, ThemeData will seemlessly provide the rest of the world with default values retrieved from the current * look&feel. * *

    * This default values system means that theme items can change outside of anybody's control: Swing UI properties can be updated, the current * look&feel can be modified... ThemeData will track this changes and make sure that the proper event are dispatched * to listeners. * *

    * In theory, classes that use the theme API should not need to worry about default value modifications. This is already managed internally, and * if the change affects any of the themes being listened on, the event will be propagated to them. There might special cases where it's necessary, * however, for which ThemeData provides a {@link #addDefaultValuesListener(ThemeListener) listening} mechanism. * * @see Theme * @see ThemeManager * @see javax.swing.UIManager * @author Nicolas Rinaudo */ public class ThemeData implements ThemeId { // - Default fonts ------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- // The following fields are look&feel dependant values for the fonts that are used by // themes. We need to monitor them, as they are prone to change through UIManager. // - Default identifiers ------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- private static final String DEFAULT_TEXT_AREA_FOREGROUND = "TextArea.foreground"; private static final String DEFAULT_TEXT_AREA_BACKGROUND = "TextArea.background"; private static final String DEFAULT_TEXT_AREA_SELECTION_FOREGROUND = "TextArea.selectionForeground"; private static final String DEFAULT_TEXT_AREA_SELECTION_BACKGROUND = "TextArea.selectionBackground"; private static final String DEFAULT_TEXT_AREA_CURRENT_BACKGROUND = "TextArea.currentBackground"; private static final String DEFAULT_TEXT_FIELD_FOREGROUND = "TextField.foreground"; private static final String DEFAULT_TEXT_FIELD_BACKGROUND = "TextField.background"; private static final String DEFAULT_TEXT_FIELD_SELECTION_FOREGROUND = "TextField.selectionForeground"; private static final String DEFAULT_TEXT_FIELD_SELECTION_BACKGROUND = "TextField.selectionBackground"; private static final String DEFAULT_TEXT_FIELD_PROGRESS_BACKGROUND = "TextField.progress"; private static final String DEFAULT_TABLE_FOREGROUND = "Table.foreground"; private static final String DEFAULT_TABLE_BACKGROUND = "Table.background"; private static final String DEFAULT_TABLE_SELECTION_FOREGROUND = "Table.selectionForeground"; private static final String DEFAULT_TABLE_SELECTION_BACKGROUND = "Table.selectionBackground"; private static final String DEFAULT_TABLE_UNMATCHED_FOREGROUND = "Table.unmatchedForeground"; private static final String DEFAULT_TABLE_UNMATCHED_BACKGROUND = "Table.unmatchedBackground"; private static final String DEFAULT_MENU_HEADER_FOREGROUND = "MenuHeader.foreground"; private static final String DEFAULT_MENU_HEADER_BACKGROUND = "MenuHeader.background"; private static final String DEFAULT_TEXT_AREA_FONT = "TextArea.font"; private static final String DEFAULT_TEXT_FIELD_FONT = "TextField.font"; private static final String DEFAULT_LABEL_FONT = "Label.font"; private static final String DEFAULT_TABLE_FONT = "Table.font"; private static final String DEFAULT_MENU_HEADER_FONT = "MenuHeader.font"; private static final String DEFAULT_HEX_VIEWER_FONT = "HexViewer.font"; // - Listeners ----------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** Listeners on the default font and colors. */ private static final WeakHashMap listeners = new WeakHashMap<>(); // - Registered colors & fonts ------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** All registered colors. */ private static final Map COLORS = new Hashtable<>(); /** All registered default colors. */ private static final Map DEFAULT_COLORS = new Hashtable<>(); /** All registered fonts. */ private static final Map FONTS = new Hashtable<>(); /** All registered default fonts. */ private static final Map DEFAULT_FONTS = new Hashtable<>(); // - Instance variables -------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** All the colors contained by the theme. */ private Color[] colors; /** All the fonts contained by the theme. */ private Font[] fonts; private static void registerDefaultColor(String name, DefaultColor color) { DEFAULT_COLORS.put(name, color); } private static void registerDefaultFont(String name, DefaultFont font) { DEFAULT_FONTS.put(name, font); } private static void registerColor(int id, String defaultColor) { DefaultColor color = DEFAULT_COLORS.get(defaultColor); if (color == null) { throw new IllegalArgumentException("Not a registered default color: " + defaultColor); } registerColor(id, color); } private static void registerFont(int id, String defaultFont) { DefaultFont font = DEFAULT_FONTS.get(defaultFont); if (font == null) { throw new IllegalArgumentException("Not a registered default font: " + defaultFont); } registerFont(id, font); } private static void registerColor(int id, Color color) { registerColor(id, new FixedDefaultColor(color)); } private static void registerFont(int id, Font font) { registerFont(id, new FixedDefaultFont(font)); } private static void registerColor(int id, int defaultId) { registerColor(id, new LinkedDefaultColor(defaultId)); } public static void registerFont(int id, int defaultId) { registerFont(id, new LinkedDefaultFont(defaultId)); } private static void registerColor(int id, DefaultColor color) { Integer colorId = id; COLORS.put(colorId, color); color.link(colorId); } private static void registerFont(int id, DefaultFont font) { Integer fontId = id; FONTS.put(fontId, font); font.link(fontId); } static { FontUtils.setup(); // - Default values registering -------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------------------- ComponentMapper mapper = new ComponentMapper() { @Override public JComponent getComponent() { return new RTextArea(); } }; registerDefaultFont(DEFAULT_TEXT_AREA_FONT, new SystemDefaultFont("TextArea.font", mapper) { @Override public Font getFont(ThemeData data) { Font font = super.getFont(data); switch (OsFamily.getCurrent()) { case MAC_OS_X: return new Font("Menlo", font.getStyle(), font.getSize()); case LINUX: if (RuntimeConstants.DISPLAY_4K) { return new Font(font.getName(), font.getStyle(), 32); } } return font; } }); registerDefaultFont(DEFAULT_HEX_VIEWER_FONT, new SystemDefaultFont("Table.font", mapper) { @Override public Font getFont(ThemeData data) { return createDefaultHexViewerFont(); } }); registerDefaultColor(DEFAULT_TEXT_AREA_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.FOREGROUND, "TextArea.foreground", mapper)); registerDefaultColor(DEFAULT_TEXT_AREA_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.BACKGROUND, "TextArea.background", mapper)); registerDefaultColor(DEFAULT_TEXT_AREA_SELECTION_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_FOREGROUND, "TextArea.selectionForeground", mapper)); registerDefaultColor(DEFAULT_TEXT_AREA_SELECTION_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_BACKGROUND, "TextArea.selectionBackground", mapper)); registerDefaultColor(DEFAULT_TEXT_AREA_CURRENT_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.CURRENT_LINE_BACKGROUND, "TextArea.currentBackground", mapper)); // Register TextField related default values. mapper = new ComponentMapper() { @Override public JComponent getComponent() { return new JTextField(); } }; registerDefaultFont(DEFAULT_TEXT_FIELD_FONT, new SystemDefaultFont("TextField.font", mapper)); registerDefaultColor(DEFAULT_TEXT_FIELD_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.FOREGROUND, "TextField.foreground", mapper)); registerDefaultColor(DEFAULT_TEXT_FIELD_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.BACKGROUND, "TextField.background", mapper)); registerDefaultColor(DEFAULT_TEXT_FIELD_SELECTION_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_FOREGROUND, "TextField.selectionForeground", mapper)); registerDefaultColor(DEFAULT_TEXT_FIELD_SELECTION_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_BACKGROUND, "TextField.selectionBackground", mapper)); registerDefaultColor(DEFAULT_TEXT_FIELD_PROGRESS_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_BACKGROUND, "TextField.selectionBackground", mapper) { @Override public Color getColor(ThemeData data) { Color color = super.getColor(data); return new Color(color.getRed(), color.getGreen(), color.getBlue(), 64); } }); // Register Table related default values. mapper = new ComponentMapper() { @Override public JComponent getComponent() { return new JTable(); } }; registerDefaultFont(DEFAULT_TABLE_FONT, new SystemDefaultFont("Table.font", mapper)); registerDefaultColor(DEFAULT_TABLE_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.FOREGROUND, "Table.foreground", mapper)); registerDefaultColor(DEFAULT_TABLE_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.BACKGROUND, "Table.background", mapper)); registerDefaultColor(DEFAULT_TABLE_SELECTION_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_FOREGROUND, "Table.selectionForeground", mapper)); registerDefaultColor(DEFAULT_TABLE_SELECTION_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.SELECTION_BACKGROUND, "Table.selectionBackground", mapper)); registerDefaultColor(DEFAULT_TABLE_UNMATCHED_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.FOREGROUND, "Table.foreground", mapper) { @Override public Color getColor(ThemeData data) { return super.getColor(data).darker(); } }); registerDefaultColor(DEFAULT_TABLE_UNMATCHED_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.BACKGROUND, "Table.background", mapper) { @Override public Color getColor(ThemeData data) { return super.getColor(data).darker(); } }); // Menu header related default values. mapper = new ComponentMapper() { @Override public JComponent getComponent() { return new JInternalFrame(); } }; registerDefaultFont(DEFAULT_MENU_HEADER_FONT, new SystemDefaultFont("InternalFrame.font", mapper)); registerDefaultColor(DEFAULT_MENU_HEADER_BACKGROUND, new SystemDefaultColor(SystemDefaultColor.BACKGROUND, "InternalFrame.activeTitleBackground", mapper)); registerDefaultColor(DEFAULT_MENU_HEADER_FOREGROUND, new SystemDefaultColor(SystemDefaultColor.FOREGROUND, "InternalFrame.activeTitleForeground", mapper)); // Label related default values. mapper = new ComponentMapper() { @Override public JComponent getComponent() { return new JLabel(); } }; registerDefaultFont(DEFAULT_LABEL_FONT, new SystemDefaultFont("Label.font", mapper)); // - Default values linking ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------------------- // QuickList default values. registerFont(QUICK_LIST_ITEM_FONT, DEFAULT_TABLE_FONT); registerFont(QUICK_LIST_HEADER_FONT, DEFAULT_MENU_HEADER_FONT); registerColor(QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR, DEFAULT_MENU_HEADER_BACKGROUND); registerColor(QUICK_LIST_HEADER_BACKGROUND_COLOR, DEFAULT_MENU_HEADER_BACKGROUND); registerColor(QUICK_LIST_HEADER_FOREGROUND_COLOR, DEFAULT_MENU_HEADER_FOREGROUND); registerColor(QUICK_LIST_ITEM_BACKGROUND_COLOR, FILE_TABLE_BACKGROUND_COLOR); registerColor(QUICK_LIST_ITEM_FOREGROUND_COLOR, FILE_FOREGROUND_COLOR); registerColor(QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR, FILE_TABLE_SELECTED_BACKGROUND_COLOR); registerColor(QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR, FILE_SELECTED_FOREGROUND_COLOR); // File default values. registerColor(HIDDEN_FOLDER_FOREGROUND_COLOR, Color.GRAY); registerColor(HIDDEN_FILE_FOREGROUND_COLOR, Color.GRAY); registerColor(FOLDER_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(ARCHIVE_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(SYMLINK_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(FILE_INACTIVE_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR, Color.GRAY); registerColor(HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR, Color.GRAY); registerColor(FOLDER_INACTIVE_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(ARCHIVE_INACTIVE_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(SYMLINK_INACTIVE_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(FILE_FOREGROUND_COLOR, DEFAULT_TABLE_FOREGROUND); registerColor(HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(HIDDEN_FILE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(FOLDER_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(ARCHIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(SYMLINK_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(FILE_INACTIVE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); registerColor(FILE_SELECTED_FOREGROUND_COLOR, DEFAULT_TABLE_SELECTION_FOREGROUND); // FileTable default values. registerFont(FILE_TABLE_FONT, DEFAULT_TABLE_FONT); registerColor(FILE_TABLE_BACKGROUND_COLOR, DEFAULT_TABLE_BACKGROUND); registerColor(FILE_TABLE_INACTIVE_BACKGROUND_COLOR, DEFAULT_TABLE_BACKGROUND); registerColor(FILE_TABLE_ALTERNATE_BACKGROUND_COLOR, DEFAULT_TABLE_BACKGROUND); registerColor(FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR, DEFAULT_TABLE_BACKGROUND); registerColor(FILE_TABLE_SELECTED_BACKGROUND_COLOR, DEFAULT_TABLE_SELECTION_BACKGROUND); registerColor(FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR, DEFAULT_TABLE_SELECTION_BACKGROUND); registerColor(FILE_TABLE_UNMATCHED_FOREGROUND_COLOR, DEFAULT_TABLE_UNMATCHED_FOREGROUND); registerColor(FILE_TABLE_UNMATCHED_BACKGROUND_COLOR, DEFAULT_TABLE_UNMATCHED_BACKGROUND); registerColor(STATUS_BAR_BACKGROUND_COLOR, new Color(0xD5D5D5)); registerColor(MARKED_FOREGROUND_COLOR, Color.RED); registerColor(MARKED_INACTIVE_FOREGROUND_COLOR, Color.RED); registerColor(MARKED_SELECTED_FOREGROUND_COLOR, Color.RED); registerColor(MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR, Color.RED); registerColor(EXECUTABLE_FOREGROUND_COLOR, Color.GREEN); registerColor(EXECUTABLE_INACTIVE_FOREGROUND_COLOR, Color.GREEN); registerColor(EXECUTABLE_SELECTED_FOREGROUND_COLOR, Color.GREEN); registerColor(EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR, Color.GREEN); registerColor(FILE_TABLE_BORDER_COLOR, Color.GRAY); registerColor(FILE_TABLE_INACTIVE_BORDER_COLOR, Color.GRAY); registerColor(FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR, FILE_TABLE_SELECTED_BACKGROUND_COLOR); registerColor(FILE_TABLE_SELECTED_OUTLINE_COLOR, FILE_TABLE_SELECTED_BACKGROUND_COLOR); registerColor(FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR, FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR); registerColor(FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR, FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR); // File groups colors for (int i = FILE_GROUP_1_FOREGROUND_COLOR; i<= FILE_GROUP_10_FOREGROUND_COLOR; i++) { registerColor(i, DEFAULT_TABLE_FOREGROUND); } // Shell default values. registerFont(SHELL_FONT, DEFAULT_TEXT_AREA_FONT); registerFont(SHELL_HISTORY_FONT, DEFAULT_TEXT_FIELD_FONT); registerColor(SHELL_FOREGROUND_COLOR, DEFAULT_TEXT_AREA_FOREGROUND); registerColor(SHELL_BACKGROUND_COLOR, DEFAULT_TEXT_AREA_BACKGROUND); registerColor(SHELL_SELECTED_FOREGROUND_COLOR, DEFAULT_TEXT_AREA_SELECTION_FOREGROUND); registerColor(SHELL_SELECTED_BACKGROUND_COLOR, DEFAULT_TEXT_AREA_SELECTION_BACKGROUND); registerColor(SHELL_HISTORY_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(SHELL_HISTORY_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_BACKGROUND); registerColor(SHELL_HISTORY_SELECTED_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_SELECTION_FOREGROUND); registerColor(SHELL_HISTORY_SELECTED_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_SELECTION_BACKGROUND); // Terminal registerFont(TERMINAL_FONT, createDefaultTerminalFont()); registerColor(TERMINAL_BACKGROUND_COLOR, Color.BLACK); registerColor(TERMINAL_FOREGROUND_COLOR, Color.GREEN); registerColor(TERMINAL_SELECTED_BACKGROUND_COLOR, new Color(0x6666ff)); registerColor(TERMINAL_SELECTED_FOREGROUND_COLOR, Color.WHITE); // Editor default values. registerFont(EDITOR_FONT, DEFAULT_TEXT_AREA_FONT); registerColor(EDITOR_FOREGROUND_COLOR, DEFAULT_TEXT_AREA_FOREGROUND); registerColor(EDITOR_BACKGROUND_COLOR, DEFAULT_TEXT_AREA_BACKGROUND); registerColor(EDITOR_SELECTED_FOREGROUND_COLOR, DEFAULT_TEXT_AREA_SELECTION_FOREGROUND); registerColor(EDITOR_SELECTED_BACKGROUND_COLOR, DEFAULT_TEXT_AREA_SELECTION_BACKGROUND); registerColor(EDITOR_CURRENT_BACKGROUND_COLOR, DEFAULT_TEXT_AREA_CURRENT_BACKGROUND); // Location default values. registerFont(LOCATION_BAR_FONT, DEFAULT_TEXT_FIELD_FONT); registerColor(LOCATION_BAR_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(LOCATION_BAR_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_BACKGROUND); registerColor(LOCATION_BAR_SELECTED_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_SELECTION_FOREGROUND); registerColor(LOCATION_BAR_SELECTED_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_SELECTION_BACKGROUND); registerColor(LOCATION_BAR_PROGRESS_COLOR, DEFAULT_TEXT_FIELD_PROGRESS_BACKGROUND); // Status bar default values. registerFont(STATUS_BAR_FONT, DEFAULT_LABEL_FONT); registerColor(STATUS_BAR_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(STATUS_BAR_CRITICAL_COLOR, Color.RED); registerColor(STATUS_BAR_BORDER_COLOR, Color.GRAY); registerColor(STATUS_BAR_BACKGROUND_COLOR, new Color(0xD5D5D5)); registerColor(STATUS_BAR_OK_COLOR, new Color(0x70EC2B)); registerColor(STATUS_BAR_WARNING_COLOR, new Color(0xFF7F00)); // Hex viewer default values registerFont(HEX_VIEWER_FONT, DEFAULT_HEX_VIEWER_FONT); registerColor(HEX_VIEWER_HEX_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(HEX_VIEWER_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_BACKGROUND); registerColor(HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_BACKGROUND); registerColor(HEX_VIEWER_ASCII_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(HEX_VIEWER_OFFSET_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(HEX_VIEWER_SELECTED_DUMP_FOREGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); registerColor(HEX_VIEWER_SELECTED_BACKGROUND_COLOR, new Color(0x0000ff)); registerColor(HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR, DEFAULT_TEXT_FIELD_FOREGROUND); } /** * Creates an empty set of theme data. *

    * ThemeData instances created that way will return default values for every * single one of their items. * * @see #cloneData() */ public ThemeData() { colors = new Color[COLOR_COUNT]; fonts = new Font[FONT_COUNT]; } /** * Creates a new set of theme data. *

    * The content of from will be copied in the new theme data. Note that * since we're copying the arrays themselves, rather than creating new ones and copying * each color and font individually, from will be unreliable at the end of this * call. * *

    * This constructor is only meant for optimisation purposes. When transforming * theme data in a proper theme, using this constructor allows us to not duplicate * all the fonts and color. It's a risky constructor to use, however, and should not be exposed * outside of the scope of the package. * * @param from theme data from which to import values. */ ThemeData(ThemeData from) { this(); fonts = from.fonts; colors = from.colors; } // - Data import / export ------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Clones the current theme data. *

    * This method allows callers to decide whether they want to freeze default values or * not. Freezing a value means that it will be considered to have been set to the default value, * and will not be updated when this default value changes. * * @param freezeDefaults whether to freeze the data's default values. * @return a clone of the current theme data. * @see #cloneData() */ private ThemeData cloneData(boolean freezeDefaults) { ThemeData data = new ThemeData(); // Clones the theme's colors. for (int i = 0; i < COLOR_COUNT; i++) { data.colors[i] = freezeDefaults ? getColor(i) : colors[i]; } // Clones the theme's fonts. for (int i = 0; i < FONT_COUNT; i++) { data.fonts[i] = freezeDefaults ? getFont(i) : fonts[i]; } return data; } /** * Clones the theme data without freezing default values. *

    * This is a convenience method, and is exactly equivalent to calling {@link #cloneData(boolean) cloneData(false)}. * * @return a clone of the current theme data. */ public ThemeData cloneData() {return cloneData(false);} /** * Imports the specified data in the current one. *

    * This method can be dangerous in that it overwrites every single value * of the current data without hope of retrieval. Moreoever, if something were to * go wrong during the operation and an exception was raised, the current data would * find itself in an invalid state, where some of its values would have been updated but * not all of them. It is up to callers to deal with these issues. * *

    * Values overwriting is done through the use of the current instance's {@link #setColor(int,Color)} * and {@link #setFont(int,Font)} methods. This allows subclasses to plug their own code here. A good * example of that is {@link Theme}, which will automatically trigger font and color events when * importing data. * * @param data data to import. */ public void importData(ThemeData data) { // Imports the theme's colors. for (int i = 0; i < COLOR_COUNT; i++) { setColor(i, data.colors[i]); } // Imports the theme's fonts. for (int i = 0; i < FONT_COUNT; i++) { setFont(i, data.fonts[i]); } } /** * Sets the specified color to the specified value. *

    * Use a value of null to restore the color to it's default value. * *

    * This method will return false if it didn't actually change the theme data. * This is checked through the use of {@link #isColorDifferent(int,Color) isColorDifferent(}id,color). * *

    * Note that even if the color is found to be identical, the previous value will be overwritten - * this is a design choice, meant for these cases where developers need to work with home-made * subclasses of Color. * * @param id identifier of the color to set. * @param color value to which the color should be set. * @return true if the call actually changed the data, false otherwise. */ public synchronized boolean setColor(int id, Color color) { boolean buffer = isColorDifferent(id, color); // Used to store the result of isColorDifferent. colors[id] = color; if (id >= FILE_GROUP_1_FOREGROUND_COLOR && id <= FILE_GROUP_10_FOREGROUND_COLOR) { triggerColorEvent(id, color); } else { switch (id) { case FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR: case FILE_TABLE_SELECTED_OUTLINE_COLOR: case FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR: case FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR: triggerColorEvent(id, color); } } return buffer; } void setColorFast(int id, Color color) { colors[id] = color; } /** * Sets the specified font to the specified value. *

    * Use a value of null to restore the font to it's default value. * *

    * This method will return false if it didn't actually change the theme data. * This is checked through the use of {@link #isFontDifferent(int,Font) isFontDifferent(}id, font). * *

    * Note that even if the font is found to be identical, the previous value will be overwritten - * this is a design choice, meant for these cases where developers need to work with home-made * subclasses of Font. * * @param id identifier of the font to set. * @param font value to which the font should be set. * @return true if the call actually changed the data, false otherwise. */ public synchronized boolean setFont(int id, Font font) { boolean buffer = isFontDifferent(id, font); // Used to store the result of isFontDifferent. fonts[id] = font; return buffer; } void setFontFast(int id, Font font) { fonts[id] = font; } // - Items retrieval ----------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Returns the requested color. *

    * If the requested color wasn't set, its default value will be returned. * * @param id identifier of the color to retrieve. * @return the requested color, or its default value if not set. * @see #getDefaultColor(int,ThemeData) * @see #isColorSet(int) */ public synchronized Color getColor(int id) { checkColorIdentifier(id); return (colors[id] == null) ? getDefaultColor(id, this) : colors[id]; } /** * Returns the requested font. *

    * If the requested font wasn't set, its default value will be returned. * * @param id identifier of the font to retrieve. * @return the requested font, or its default value if not set. * @see #getDefaultFont(int, ThemeData) * @see #isFontSet(int) */ public synchronized Font getFont(int id) { checkFontIdentifier(id); return fonts[id] == null ? getDefaultFont(id, this) : fonts[id]; } /** * Returns true if the specified color is set. * @param id identifier of the color to check for. * @return true if the specified color is set, false otherwise. * @see #getDefaultColor(int,ThemeData) */ boolean isColorSet(int id) { return colors[id] != null; } /** * Returns true if the specified font is set. * @param id identifier of the font to check for. * @return true if the specified font is set, false otherwise. * @see #getDefaultFont(int, ThemeData) */ boolean isFontSet(int id) { return fonts[id] != null; } /** * Returns the default value for the specified color. *

    * Default values are look&feel dependant, and are subject to change during the application's * life time.
    * Classes that need to monitor such changes can register themselves using {@link #addDefaultValuesListener(ThemeListener)}. * * @param id identifier of the color whose default value should be retrieved. * @param data theme data from which to retrieve default values. * @return the default value for the specified color. * @see #addDefaultValuesListener(ThemeListener) */ private static Color getDefaultColor(int id, ThemeData data) { // Makes sure id is a legal color identifier. checkColorIdentifier(id); return COLORS.get(id).getColor(data); } /** * Returns the default value for the specified font. *

    * Default values are look&feel dependant, and are subject to change during the application's * life time.
    * Classes that need to monitor such changes can register themselves using {@link #addDefaultValuesListener(ThemeListener)}. * * @param id identifier of the font whose default value should be retrieved. * @return the default value for the specified font. * @see #addDefaultValuesListener(ThemeListener) */ private static Font getDefaultFont(int id, ThemeData data) { checkFontIdentifier(id); return FONTS.get(id).getFont(data); } // - Comparison ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Returns true if the specified data and the current one are identical. *

    * Comparisons is done by calling {@link #isFontDifferent(int,Font,boolean)} and {@link #isColorDifferent(int,Color,boolean)} * on every font and color. Refer to the documentation of these methods for more information on using the ignoreDefaults * parameter. * * @param data data against which to compare. * @param ignoreDefaults whether to compare default values. * @return true if the specified data and the current one are identical, false otherwise. * @see #isFontDifferent(int,Font,boolean) * @see #isColorDifferent(int,Color,boolean) */ private boolean isIdentical(ThemeData data, boolean ignoreDefaults) { // Compares the colors. for (int i = 0; i < COLOR_COUNT; i++) { if (isColorDifferent(i, data.colors[i], ignoreDefaults)) { return false; } } // Compares the fonts. for (int i = 0; i < FONT_COUNT; i++) { if (isFontDifferent(i, data.fonts[i], ignoreDefaults)) { return false; } } return true; } /** * Returns true if the current data is identical to the specified one, using default values when items haven't been set. *

    * This is a convenience method, and is strictly equivalent to calling {@link #isIdentical(ThemeData,boolean) isIdentical(data, false)}. * * @param data data against which to compare. * @return true if the specified data and the current one are identical, false otherwise. */ public boolean isIdentical(ThemeData data) {return isIdentical(data, false);} /** * Checks whether the current font and the specified one are different from one another. *

    * This is a convenience method, and is stricly equivalent to calling * {@link #isFontDifferent(int,Font,boolean) isFontDifferent(}id, font, false). * * @param id identifier of the font to check. * @param font font to check. * @return true if font is different from the one defined in the data. * @see #isFontDifferent(int,Font,boolean) * @see #isColorDifferent(int,Color) */ boolean isFontDifferent(int id, Font font) {return isFontDifferent(id, font, false);} /** * Checks whether the current font and the specified one are different from one another. *

    * Setting ignoreDefaults to false will compare both fonts from a 'user' point of view: comparison * will be done on the values that are used by the rest of the application. It might however be necessary to consider * fonts to be different if one is set but not the other. This can be achieved by setting ignoreDefaults to true. * * @param id identifier of the font to check. * @param font font to check. * @param ignoreDefaults whether to ignore defaults if the requested item doesn't have a value. * @return true if font is different from the one defined in the data. * @see #isFontDifferent(int,Font) * @see #isColorDifferent(int,Color) */ private synchronized boolean isFontDifferent(int id, Font font, boolean ignoreDefaults) { checkFontIdentifier(id); // If the specified font is null, the only way for both fonts to be equal is for fonts[id] // to be null as well. if (font == null) { return fonts[id] != null; } // If fonts[id] is null and we're set to ignore defaults, both fonts are different. // If we're set to use defaults, we must compare font and the default value for id. if (fonts[id] == null) { return ignoreDefaults || !getDefaultFont(id, this).equals(font); } // 'Standard' case: both fonts are set, compare them normally. return !font.equals(fonts[id]); } /** * Checks whether the current color and the specified one are different from one another. *

    * This is a convenience method, and is strictly equivalent to calling * {@link #isColorDifferent(int,Color,boolean) isColorDifferent(}id, color, false). * * @param id identifier of the color to check. * @param color color to check. * @return true if color is different from the one defined in the data. * @see #isColorDifferent(int,Color,boolean) * @see #isFontDifferent(int,Font) */ public boolean isColorDifferent(int id, Color color) {return isColorDifferent(id, color, false);} /** * Checks whether the current color and the specified one are different from one another. *

    * Setting ignoreDefaults to false will compare both colors from a 'user' point of view: comparison * will be done on the values that are used by the rest of the application. It might however be necessary to consider * colors to be different if one is set but not the other. This can be achieved by setting ignoreDefaults to true. * * @param id identifier of the color to check. * @param color color to check. * @param ignoreDefaults whether to ignore defaults if the requested item doesn't have a value. * @return true if color is different from the one defined in the data. * @see #isColorDifferent(int,Color) * @see #isFontDifferent(int,Font) */ private synchronized boolean isColorDifferent(int id, Color color, boolean ignoreDefaults) { checkColorIdentifier(id); // If the specified color is null, the only way for both colors to be equal is for colors[id] // to be null as well. if (color == null) return colors[id] != null; // If colors[id] is null and we're set to ignore defaults, both colors are different. // If we're set to use defaults, we must compare color and the default value for id. if (colors[id] == null) return ignoreDefaults || !getDefaultColor(id, this).equals(color); // 'Standard' case: both colors are set, compare them normally. return !color.equals(colors[id]); } // - Theme events -------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Registers the specified theme listener. *

    * The listener will receive {@link FontChangedEvent font} and {@link ColorChangedEvent color} events whenever * one of the default values has been changed, by a modification to the current look&feel for example. * *

    * It is not necessary for 'themable' components to listen to default values, as they are automatically propagated * through {@link Theme} and {@link ThemeManager}. * *

    * Note that listeners are stored as weak references, to make sure that the API doesn't keep ghost copies of objects * whose usefulness is long since past. This forces callers to make sure they keep a copy of the listener's instance: if * they do not, the instance will be weakly linked and garbage collected out of existence. * * @param listener theme listener to register. * @see #removeDefaultValuesListener(ThemeListener) */ static void addDefaultValuesListener(ThemeListener listener) { listeners.put(listener, null); } /** * Removes the specified instance from the list of registered theme listeners. *

    * Note that since listeners are stored as weak references, calling this method is not strictly necessary. As soon * as a listener instance is not referenced anymore, it will automatically be caught and destroyed by the garbage * collector. * * @param listener instance to remove from the list of registered theme listeners. * @see #addDefaultValuesListener(ThemeListener) */ private static void removeDefaultValuesListener(ThemeListener listener) { listeners.remove(listener); } /** * Dispatches a {@link FontChangedEvent} to all registered listeners. * @param id identifier of the font that changed. * @param font new value for the font that changed. */ static void triggerFontEvent(int id, Font font) { // Creates the event. FontChangedEvent event = new FontChangedEvent(null, id, font); // Event that will be dispatched. // Dispatches it. for (ThemeListener listener : listeners.keySet()) { listener.fontChanged(event); } } /** * Dispatches a {@link ColorChangedEvent} to all registered listeners. * @param id identifier of the color that changed. * @param color new value for the color that changed. */ static void triggerColorEvent(int id, Color color) { ColorChangedEvent event = new ColorChangedEvent(null, id, color); // Dispatches it. for (ThemeListener listener : listeners.keySet()) { listener.colorChanged(event); } } // - Helper methods ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Checks whether the specified color identifier is legal. * @param id identifier to check against. * @throws IllegalArgumentException if id is not a legal color identifier. */ private static void checkColorIdentifier(int id) { if (id < 0 || id >= COLOR_COUNT) { throw new IllegalArgumentException("Illegal color identifier: " + id); } } /** * Checks whether the specified font identifier is legal. * @param id identifier to check against. * @throws IllegalArgumentException if id is not a legal font identifier. */ private static void checkFontIdentifier(int id) { if (id < 0 || id >= FONT_COUNT) { throw new IllegalArgumentException("Illegal font identifier: " + id); } } private static Font createDefaultTerminalFont() { String name; switch (OsFamily.getCurrent()) { case WINDOWS: name = "Consolas"; break; case MAC_OS_X: return new Font("Menlo", Font.PLAIN, 14); case LINUX: int size = RuntimeConstants.DISPLAY_4K ? 32 : 14; return new Font("Monospaced", Font.PLAIN, size); default: name = "Monospaced"; } return new Font(name, Font.PLAIN, 14); } private static Font createDefaultHexViewerFont() { int size = OsFamily.getCurrent() == OsFamily.LINUX && RuntimeConstants.DISPLAY_4K ? 28 : 14; return new Font("Monospaced", Font.PLAIN, size); } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeId.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; /** * Created on 09/02/17. * @author Oleg Trifonov */ public interface ThemeId { // - Dirty hack ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- // This is an effort to make the ThemeData class a bit easier to maintain, but I'm the first // to admit it's rather dirty. // // For optimization reasons, we're storing the fonts and colors in arrays, using their // identifiers as indexes in the array. This, however, means that lots of bits of code // must be updated whenever a font or color is added or removed. The probability of // someone forgetting this is, well, 100%. // // For this reason, we've declared the number of font and colors as constants. // People are still going to forget to update these constants, but at least it'll be // a lot easier to fix. // - Font definitions ---------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Font used in the folder panels. *

    * This defaults to the current JTable font. */ int FILE_TABLE_FONT = 0; /** * Font used to display shell output. *

    * This defaults to the current JTextArea font. */ int SHELL_FONT = 1; /** * Font used in the file editor and viewer. *

    * This defaults to the current JTable font. */ int EDITOR_FONT = 2; /** * Font used in the location bar. *

    * This defaults to the current JTextField font. */ int LOCATION_BAR_FONT = 3; /** * Font used in the shell history widget. *

    * This defaults to the current JTextField font. */ int SHELL_HISTORY_FONT = 4; /** * Font used in the status bar. *

    * This defaults to the current JLabel font. */ int STATUS_BAR_FONT = 5; /** * Font used in the quick list header. *

    * This defaults to a similar font of the current JTable font, but a little bigger. */ int QUICK_LIST_HEADER_FONT = 6; /** * Font used in the quick list item. *

    * This defaults to the current JTable font. */ int QUICK_LIST_ITEM_FONT = 7; int TERMINAL_FONT = 8; /** * Font used in the file editor and viewer. *

    * This defaults to the current JTable font. */ int HEX_VIEWER_FONT = 9; /** * Number of known fonts. *

    * Since font identifiers are contiguous, it is possible to explore all fonts contained * by an instance of theme data by looping from 0 to this value. */ int FONT_COUNT = 10; // - Color definitions --------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Color used to paint the folder panels' borders. *

    * This defaults to Color.GRAY. */ int FILE_TABLE_BORDER_COLOR = 0; /** * Color used to paint the folder panels' borders when it doesn't have the focus. *

    * This defaults to Color.GRAY. */ int FILE_TABLE_INACTIVE_BORDER_COLOR = 1; /** * Color used to paint the folder panel's background color. *

    * This defaults to the current JTable background color. */ int FILE_TABLE_BACKGROUND_COLOR = 2; /** * Color used to paint the folder panel's alternate background color. *

    * This defaults to the current JTable background color. */ int FILE_TABLE_ALTERNATE_BACKGROUND_COLOR = 3; /** * Color used to paint the folder panel's background color when it doesn't have the focus. *

    * This behaves in exactly the same fashion as {@link #FILE_TABLE_BACKGROUND_COLOR}, and defaults * to the same value. */ int FILE_TABLE_INACTIVE_BACKGROUND_COLOR = 4; /** * Color used to paint the folder panel's alternate background color when inactive. *

    * This defaults to the current JTable background color. */ int FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR = 5; /** * Color used to paint the file table's background color when it's part of an unmatched file. */ int FILE_TABLE_UNMATCHED_BACKGROUND_COLOR = 6; /** * Color used to paint the file table's foreground color when it's part of an unmatched file. */ int FILE_TABLE_UNMATCHED_FOREGROUND_COLOR = 7; /** * Color used to paint the file table's background color when in a selected row. */ int FILE_TABLE_SELECTED_BACKGROUND_COLOR = 8; /** * Color used to paint the gradient of the file table's selection. */ int FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR = 9; /** * Color used to paint the gradient of the file table's selection when inactive. */ int FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR = 10; /** * Colors used to pain the file table's background color when in an inactive selected row. */ int FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR = 11; /** * Color used to paint hidden files text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int HIDDEN_FOLDER_FOREGROUND_COLOR = 12; /** * Color used to paint hidden files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #HIDDEN_FILE_FOREGROUND_COLOR}, and defaults * to the same value. */ int HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR = 13; /** * Color used to paint selected hidden files text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR = 14; /** * Color used to paint selected hidden files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #HIDDEN_FILE_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR = 15; /** * Color used to paint hidden files text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int HIDDEN_FILE_FOREGROUND_COLOR = 16; /** * Color used to paint hidden files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #HIDDEN_FILE_FOREGROUND_COLOR}, and defaults * to the same value. */ int HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR = 17; /** * Color used to paint selected hidden files text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int HIDDEN_FILE_SELECTED_FOREGROUND_COLOR = 18; /** * Color used to paint selected hidden files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #HIDDEN_FILE_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR = 19; /** * Color used to paint folders text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int FOLDER_FOREGROUND_COLOR = 20; /** * Color used to paint folders text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #FOLDER_FOREGROUND_COLOR}, and defaults * to the same value. */ int FOLDER_INACTIVE_FOREGROUND_COLOR = 21; /** * Color used to paint selected folders text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int FOLDER_SELECTED_FOREGROUND_COLOR = 22; /** * Color used to paint selected folders text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #FOLDER_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR = 23; /** * Color used to paint archives text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int ARCHIVE_FOREGROUND_COLOR = 24; /** * Color used to paint archives text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #ARCHIVE_FOREGROUND_COLOR}, and defaults * to the same value. */ int ARCHIVE_INACTIVE_FOREGROUND_COLOR = 25; /** * Color used to paint selected archives text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int ARCHIVE_SELECTED_FOREGROUND_COLOR = 26; /** * Color used to paint selected archives text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #ARCHIVE_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR = 27; /** * Color used to paint symlinks text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int SYMLINK_FOREGROUND_COLOR = 28; /** * Color used to paint symlinks text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #SYMLINK_FOREGROUND_COLOR}, and defaults * to the same value. */ int SYMLINK_INACTIVE_FOREGROUND_COLOR = 29; /** * Color used to paint selected symlinks text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int SYMLINK_SELECTED_FOREGROUND_COLOR = 30; /** * Color used to paint selected symlinks text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #SYMLINK_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR = 31; /** * Color used to paint marked files text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int MARKED_FOREGROUND_COLOR = 32; /** * Color used to paint marked files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #MARKED_FOREGROUND_COLOR}, and defaults * to the same value. */ int MARKED_INACTIVE_FOREGROUND_COLOR = 33; /** * Color used to paint selected marked files text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int MARKED_SELECTED_FOREGROUND_COLOR = 34; /** * Color used to paint selected marked files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #MARKED_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR = 35; /** * Color used to paint plain files text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int FILE_FOREGROUND_COLOR = 36; /** * Color used to paint plain files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #FILE_FOREGROUND_COLOR}, and defaults * to the same value. */ int FILE_INACTIVE_FOREGROUND_COLOR = 37; /** * Color used to paint selected plain files text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int FILE_SELECTED_FOREGROUND_COLOR = 38; /** * Color used to paint selected plain files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #FILE_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int FILE_INACTIVE_SELECTED_FOREGROUND_COLOR = 39; /** * Color used to paint shell commands output. *

    * This defaults to the current JTextArea foreground color. */ int SHELL_FOREGROUND_COLOR = 40; /** * Color used to paint the background of shell commands output. *

    * This defaults to the current JTextArea background color. */ int SHELL_BACKGROUND_COLOR = 41; /** * Color used to paint shell commands output when selected. *

    * This defaults to the current JTextArea selection foreground color. */ int SHELL_SELECTED_FOREGROUND_COLOR = 42; /** * Color used to paint the background of shell commands output when selected. *

    * This defaults to the current JTextArea selection background color. */ int SHELL_SELECTED_BACKGROUND_COLOR = 43; /** * Color used to paint the shell history's text. *

    * This defaults to the current JTextField foreground color. */ int SHELL_HISTORY_FOREGROUND_COLOR = 44; /** * Color used to paint the shell history's background. *

    * This defaults to the current JTextField background color. */ int SHELL_HISTORY_BACKGROUND_COLOR = 45; /** * Color used to paint the shell history's text when selected. *

    * This defaults to the current JTextField selection foreground color. */ int SHELL_HISTORY_SELECTED_FOREGROUND_COLOR = 46; /** * Color used to paint the shell history's background when selected. *

    * This defaults to the current JTextField selection background color. */ int SHELL_HISTORY_SELECTED_BACKGROUND_COLOR = 47; /** * Color used to paint the file editor / viewer's text. *

    * This defaults to the current JTextArea foreground color. */ int EDITOR_FOREGROUND_COLOR = 48; /** * Color used to paint the file editor / viewer's background. *

    * This defaults to the current JTextArea background color. */ int EDITOR_BACKGROUND_COLOR = 49; /** * Color used to paint the file editor / viewer's foreground when selected. *

    * This defaults to the current JTextArea selection foreground color. */ int EDITOR_SELECTED_FOREGROUND_COLOR = 50; /** * Color used to paint the file editor / viewer's background when selected. *

    * This defaults to the current JTextArea selection background color. */ int EDITOR_SELECTED_BACKGROUND_COLOR = 51; /** * Color used to paint the location's bar text. *

    * This defaults to the current JTextField foreground color. */ int LOCATION_BAR_FOREGROUND_COLOR = 52; /** * Color used to paint the location's bar background. *

    * This defaults to the current JTextField background color. */ int LOCATION_BAR_BACKGROUND_COLOR = 53; /** * Color used to paint the location's bar text when selected. *

    * This defaults to the current JTextField selection foreground color. */ int LOCATION_BAR_SELECTED_FOREGROUND_COLOR = 54; /** * Color used to paint the location's bar background when selected. *

    * This defaults to the current JTextField selection background color. */ int LOCATION_BAR_SELECTED_BACKGROUND_COLOR = 55; /** * Color used to paint the location's bar background when used as a progress bar. *

    * Note that this color is painted over the location's bar background and foreground. In order * for anything to be visible under it, it needs to have an alpha transparency component. *

    * This defaults to the current JTextField selection background color, with an * alpha transparency value of 64. */ int LOCATION_BAR_PROGRESS_COLOR = 56; /** * Color used to paint the status bar's text. *

    * This defaults to the current JLabel foreground color. */ int STATUS_BAR_FOREGROUND_COLOR = 57; /** * Color used to paint the status bar's background *

    * This defaults to the current JLabel background color. */ int STATUS_BAR_BACKGROUND_COLOR = 58; /** * Color used to paint the status bar's border. *

    * This defaults to Color.GRAY. */ int STATUS_BAR_BORDER_COLOR = 59; /** * Color used to paint the status bar's drive usage color when there's plenty of space left. *

    * This defaults to 0x70EC2B. */ int STATUS_BAR_OK_COLOR = 60; /** * Color used to paint the status bar's drive usage color when there's an average amount of space left. *

    * This defaults to 0xFF7F00. */ int STATUS_BAR_WARNING_COLOR = 61; /** * Color used to paint the status bar's drive usage color when there's dangerously little space left. *

    * This defaults to Color.RED. */ int STATUS_BAR_CRITICAL_COLOR = 62; /** * Color used to paint the outline of selected files. */ int FILE_TABLE_SELECTED_OUTLINE_COLOR = 63; /** * Color used to paint the outline of selected files in an inactive table. */ int FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR = 64; /** * Color used to paint the main background of a quick list header. */ int QUICK_LIST_HEADER_BACKGROUND_COLOR = 65; /** * Color used to paint the secondary background of a quick list header. */ int QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR = 66; /** * Color used to paint the text of a quick list header. */ int QUICK_LIST_HEADER_FOREGROUND_COLOR = 67; /** * Color used to paint the background of a quick list item. */ int QUICK_LIST_ITEM_BACKGROUND_COLOR = 68; /** * Color used to paint the text of a quick list item. */ int QUICK_LIST_ITEM_FOREGROUND_COLOR = 69; /** * Color used to paint the background of a selected quick list item. */ int QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR = 70; /** * Color used to paint the text of a selected quick list item. */ int QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR = 71; /** * Color used to paint current line in the file editor / viewer's background when selected. *

    * This defaults to the current RTextArea selection background color */ int EDITOR_CURRENT_BACKGROUND_COLOR = 72; int FILE_GROUP_1_FOREGROUND_COLOR = 73; // static final int FILE_GROUP_2_FOREGROUND_COLOR = 74; // static final int FILE_GROUP_3_FOREGROUND_COLOR = 75; // static final int FILE_GROUP_4_FOREGROUND_COLOR = 76; // static final int FILE_GROUP_5_FOREGROUND_COLOR = 77; // static final int FILE_GROUP_6_FOREGROUND_COLOR = 78; // static final int FILE_GROUP_7_FOREGROUND_COLOR = 79; // static final int FILE_GROUP_8_FOREGROUND_COLOR = 80; // static final int FILE_GROUP_9_FOREGROUND_COLOR = 81; int FILE_GROUP_10_FOREGROUND_COLOR = 82; int TERMINAL_BACKGROUND_COLOR = 83; int TERMINAL_FOREGROUND_COLOR = 84; int TERMINAL_SELECTED_FOREGROUND_COLOR = 85; int TERMINAL_SELECTED_BACKGROUND_COLOR = 86; /** * Color used to paint plain files text in the folder panels. *

    * This defaults to the current JTable foreground color. */ int EXECUTABLE_FOREGROUND_COLOR = 87; /** * Color used to paint plain files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #FILE_FOREGROUND_COLOR}, and defaults * to the same value. */ int EXECUTABLE_INACTIVE_FOREGROUND_COLOR = 88; /** * Color used to paint selected plain files text in the folder panels. *

    * This defaults to the current JTable selection foreground color. */ int EXECUTABLE_SELECTED_FOREGROUND_COLOR = 89; /** * Color used to paint selected plain files text in the folder panels when they don't have the focus. *

    * This behaves in exactly the same fashion as {@link #FILE_SELECTED_FOREGROUND_COLOR}, and defaults * to the same value. */ int EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR = 90; /** * Color used to paint the hex viewer dump's. */ int HEX_VIEWER_HEX_FOREGROUND_COLOR = 91; /** * Color used to paint the hex viewer background. */ int HEX_VIEWER_BACKGROUND_COLOR = 92; /** * Color used to paint the hex viewer alternate background. */ int HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR = 93; /** * Color used to paint the hex viewer ascii's text. */ int HEX_VIEWER_ASCII_FOREGROUND_COLOR = 94; /** * Color used to paint the hex viewer offset's text. */ int HEX_VIEWER_OFFSET_FOREGROUND_COLOR = 95; /** * Color used to paint the hex viewer's dump foreground when selected. */ int HEX_VIEWER_SELECTED_DUMP_FOREGROUND_COLOR = 96; /** * Color used to paint the hex viewer's background when selected. */ int HEX_VIEWER_SELECTED_BACKGROUND_COLOR = 97; /** * Color used to paint the hex viewer's ascii foreground when selected. */ int HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR = 98; /** * Number of known colors. *

    * Since color identifiers are contiguous, it is possible to explore all colors contained * by an instance of theme data by looping from 0 to this color. */ int COLOR_COUNT = 99; } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeListener.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; /** * Implementations of this interface can listen to changes in the current theme. * @author Nicolas Rinaudo */ public interface ThemeListener { /** * Notifies the listener that a color has been changed. */ void colorChanged(ColorChangedEvent event); /** * Notifies the listener that a font has been changed. */ void fontChanged(FontChangedEvent event); } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeManager.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.Color; import java.awt.Font; import java.io.*; import java.util.*; import com.mucommander.commons.file.util.PathUtils; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import com.mucommander.PlatformManager; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.commons.file.util.ResourceLoader; import com.mucommander.commons.io.StreamUtils; import com.mucommander.commons.util.StringUtils; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.io.backup.BackupInputStream; import com.mucommander.io.backup.BackupOutputStream; import com.mucommander.utils.text.Translator; import com.mucommander.ui.theme.Theme.Type; /** * Offers methods for accessing and modifying themes. * @author Nicolas Rinaudo */ @Slf4j public class ThemeManager { // - Class variables ----------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Path to the user defined theme file. */ private static AbstractFile userThemeFile; /** Default user defined theme file name. */ private static final String USER_THEME_FILE_NAME = "user_theme.xml"; /** Path to the custom themes repository. */ private static final String CUSTOM_THEME_FOLDER = "themes"; /** List of all registered theme change listeners. */ private static final WeakHashMap listeners = new WeakHashMap<>(); /** List of all predefined theme names. */ private static List predefinedThemeNames; /** List of all predefined syntax highlight theme names. */ private static List predefinedSyntaxThemeNames; // - Instance variables -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Whether the user theme was modified. */ private static boolean wasUserThemeModified; /** Theme that is currently applied to muCommander. */ @Getter private static Theme currentTheme; /** Used to listen on the current theme's modifications. */ private static final ThemeListener listener = new CurrentThemeListener(); /** Theme that is currently applied to viewer and editor. */ @Getter private static String currentSyntaxThemeName; private ThemeManager() {} /** * Loads the current theme. *

    * This method goes through the following steps: *

      *
    • Try to load the theme defined in the configuration.
    • *
    • If that failed, try to load the default theme.
    • *
    • If that failed, try to load the user theme if that hasn't been tried yet.
    • *
    • If that failed, use an empty theme.
    • *
    */ public static void loadCurrentTheme() { Theme.Type type; // Current theme's type. String name; // Current theme's name. boolean wasUserThemeLoaded; // Whether we have tried loading the user theme or not. // Loads the current theme type as defined in configuration. try { type = getThemeTypeFromLabel(TcConfigurations.getPreferences().getVariable(TcPreference.THEME_TYPE, TcPreferences.DEFAULT_THEME_TYPE)); } catch(Exception e) { log.error("Theme type error", e); type = getThemeTypeFromLabel(TcPreferences.DEFAULT_THEME_TYPE); } // Loads the current theme name as defined in configuration. if (type != Theme.Type.USER) { wasUserThemeLoaded = false; name = TcConfigurations.getPreferences().getVariable(TcPreference.THEME_NAME, TcPreferences.DEFAULT_THEME_NAME); } else { name = null; wasUserThemeLoaded = true; } // If the current theme couldn't be loaded, uses the default theme as defined in the configuration. currentTheme = null; try { currentTheme = readTheme(type, name); } catch(Exception e1) { log.error("Can't read theme", e1); type = getThemeTypeFromLabel(TcPreferences.DEFAULT_THEME_TYPE); name = TcPreferences.DEFAULT_THEME_NAME; if (type == Theme.Type.USER) { wasUserThemeLoaded = true; } // If the default theme can be loaded, tries to load the user theme if we haven't done so yet. // If we have, or if it fails, defaults to an empty user theme. try { currentTheme = readTheme(type, name); } catch(Exception e2) { if (!wasUserThemeLoaded) { try { currentTheme = readTheme(Theme.Type.USER, null); } catch(Exception e3) { log.error("Can't read theme", e3); } } if (currentTheme == null) { currentTheme = new Theme(listener); wasUserThemeModified = true; } } setConfigurationTheme(currentTheme); } currentSyntaxThemeName = TcConfigurations.getPreferences().getVariable(TcPreference.SYNTAX_THEME_NAME, TcPreferences.DEFAULT_SYNTAX_THEME_NAME); } // - Themes access ------------------------------------------------------------------- // ----------------------------------------------------------------------------------- private static Iterator predefinedThemeNames() { // The list of predefined themes is no longer dynamically created as this causes Webstart to retrieve and // explore the application's JAR via HTTP, which is inefficient and prevents the application from being // launched offline. if (predefinedThemeNames == null) { try { predefinedThemeNames = getThemeNames(ResourceLoader.getRootPackageAsFile(ThemeManager.class).getChild(PathUtils.removeLeadingSeparator(RuntimeConstants.THEMES_PATH, "/"))); } catch (IOException e) { predefinedThemeNames = new ArrayList<>(); } } return predefinedThemeNames.iterator(); } public static List predefinedSyntaxThemeNames() { // The list of predefined themes is no longer dynamically created as this causes Webstart to retrieve and // explore the application's JAR via HTTP, which is inefficient and prevents the application from being // launched offline. if (predefinedSyntaxThemeNames == null) { try { predefinedSyntaxThemeNames = getThemeNames(ResourceLoader.getRootPackageAsFile(ThemeManager.class).getChild(PathUtils.removeLeadingSeparator(RuntimeConstants.TEXT_SYNTAX_THEMES_PATH, "/"))); } catch (IOException e) { predefinedSyntaxThemeNames = new ArrayList<>(); } } return predefinedSyntaxThemeNames; } private static Iterator customThemeNames() throws IOException { return getThemeNames(FileFactory.getFile(getCustomThemesFolder().getAbsolutePath())).iterator(); } private static List getThemeNames(AbstractFile themeFolder) { try { AbstractFile[] files = themeFolder.ls(new ExtensionFilenameFilter(".xml")); List names = new ArrayList<>(); for (AbstractFile file : files) names.add(getThemeName(file)); return names; } catch(Exception e) { return new ArrayList<>(); } } private static List getAvailableThemes() { Iterator iterator; List themes = new ArrayList<>(); // Tries to load the user theme. If it's corrupt, uses an empty user theme. try { themes.add(readTheme(Theme.Type.USER, null)); } catch(Exception e) { themes.add(new Theme(listener)); } // Loads predefined themes. iterator = predefinedThemeNames(); while (iterator.hasNext()) { String name = iterator.next(); try { themes.add(readTheme(Theme.Type.PREDEFINED, name)); } catch(Exception e) { log.warn("Failed to load predefined theme " + name, e); } } // Loads custom themes. try { iterator = customThemeNames(); while (iterator.hasNext()) { String name = iterator.next(); try { themes.add(readTheme(Theme.Type.CUSTOM, name)); } catch(Exception e) { log.warn("Failed to load custom theme " + name, e); } } } catch(Exception e) { log.warn("Failed to load custom themes", e); } // Sorts the themes by name. themes.sort(Comparator.comparing(Theme::getName)); return themes; } private static List getAvailableThemeNames() { List themes = new ArrayList<>(); // Adds the user theme name. themes.add(Translator.get("theme.custom_theme")); // Adds predefined theme names. Iterator iterator = predefinedThemeNames(); while(iterator.hasNext()) themes.add(iterator.next()); // Adds custom theme names. try { iterator = customThemeNames(); while (iterator.hasNext()) { themes.add(iterator.next()); } } catch(Exception e) { log.debug("Failed to load custom theme names", e); } // Sorts the theme names. Collections.sort(themes); return themes; } public static Iterator availableThemeNames() { return getAvailableThemeNames().iterator(); } public static synchronized Iterator availableThemes() { return getAvailableThemes().iterator(); } // - Theme paths access -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Returns the path to the user's theme file. *

    * This method cannot guarantee the file's existence, and it's up to the caller * to deal with the fact that the user might not actually have created a user theme. *

    * This method's return value can be modified through {@link #setUserThemeFile(String)}. * If this wasn't called, the default path will be used. This is generated by calling * new java.io.File({@link com.mucommander.PlatformManager#getPreferencesFolder()}, {@link #USER_THEME_FILE_NAME}). * * @return the path to the user's theme file. * @see #setUserThemeFile(String) * @throws IOException if an error occurred while locating the default user theme file. */ private static AbstractFile getUserThemeFile() throws IOException { if (userThemeFile == null) { return PlatformManager.getPreferencesFolder().getChild(USER_THEME_FILE_NAME); } return userThemeFile; } /** * Sets the path to the user theme file. *

    * The specified file does not have to exist. If it does, however, it must be accessible. * * @param file path to the user theme file. * @throws FileNotFoundException if file is not accessible. * @see #getUserThemeFile() */ private static void setUserThemeFile(File file) throws FileNotFoundException { setUserThemeFile(FileFactory.getFile(file.getAbsolutePath())); } /** * Sets the path to the user theme file. *

    * The specified file does not have to exist. If it does, however, it must be accessible. * * @param file path to the user theme file. * @throws IllegalArgumentException if file exists but is not accessible. * @see #getUserThemeFile() */ private static void setUserThemeFile(AbstractFile file) throws FileNotFoundException { if (file.isBrowsable()) { throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath()); } userThemeFile = file; } /** * Sets the path to the user theme file. *

    * The specified file does not have to exist. If it does, however, it must be accessible. * * @param path path to the user theme file. * @throws FileNotFoundException if path is not accessible. * @see #getUserThemeFile() */ private static void setUserThemeFile(String path) throws FileNotFoundException { AbstractFile file = FileFactory.getFile(path); if (file == null) { setUserThemeFile(new File(path)); } else { setUserThemeFile(file); } } /** * Returns the path to the custom themes' folder. *

    * This method guarantees that the returned file actually exists. * * @return the path to the custom themes' folder. * @throws IOException if an error occured while locating the default user themes folder. */ public static AbstractFile getCustomThemesFolder() throws IOException { // Retrieves the path to the custom themes folder and creates it if necessary. AbstractFile customFolder = PlatformManager.getPreferencesFolder().getChild(CUSTOM_THEME_FOLDER); if (!customFolder.exists()) { customFolder.mkdir(); } return customFolder; } // - Theme renaming / deleting ------------------------------------------------------- // ----------------------------------------------------------------------------------- public static void deleteCustomTheme(String name) throws IOException { // Makes sure the specified theme is not the current one. if (isCurrentTheme(Theme.Type.CUSTOM, name)) { throw new IllegalArgumentException("Cannot delete current theme."); } // Deletes the theme. AbstractFile file = getFile(Type.CUSTOM, name); if (file.exists()) { file.delete(); } } public static void renameCustomTheme(Theme theme, String name) throws IOException { if (theme.getType() != Theme.Type.CUSTOM) { throw new IllegalArgumentException("Cannot rename non-custom themes."); } // Makes sure the operation is necessary. if (theme.getName().equals(name)) { return; } // Computes a legal new name and renames theme. name = getAvailableCustomThemeName(name); getFile(Type.CUSTOM, theme.getName()).renameTo(getFile(Type.CUSTOM, name)); theme.setName(name); if (isCurrentTheme(theme)) { setConfigurationTheme(theme); } } // - Theme writing ------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Returns an output stream on the specified custom theme. * @param name name of the custom theme on which to open an output stream. * @return an output stream on the specified custom theme. * @throws IOException if an I/O related error occurs. */ private static BackupOutputStream getCustomThemeOutputStream(String name) throws IOException { return new BackupOutputStream(getCustomThemesFolder().getChild(name + ".xml")); } /** * Returns an output stream on the user theme. * @return an output stream on the user theme. * @throws IOException if an I/O related error occurs. */ private static BackupOutputStream getUserThemeOutputStream() throws IOException { return new BackupOutputStream(getUserThemeFile()); } /** * Returns the file to read/write the requested theme. *

    * If type is equal to {@link Theme.Type#USER}, the name argument will be ignored: there * is only one user theme. * * If type is equal to {@link Theme.Type#PREDEFINED}, an IllegalArgumentException will be * thrown: predefined themes are not editable. * * @param type * type of the theme for which to get the file. * @param name * name of the theme for which to get the file. * @return a file for the requested theme. * @throws IllegalArgumentException * if type is equal to {@link Theme.Type#PREDEFINED}. */ public static AbstractFile getFile(Theme.Type type, String name) throws IOException { switch (type) { case PREDEFINED: throw new IllegalArgumentException("Can not open output streams on predefined themes."); case CUSTOM: return getCustomThemesFolder().getChild(name + ".xml"); case USER: return getUserThemeFile(); } // Unknown theme. throw new IllegalArgumentException("Illegal theme type: " + type); } /** * Returns an output stream on the requested theme. *

    * This method is just a convenience, and wraps calls to {@link #getUserThemeInputStream()}, * and {@link #getCustomThemeInputStream(String)}. * *

    * If type is equal to {@link Theme.Type#USER}, the name argument * will be ignored: there is only one user theme. * *

    * If type is equal to {@link Theme.Type#PREDEFINED}, an IllegalArgumentException * will be thrown: predefined themes are not editable. * * @param type type of the theme on which to open an output stream. * @param name name of the theme on which to open an output stream. * @return an output stream on the requested theme. * @throws IOException if an I/O related error occurs. */ private static BackupOutputStream getOutputStream(Theme.Type type, String name) throws IOException { switch(type) { case PREDEFINED: throw new IllegalArgumentException("Can not open output streams on predefined themes."); case CUSTOM: return getCustomThemeOutputStream(name); case USER: return getUserThemeOutputStream(); } // Unknown theme. throw new IllegalArgumentException("Illegal theme type: " + type); } /** * Writes the content of the specified theme data to the specified output stream. *

    * This method differs from {@link #exportTheme(Theme,OutputStream)} in that it will * write the theme data only, skipping comments and other metadata. * * @param data theme data to write. * @param out where to write the theme data. * @throws IOException if an I/O related error occurs. * @see #exportTheme(Theme,OutputStream) * @see #exportTheme(Theme,File) * @see #writeThemeData(ThemeData,File). */ private static void writeThemeData(ThemeData data, OutputStream out) throws IOException {ThemeWriter.write(data, out);} /** * Writes the content of the specified theme data to the specified file. *

    * This method differs from {@link #exportTheme(Theme,File)} in that it will * write the theme data only, skipping comments and other metadata. * * @param data theme data to write. * @param file file in which to write the theme data. * @throws IOException if an I/O related error occurs. * @see #exportTheme(Theme,OutputStream) * @see #exportTheme(Theme,File) * @see #writeThemeData(ThemeData,OutputStream). */ private static void writeThemeData(ThemeData data, File file) throws IOException { try (OutputStream out = new FileOutputStream(file)) { writeThemeData(data, out); } } /** * Writes the content of the specified theme to its description file. * @param theme theme to write. * @throws IOException if any I/O related error occurs. * @throws IllegalArgumentException if theme is a predefined theme. * @see #writeTheme(ThemeData,Theme.Type,String) */ public static void writeTheme(Theme theme) throws IOException {writeTheme(theme, theme.getType(), theme.getName());} /** * Writes the specified theme data over the theme described by type and name. *

    * Note that this method doesn't check whether this will overwrite an existing theme. * *

    * If type equals {@link Theme.Type#USER}, name will be ignored. * * @param data data to write. * @param type type of the theme that is being written. * @param name name of the theme that is being written. * @throws IOException if any I/O related error occurs. * @throws IllegalArgumentException if theme is a predefined theme. * @see #writeTheme(Theme) */ public static void writeTheme(ThemeData data, Theme.Type type, String name) throws IOException { try (OutputStream out = getOutputStream(type, name)) { writeThemeData(data, out); } } /** * Exports the specified theme to the specified output stream. *

    * If type is equal to {@link Theme.Type#USER}, the name argument will be ignored * as there is only one user theme. * *

    * This method differs from {@link #writeThemeData(ThemeData,OutputStream)} in that it doesn't only copy * the theme's data, but the whole content of the theme file, including comments. It also requires the theme * file to exist. * * @param type type of the theme to export. * @param name name of the theme to export. * @param out where to write the theme. * @throws IOException if any I/O related error occurs. * @see #exportTheme(Theme.Type,String,File) * @see #writeThemeData(ThemeData,OutputStream) */ private static void exportTheme(Theme.Type type, String name, OutputStream out) throws IOException { try (InputStream in = getInputStream(type, name)) { StreamUtils.copyStream(in, out); } } /** * Exports the specified theme to the specified output stream. *

    * If type is equal to {@link Theme.Type#USER}, the name argument will be ignored * as there is only one user theme. * *

    * This method differs from {@link #writeThemeData(ThemeData,File)} in that it doesn't only copy * the theme's data, but the whole content of the theme file, including comments. * * @param type type of the theme to export. * @param name name of the theme to export. * @param file where to write the theme. * @throws IOException if any I/O related error occurs * @see #exportTheme(Theme.Type,String,OutputStream) * @see #writeThemeData(ThemeData,File). */ private static void exportTheme(Theme.Type type, String name, File file) throws IOException { try (OutputStream out = new FileOutputStream(file)) { exportTheme(type, name, out); } } /** * Exports the specified theme to the specified output stream. *

    * This is a convenience method only and is strictly equivalent to calling * {@link #exportTheme(Theme.Type,String,OutputStream) exportTheme(}theme.getType(), theme.getName(), out); * * @param theme theme to export. * @param out where to write the theme. * @throws IOException if any I/O related error occurs. */ public static void exportTheme(Theme theme, OutputStream out) throws IOException {exportTheme(theme.getType(), theme.getName(), out);} /** * Exports the specified theme to the specified output stream. *

    * This is a convenience method only and is strictly equivalent to calling * {@link #exportTheme(Theme.Type,String,File) exportTheme(}theme.getType(), theme.getName(), file); * * @param theme theme to export. * @param file where to write the theme. * @throws IOException if any I/O related error occurs. */ public static void exportTheme(Theme theme, File file) throws IOException {exportTheme(theme.getType(), theme.getName(), file);} private static String getAvailableCustomThemeName(File file) { String name; // Retrieves the file's name, cutting the .xml extension off if // necessary. if(StringUtils.endsWithIgnoreCase(name = file.getName(), ".xml")) name = name.substring(0, name.length() - 4); return getAvailableCustomThemeName(name); } private static boolean isNameAvailable(String name, Iterator names) { while(names.hasNext()) if(names.next().equals(name)) return false; return true; } private static String getAvailableCustomThemeName(String name) { List names = getAvailableThemeNames(); // If the name is available, no need to suffix it with (xx). if(isNameAvailable(name, names.iterator())) return name; // Removes any trailing (x) construct, and adds a trailing space if necessary. name = name.replaceFirst("\\([0-9]+\\)$", ""); if (name.charAt(name.length() - 1) != ' ') { name = name + ' '; } int i = 1; String buffer; do { buffer = name + '(' + (++i) + ')'; } while(!isNameAvailable(buffer, names.iterator())); return buffer; } public static Theme duplicateTheme(Theme theme) throws IOException { return importTheme(theme.cloneData(), theme.getName()); } public static Theme importTheme(ThemeData data, String name) throws IOException { writeTheme(data, Theme.Type.CUSTOM, name = getAvailableCustomThemeName(name)); return new Theme(listener, data, Theme.Type.CUSTOM, name); } public static Theme importTheme(File file) throws Exception { String name; // Name of the new theme. OutputStream out; // Where to write the theme data to. InputStream in; // Where to read the theme data from. ThemeData data; // Makes sure the file contains a valid theme. data = readThemeData(file); // Initialisation. name = getAvailableCustomThemeName(file); out = null; in = null; // Imports the theme. try {StreamUtils.copyStream(in = new FileInputStream(file), out = getCustomThemeOutputStream(name));} // Cleanup. finally { if (in != null) { try { in.close(); } catch(Exception e) { log.error("Can't close in file", e); } } if (out != null) { try { out.close(); } catch(Exception e) { log.error("Can't close out file", e); } } } return new Theme(listener, data, Theme.Type.CUSTOM, name); } /** * Returns an input stream on the user theme. * @return an input stream on the user theme. * @throws IOException if an I/O related error occurs. */ private static InputStream getUserThemeInputStream() throws IOException { return new BackupInputStream(getUserThemeFile()); } /** * Returns an input stream on the requested predefined theme. * @param name name of the predefined theme on which to open an input stream. * @return an input stream on the requested predefined theme. */ private static InputStream getPredefinedThemeInputStream(String name) { return ResourceLoader.getResourceAsStream(RuntimeConstants.THEMES_PATH + "/" + name + ".xml"); } /** * Returns an input stream on the requested predefined theme for editor. * @param name name of the predefined editor theme on which to open an input stream. * @return an input stream on the requested predefined theme. */ private static InputStream getPredefinedEditorThemeInputStream(String name) { return ResourceLoader.getResourceAsStream(RuntimeConstants.TEXT_SYNTAX_THEMES_PATH + "/" + name + ".xml"); } /** * Returns an input stream on the requested custom theme. * @param name name of the custom theme on which to open an input stream. * @return an input stream on the requested custom theme. * @throws IOException if an I/O related error occurs. */ private static InputStream getCustomThemeInputStream(String name) throws IOException { return new BackupInputStream(getFile(Type.CUSTOM, name)); } /** * Opens an input stream on the requested theme. *

    * This method is just a convenience, and wraps calls to {@link #getUserThemeInputStream()}, * {@link #getPredefinedThemeInputStream(String)} and {@link #getCustomThemeInputStream(String)}. * * @param type type of the theme to open an input stream on. * @param name name of the theme to open an input stream on. * @return an input stream opened on the requested theme. * @throws IOException thrown if an IO related error occurs. * @throws IllegalArgumentException thrown if type is not a legal theme type. */ private static InputStream getInputStream(Theme.Type type, String name) throws IOException { switch(type) { case USER: return getUserThemeInputStream(); case PREDEFINED: return getPredefinedThemeInputStream(name); case CUSTOM: return getCustomThemeInputStream(name); } // Error handling. throw new IllegalArgumentException("Illegal theme type: " + type); } /** * Returns the requested theme. * @param type type of theme to retrieve. * @param name name of the theme to retrieve. * @return the requested theme. */ private static Theme readTheme(Theme.Type type, String name) throws Exception { // Do not reload the current theme, both for optimisation purposes and because // it might cause user theme modifications to be lost. if (currentTheme != null && isCurrentTheme(type, name)) return currentTheme; // Reads the theme data. try (InputStream in = getInputStream(type, name)) { ThemeData data = readThemeData(in); return new Theme(listener, data, type, name); } } /** * Return the requested theme for file viewer/editor * @param name name ot the theme * @return the resulting theme data. */ public static EditorTheme readEditorTheme(String name) throws IOException { InputStream is = getPredefinedEditorThemeInputStream(name); return EditorTheme.load(is, ThemeManager.getCurrentFont(Theme.EDITOR_FONT)); } /** * Reads theme data from the specified input stream. * @param in where to read the theme data from. * @return the resulting theme data. * @throws IOException if an I/O or syntax error occurs. */ private static ThemeData readThemeData(InputStream in) throws Exception { ThemeData data = new ThemeData(); // Buffer for the data. // Reads the theme data. ThemeReader.read(in, data); return data; } /** * Reads theme data from the specified file. * @param file where to read the theme data from. * @return the resulting theme data. * @throws Exception if an I/O or syntax error occurs. */ private static ThemeData readThemeData(File file) throws Exception { try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { return readThemeData(in); } } // - Current theme access ------------------------------------------------------------ // ----------------------------------------------------------------------------------- private static void setConfigurationTheme(Theme.Type type, String name) { // Sets configuration depending on the new theme's type. switch(type) { case USER: TcConfigurations.getPreferences().setVariable(TcPreference.THEME_TYPE, TcPreferences.THEME_USER); TcConfigurations.getPreferences().setVariable(TcPreference.THEME_NAME, null); break; case PREDEFINED: TcConfigurations.getPreferences().setVariable(TcPreference.THEME_TYPE, TcPreferences.THEME_PREDEFINED); TcConfigurations.getPreferences().setVariable(TcPreference.THEME_NAME, name); break; case CUSTOM: TcConfigurations.getPreferences().setVariable(TcPreference.THEME_TYPE, TcPreferences.THEME_CUSTOM); TcConfigurations.getPreferences().setVariable(TcPreference.THEME_NAME, name); break; // Error. default: throw new IllegalStateException("Illegal theme type: " + type); } } /** * Sets the specified theme as the current theme in configuration. * @param theme theme to set as current. */ private static void setConfigurationTheme(Theme theme) {setConfigurationTheme(theme.getType(), theme.getName());} /** * Saves the current theme if necessary. */ private static void saveCurrentTheme() throws IOException { // Makes sure no NullPointerException is raised if this method is called // before themes have been initialized. if (currentTheme == null) { return; } // Saves the user theme if it's the current one. if (currentTheme.getType() == Theme.Type.USER && wasUserThemeModified) { writeTheme(currentTheme); wasUserThemeModified = false; } } /** * Changes the current theme. *

    * This method will change the current theme and trigger all the proper events. * * @param theme theme to use as the current theme. * @throws IllegalArgumentException thrown if the specified theme could not be loaded. */ public synchronized static void setCurrentTheme(Theme theme) { // Makes sure we're not doing something useless. if (isCurrentTheme(theme)) { return; } // Saves the current theme if necessary. try { saveCurrentTheme(); } catch(IOException e) { log.warn("Couldn't save current theme", e); } // Updates muCommander's configuration. Theme oldTheme = currentTheme; setConfigurationTheme(currentTheme = theme); // Triggers the events generated by the theme change. triggerThemeChange(oldTheme, currentTheme); } /** * * @param name name of the theme */ public synchronized static void setCurrentSyntaxTheme(String name) { currentSyntaxThemeName = name; TcConfigurations.getPreferences().setVariable(TcPreference.SYNTAX_THEME_NAME, name); } public synchronized static Font getCurrentFont(int id) {return currentTheme.getFont(id);} public synchronized static Color getCurrentColor(int id) {return currentTheme.getColor(id);} public synchronized static Theme overwriteUserTheme(ThemeData themeData) throws IOException { // If the current theme is the user one, we just need to import the new data. if (currentTheme.getType() == Theme.Type.USER) { currentTheme.importData(themeData); writeTheme(currentTheme); return currentTheme; } else { writeTheme(themeData, Theme.Type.CUSTOM, currentTheme.getName()); return new Theme(listener, themeData); } } /** * Checks whether setting the specified font would require overwriting of the user theme. * @param fontId identifier of the font to set. * @param font value for the specified font. * @return true if applying the specified font will overwrite the user theme, * false otherwise. */ private synchronized static boolean willOverwriteUserTheme(int fontId, Font font) { return currentTheme.isFontDifferent(fontId, font) && currentTheme.getType() != Theme.Type.USER; } /** * Checks whether setting the specified color would require overwriting of the user theme. * @param colorId identifier of the color to set. * @param color value for the specified color. * @return true if applying the specified color will overwrite the user theme, * false otherwise. */ private synchronized static boolean willOverwriteUserTheme(int colorId, Color color) { return currentTheme.isColorDifferent(colorId, color) && currentTheme.getType() != Theme.Type.USER; } /** * Updates the current theme with the specified font. *

    * This method might require to overwrite the user theme: custom and predefined themes are * read only. In order to modify them, the ThemeManager must overwrite the user theme with * the current theme and then set the font.
    * If necessary, this can be checked beforehand by a call to {@link #willOverwriteUserTheme(int,Font)}. * * @param id identifier of the font to set. * @param font font to set. */ synchronized static boolean setCurrentFont(int id, Font font) { // Only updates if necessary. if (currentTheme.isFontDifferent(id, font)) { // Checks whether we need to overwrite the user theme to perform this action. if (currentTheme.getType() != Theme.Type.USER) { currentTheme.setType(Theme.Type.USER); setConfigurationTheme(currentTheme); } currentTheme.setFont(id, font); return true; } return false; } /** * Updates the current theme with the specified color. *

    * This method might require to overwrite the user theme: custom and predefined themes are * read only. In order to modify them, the ThemeManager must overwrite the user theme with * the current theme and then set the color.
    * If necessary, this can be checked beforehand by a call to {@link #willOverwriteUserTheme(int,Color)}. * * @param id identifier of the color to set. * @param color color to set. */ synchronized static boolean setCurrentColor(int id, Color color) { // Only updates if necessary. if(currentTheme.isColorDifferent(id, color)) { // Checks whether we need to overwrite the user theme to perform this action. if(currentTheme.getType() != Theme.Type.USER) { currentTheme.setType(Theme.Type.USER); setConfigurationTheme(currentTheme); } // Updates the color and notifies listeners. currentTheme.setColor(id, color); return true; } return false; } /** * Returns true if the specified theme is the current one. * @param theme theme to check. * @return true if the specified theme is the current one, false otherwise. */ public static boolean isCurrentTheme(Theme theme) {return theme == currentTheme;} private static boolean isCurrentTheme(Theme.Type type, String name) { return type == currentTheme.getType() && (type == Type.USER || name.equals(currentTheme.getName())); } // - Events management --------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Notifies all listeners that the current theme has changed. *

    * This method is meant to be called when the current theme has been changed. * It will compare all fonts and colors in oldTheme and newTheme and, * if any is found to be different, trigger the corresponding event. * *

    * At the end of this method, all registered listeners will have been made aware of the new values * they should be using. * * @param oldTheme previous current theme. * @param newTheme new current theme. * @see #triggerFontEvent(FontChangedEvent) * @see #triggerColorEvent(ColorChangedEvent) */ private static void triggerThemeChange(Theme oldTheme, Theme newTheme) { // Triggers font events. for(int i = 0; i < Theme.FONT_COUNT; i++) if(oldTheme.isFontDifferent(i, newTheme.getFont(i))) triggerFontEvent(new FontChangedEvent(currentTheme, i, newTheme.getFont(i))); // Triggers color events. for(int i = 0; i < Theme.COLOR_COUNT; i++) if(oldTheme.isColorDifferent(i, newTheme.getColor(i))) triggerColorEvent(new ColorChangedEvent(currentTheme, i, newTheme.getColor(i))); } /** * Adds the specified object to the list of registered current theme listeners. *

    * Any object registered through this method will received {@link ThemeListener#colorChanged(ColorChangedEvent) color} * and {@link ThemeListener#fontChanged(FontChangedEvent) font} events whenever the current theme changes. * *

    * Note that these events will not necessarily be fired as a result of a direct theme change: if, for example, * the current theme is using look&feel dependant values and the current look&feel changes, the corresponding * events will be passed to registered listeners. * *

    * Listeners are stored as weak references, to make sure that the API doesn't keep ghost copies of objects * whose usefulness is long since past. This forces callers to make sure they keep a copy of the listener's instance: if * they do not, the instance will be weakly linked and garbage collected out of existence. * * @param listener new current theme listener. */ public static void addCurrentThemeListener(ThemeListener listener) { synchronized (listeners) { listeners.put(listener, null); } } /** * Removes the specified object from the list of registered theme listeners. *

    * Note that since listeners are stored as weak references, calling this method is not strictly necessary. As soon * as a listener instance is not referenced anymore, it will automatically be caught and destroyed by the garbage * collector. * * @param listener current theme listener to remove. */ public static void removeCurrentThemeListener(ThemeListener listener) { synchronized (listeners) { listeners.remove(listener); } } /** * Notifies all theme listeners of the specified font event. * @param event event to pass down to registered listeners. * @see #triggerThemeChange(Theme,Theme) */ private static void triggerFontEvent(FontChangedEvent event) { synchronized (listeners) { for (ThemeListener listener : listeners.keySet()) { listener.fontChanged(event); } } } /** * Notifies all theme listeners of the specified color event. * @param event event to pass down to registered listeners. * @see #triggerThemeChange(Theme,Theme) */ private static void triggerColorEvent(ColorChangedEvent event) { synchronized (listeners) { for (ThemeListener listener : listeners.keySet()) { listener.colorChanged(event); } } } // - Helper methods ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Returns a valid type identifier from the specified configuration type definition. * @param label label of the theme type as defined in {@link TcPreferences}. * @return a valid theme type identifier. */ private static Theme.Type getThemeTypeFromLabel(String label) { switch (label) { case TcPreferences.THEME_USER: return Theme.Type.USER; case TcPreferences.THEME_PREDEFINED: return Theme.Type.PREDEFINED; case TcPreferences.THEME_CUSTOM: return Theme.Type.CUSTOM; } throw new IllegalStateException("Unknown theme type: " + label); } private static String getThemeName(AbstractFile themeFile) { return themeFile.getNameWithoutExtension(); } // - Listener methods ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * @author Nicolas Rinaudo */ private static class CurrentThemeListener implements ThemeListener { public void fontChanged(FontChangedEvent event) { if (event.getSource().getType() == Theme.Type.USER) { wasUserThemeModified = true; } if (event.getSource() == currentTheme) { triggerFontEvent(event); } } public void colorChanged(ColorChangedEvent event) { if (event.getSource().getType() == Theme.Type.USER) { wasUserThemeModified = true; } if (event.getSource() == currentTheme) { triggerColorEvent(event); } } } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeReader.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import java.awt.Color; import java.awt.Font; import java.awt.GraphicsEnvironment; import java.io.InputStream; import java.util.StringTokenizer; import javax.xml.parsers.SAXParserFactory; import org.intellij.lang.annotations.MagicConstant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; /** * Loads theme instances from properly formatted XML files. * @author Nicolas Rinaudo */ class ThemeReader extends DefaultHandler implements ThemeXmlConstants, ThemeId { private static Logger logger; // TODO !!! add previous state to enum insteadof terrible if-else list private enum State { /** Parsing hasn't started yet. */ UNKNOWN, /** Parsing the root element. */ ROOT, /** Parsing the table element.*/ TABLE, /** Parsing the shell element. */ SHELL, /** Parsing the editor element. */ EDITOR, /** Parsing the location bar element. */ LOCATION_BAR, /** Parsing the shell.normal element. */ SHELL_NORMAL, /** Parsing the shell.selected element. */ SHELL_SELECTED, /** Parsing the editor.normal element. */ EDITOR_NORMAL, /** Parsing the location bar.normal element. */ LOCATION_BAR_NORMAL, /** Parsing the editor.selected element. */ EDITOR_SELECTED, /** Parsing the location bar.selected element. */ LOCATION_BAR_SELECTED, /** Parsing the shell_history element. */ SHELL_HISTORY, /** Parsing the shell_history.normal element. */ SHELL_HISTORY_NORMAL, /** Parsing the shell_history.selected element. */ SHELL_HISTORY_SELECTED, /** Parsing the volume_label element. */ STATUS_BAR, HIDDEN_FILE, HIDDEN_FILE_NORMAL, HIDDEN_FILE_SELECTED, HIDDEN_FOLDER, HIDDEN_FOLDER_NORMAL, HIDDEN_FOLDER_SELECTED, FOLDER, FOLDER_NORMAL, FOLDER_SELECTED, ARCHIVE, ARCHIVE_NORMAL, ARCHIVE_SELECTED, SYMLINK, SYMLINK_NORMAL, SYMLINK_SELECTED, MARKED, MARKED_NORMAL, MARKED_SELECTED, EXECUTABLE, EXECUTABLE_NORMAL, EXECUTABLE_SELECTED, FILE, FILE_NORMAL, FILE_SELECTED, TABLE_NORMAL, TABLE_SELECTED, TABLE_ALTERNATE, TABLE_UNMATCHED, /** Parsing the quick list element. */ QUICK_LIST, /** Parsing the quick list header element. */ QUICK_LIST_HEADER, /** Parsing the quick list item element. */ QUICK_LIST_ITEM, QUICK_LIST_ITEM_NORMAL, QUICK_LIST_ITEM_SELECTED, /** Parsing the editor.selected element. */ EDITOR_CURRENT, FILE_GROUP, GROUP_1, GROUP_2, GROUP_3, GROUP_4, GROUP_5, GROUP_6, GROUP_7, GROUP_8, GROUP_9, GROUP_10, TERMINAL, /** Parsing the terminal.normal element. */ TERMINAL_NORMAL, /** Parsing the terminal.selected element. */ TERMINAL_SELECTED, HEX_VIEWER, HEX_VIEWER_NORMAL, HEX_VIEWER_SELECTED, } /** Theme template that is currently being built. */ private final ThemeData template; /** Current state of the XML parser. */ private State state; /** Used to ignore the content of an unknown tag. */ private String unknownElement; /** * Creates a new theme reader. */ private ThemeReader(ThemeData t) { template = t; state = State.UNKNOWN; } /** * Attempts to read a theme from the specified input stream. * @param in where to read the theme from. * @param template template in which to store the data. * @exception Exception thrown if an error occured while reading the template. */ public static void read(InputStream in, ThemeData template) throws Exception { SAXParserFactory.newInstance().newSAXParser().parse(in, new ThemeReader(template)); } // - XML interaction ----------------------------------------------------- // ----------------------------------------------------------------------- /** * Notifies the reader that a new XML element is starting. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { // Ignores the content of unknown elements. if (unknownElement != null) { getLogger().debug("Ignoring element " + qName); return; } // Normal element declaration. if (ELEMENT_NORMAL.equals(qName)) { if (state == State.SHELL) { state = State.SHELL_NORMAL; } else if (state == State.EDITOR) { state = State.EDITOR_NORMAL; } else if (state == State.LOCATION_BAR) { state = State.LOCATION_BAR_NORMAL; } else if (state == State.SHELL_HISTORY) { state = State.SHELL_HISTORY_NORMAL; } else if (state == State.TERMINAL) { state = State.TERMINAL_NORMAL; } else if (state == State.HIDDEN_FILE) { state = State.HIDDEN_FILE_NORMAL; } else if (state == State.HIDDEN_FOLDER) { state = State.HIDDEN_FOLDER_NORMAL; } else if (state == State.FOLDER) { state = State.FOLDER_NORMAL; } else if (state == State.ARCHIVE) { state = State.ARCHIVE_NORMAL; } else if (state == State.SYMLINK) { state = State.SYMLINK_NORMAL; } else if (state == State.MARKED) { state = State.MARKED_NORMAL; } else if (state == State.EXECUTABLE) { state = State.EXECUTABLE_NORMAL; } else if (state == State.FILE) { state = State.FILE_NORMAL; } else if (state == State.TABLE) { state = State.TABLE_NORMAL; } else if (state == State.QUICK_LIST_ITEM) { state = State.QUICK_LIST_ITEM_NORMAL; } else if (state.ordinal() >= State.GROUP_1.ordinal() && state.ordinal() <= State.GROUP_10.ordinal()) { int group = state.ordinal() - State.GROUP_1.ordinal(); setColor(FILE_GROUP_1_FOREGROUND_COLOR + group, attributes); } else if (state == State.HEX_VIEWER) { state = State.HEX_VIEWER_NORMAL; } else { traceIllegalDeclaration(qName); } } // Background color. else if (ELEMENT_BACKGROUND.equals(qName)) { if (state == State.TABLE_NORMAL) { setColor(FILE_TABLE_BACKGROUND_COLOR, attributes); } else if (state == State.TABLE_SELECTED) { setColor(FILE_TABLE_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.TABLE_ALTERNATE) { setColor(FILE_TABLE_ALTERNATE_BACKGROUND_COLOR, attributes); } else if (state == State.TABLE_UNMATCHED) { setColor(FILE_TABLE_UNMATCHED_BACKGROUND_COLOR, attributes); } else if (state == State.SHELL_NORMAL) { setColor(SHELL_BACKGROUND_COLOR, attributes); } else if (state == State.SHELL_SELECTED) { setColor(SHELL_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.EDITOR_NORMAL) { setColor(EDITOR_BACKGROUND_COLOR, attributes); } else if (state == State.EDITOR_SELECTED) { setColor(EDITOR_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.EDITOR_CURRENT) { setColor(EDITOR_CURRENT_BACKGROUND_COLOR, attributes); } else if (state == State.LOCATION_BAR_NORMAL) { setColor(LOCATION_BAR_BACKGROUND_COLOR, attributes); } else if (state == State.LOCATION_BAR_SELECTED) { setColor(LOCATION_BAR_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.SHELL_HISTORY_NORMAL) { setColor(SHELL_HISTORY_BACKGROUND_COLOR, attributes); } else if (state == State.SHELL_HISTORY_SELECTED) { setColor(SHELL_HISTORY_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.TERMINAL_NORMAL) { setColor(TERMINAL_BACKGROUND_COLOR, attributes); } else if (state == State.TERMINAL_SELECTED) { setColor(TERMINAL_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.STATUS_BAR) { setColor(STATUS_BAR_BACKGROUND_COLOR, attributes); } else if (state == State.QUICK_LIST_HEADER) { setColor(QUICK_LIST_HEADER_BACKGROUND_COLOR, attributes); } else if (state == State.QUICK_LIST_ITEM_NORMAL) { setColor(QUICK_LIST_ITEM_BACKGROUND_COLOR, attributes); } else if (state == State.QUICK_LIST_ITEM_SELECTED) { setColor(QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR, attributes); } else if (state == State.HEX_VIEWER_NORMAL) { setColor(HEX_VIEWER_BACKGROUND_COLOR, attributes); } else if (state == State.HEX_VIEWER_SELECTED) { setColor(HEX_VIEWER_SELECTED_BACKGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // Selected element declaration. else if (ELEMENT_SELECTED.equals(qName)) { if (state == State.SHELL) { state = State.SHELL_SELECTED; } else if (state == State.EDITOR) { state = State.EDITOR_SELECTED; } else if (state == State.LOCATION_BAR) { state = State.LOCATION_BAR_SELECTED; } else if (state == State.SHELL_HISTORY) { state = State.SHELL_HISTORY_SELECTED; } else if (state == State.TERMINAL) { state = State.TERMINAL_SELECTED; } else if (state == State.HIDDEN_FILE) { state = State.HIDDEN_FILE_SELECTED; } else if (state == State.HIDDEN_FOLDER) { state = State.HIDDEN_FOLDER_SELECTED; } else if (state == State.FOLDER) { state = State.FOLDER_SELECTED; } else if (state == State.ARCHIVE) { state = State.ARCHIVE_SELECTED; } else if (state == State.SYMLINK) { state = State.SYMLINK_SELECTED; } else if (state == State.MARKED) { state = State.MARKED_SELECTED; } else if (state == State.EXECUTABLE) { state = State.EXECUTABLE_SELECTED; } else if (state == State.FILE) { state = State.FILE_SELECTED; } else if (state == State.TABLE) { state = State.TABLE_SELECTED; } else if (state == State.QUICK_LIST_ITEM) { state = State.QUICK_LIST_ITEM_SELECTED; } else if (state == State.HEX_VIEWER) { state = State.HEX_VIEWER_SELECTED; } else { traceIllegalDeclaration(qName); } } // Unfocused foreground color. else if (ELEMENT_INACTIVE_FOREGROUND.equals(qName)) { if (state == State.FILE_NORMAL) { setColor(FILE_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.FOLDER_NORMAL) { setColor(FOLDER_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.ARCHIVE_NORMAL) { setColor(ARCHIVE_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.SYMLINK_NORMAL) { setColor(SYMLINK_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.HIDDEN_FILE_NORMAL) { setColor(HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.HIDDEN_FOLDER_NORMAL) { setColor(HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.MARKED_NORMAL) { setColor(MARKED_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.EXECUTABLE_NORMAL) { setColor(EXECUTABLE_INACTIVE_FOREGROUND_COLOR, attributes); } else if(state == State.FILE_SELECTED) { setColor(FILE_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.FOLDER_SELECTED) { setColor(FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.ARCHIVE_SELECTED) { setColor(ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.SYMLINK_SELECTED) { setColor(SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.HIDDEN_FOLDER_SELECTED) { setColor(HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.HIDDEN_FILE_SELECTED) { setColor(HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.MARKED_SELECTED) { setColor(MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if(state == State.EXECUTABLE_SELECTED) { setColor(EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // Font creation. else if (ELEMENT_FONT.equals(qName)) { if (state == State.SHELL) { setFont(SHELL_FONT, attributes); } else if (state == State.EDITOR) { setFont(EDITOR_FONT, attributes); } else if (state == State.LOCATION_BAR) { setFont(LOCATION_BAR_FONT, attributes); } else if (state == State.SHELL_HISTORY) { setFont(SHELL_HISTORY_FONT, attributes); } else if (state == State.TERMINAL) { setFont(TERMINAL_FONT, attributes); } else if (state == State.STATUS_BAR) { setFont(STATUS_BAR_FONT, attributes); } else if (state == State.TABLE) { setFont(FILE_TABLE_FONT, attributes); } else if (state == State.QUICK_LIST_HEADER) { setFont(QUICK_LIST_HEADER_FONT, attributes); } else if (state == State.QUICK_LIST_ITEM) { setFont(QUICK_LIST_ITEM_FONT, attributes); } else if (state == State.HEX_VIEWER) { setFont(ThemeId.HEX_VIEWER_FONT, attributes); } else { traceIllegalDeclaration(qName); } } // XML root element. else if (ELEMENT_ROOT.equals(qName)) { if (state != State.UNKNOWN) { traceIllegalDeclaration(ELEMENT_ROOT); } state = State.ROOT; } // File table declaration. else if (ELEMENT_TABLE.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.TABLE; } else if (ELEMENT_FILE_GROUPS.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.FILE_GROUP; } // Shell declaration. else if (ELEMENT_SHELL.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.SHELL; } // Editor declaration. else if (ELEMENT_EDITOR.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.EDITOR; } // Location bar declaration. else if (ELEMENT_LOCATION_BAR.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.LOCATION_BAR; } // Quick list declaration. else if (ELEMENT_QUICK_LIST.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.QUICK_LIST; } // Shell history declaration. else if (ELEMENT_SHELL_HISTORY.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.SHELL_HISTORY; } else if (ELEMENT_TERMINAL.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.TERMINAL; } // Volume label declaration. else if (ELEMENT_STATUS_BAR.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.STATUS_BAR; } else if (ELEMENT_HIDDEN_FILE.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.HIDDEN_FILE; } else if (ELEMENT_HIDDEN_FOLDER.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.HIDDEN_FOLDER; } else if (ELEMENT_FOLDER.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.FOLDER; } else if (ELEMENT_ARCHIVE.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.ARCHIVE; } else if (ELEMENT_SYMLINK.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.SYMLINK; } else if (ELEMENT_MARKED.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.MARKED; } else if (ELEMENT_EXECUTABLE.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.EXECUTABLE; } else if (ELEMENT_FILE.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.FILE; } else if (ELEMENT_ALTERNATE.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.TABLE_ALTERNATE; } // Header declaration. else if (ELEMENT_HEADER.equals(qName)) { if (state == State.QUICK_LIST) { state = State.QUICK_LIST_HEADER; } else { traceIllegalDeclaration(qName); } } // Item declaration. else if (ELEMENT_ITEM.equals(qName)) { if (state == State.QUICK_LIST) { state = State.QUICK_LIST_ITEM; } else { traceIllegalDeclaration(qName); } } else if(ELEMENT_TERMINAL.equals(qName)) { if (state != State.ROOT) { traceIllegalDeclaration(qName); } state = State.TERMINAL; } // Current element declaration. else if (ELEMENT_CURRENT.equals(qName)) { if (state == State.EDITOR) { state = State.EDITOR_CURRENT; } else { traceIllegalDeclaration(qName); } } // Unfocused background color. else if (ELEMENT_INACTIVE_BACKGROUND.equals(qName)) { if (state == State.TABLE_NORMAL) { setColor(FILE_TABLE_INACTIVE_BACKGROUND_COLOR, attributes); } else { if (state == State.TABLE_SELECTED) { setColor(FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR, attributes); } else if (state == State.TABLE_ALTERNATE) { setColor(FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } } // Secondary background. else if (ELEMENT_SECONDARY_BACKGROUND.equals(qName)) { if (state == State.TABLE_SELECTED) { setColor(FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR, attributes); } else if (state == State.HEX_VIEWER_NORMAL) { setColor(HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR, attributes); } else { if (state == State.QUICK_LIST_HEADER) { setColor(QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } } // Inactive secondary background. else if (ELEMENT_INACTIVE_SECONDARY_BACKGROUND.equals(qName)) { if (state == State.TABLE_SELECTED) { setColor(FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // File table border color. else if (ELEMENT_BORDER.equals(qName)) { if (state == State.TABLE) { setColor(FILE_TABLE_BORDER_COLOR, attributes); } else { if (state == State.STATUS_BAR) { setColor(STATUS_BAR_BORDER_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } } // File table inactive border color. else if (ELEMENT_INACTIVE_BORDER.equals(qName)) { if (state == State.TABLE) { setColor(FILE_TABLE_INACTIVE_BORDER_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // File table outline color. else if (ELEMENT_OUTLINE.equals(qName)) { if (state == State.TABLE) { setColor(FILE_TABLE_SELECTED_OUTLINE_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // File table inactive outline color. else if (ELEMENT_INACTIVE_OUTLINE.equals(qName)) { if (state == State.TABLE) { setColor(FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // Unmatched file table. else if (ELEMENT_UNMATCHED.equals(qName)) { if (state != State.TABLE) { traceIllegalDeclaration(qName); } state = State.TABLE_UNMATCHED; } // Progress bar color. else if (ELEMENT_PROGRESS.equals(qName)) { if (state == State.LOCATION_BAR) { setColor(LOCATION_BAR_PROGRESS_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // 'OK' color. else if (ELEMENT_OK.equals(qName)) { if (state == State.STATUS_BAR) { setColor(STATUS_BAR_OK_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // 'WARNING' color. else if (ELEMENT_WARNING.equals(qName)) { if (state == State.STATUS_BAR) { setColor(STATUS_BAR_WARNING_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // 'CRITICAL' color. else if (ELEMENT_CRITICAL.equals(qName)) { if (state == State.STATUS_BAR) { setColor(STATUS_BAR_CRITICAL_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } // Text color. else if (ELEMENT_FOREGROUND.equals(qName)) { if (state == State.HIDDEN_FILE_NORMAL) { setColor(HIDDEN_FILE_FOREGROUND_COLOR, attributes); } else if (state == State.HIDDEN_FILE_SELECTED) { setColor(HIDDEN_FILE_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.HIDDEN_FOLDER_NORMAL) { setColor(HIDDEN_FOLDER_FOREGROUND_COLOR, attributes); } else if (state == State.HIDDEN_FOLDER_SELECTED) { setColor(HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.TABLE_UNMATCHED) { setColor(FILE_TABLE_UNMATCHED_FOREGROUND_COLOR, attributes); } else if (state == State.FOLDER_NORMAL) { setColor(FOLDER_FOREGROUND_COLOR, attributes); } else if (state == State.FOLDER_SELECTED) { setColor(FOLDER_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.ARCHIVE_NORMAL) { setColor(ARCHIVE_FOREGROUND_COLOR, attributes); } else if (state == State.ARCHIVE_SELECTED) { setColor(ARCHIVE_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.SYMLINK_NORMAL) { setColor(SYMLINK_FOREGROUND_COLOR, attributes); } else if (state == State.SYMLINK_SELECTED) { setColor(SYMLINK_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.MARKED_NORMAL) { setColor(MARKED_FOREGROUND_COLOR, attributes); } else if (state == State.MARKED_SELECTED) { setColor(MARKED_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.EXECUTABLE_NORMAL) { setColor(EXECUTABLE_FOREGROUND_COLOR, attributes); } else if (state == State.EXECUTABLE_SELECTED) { setColor(EXECUTABLE_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.FILE_NORMAL) { setColor(FILE_FOREGROUND_COLOR, attributes); } else if (state == State.FILE_SELECTED) { setColor(FILE_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.SHELL_NORMAL) { setColor(SHELL_FOREGROUND_COLOR, attributes); } else if (state == State.SHELL_SELECTED) { setColor(SHELL_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.SHELL_HISTORY_NORMAL) { setColor(SHELL_HISTORY_FOREGROUND_COLOR, attributes); } else if (state == State.SHELL_HISTORY_SELECTED) { setColor(SHELL_HISTORY_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.TERMINAL_NORMAL) { setColor(TERMINAL_FOREGROUND_COLOR, attributes); } else if (state == State.TERMINAL_SELECTED) { setColor(TERMINAL_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.EDITOR_NORMAL) { setColor(EDITOR_FOREGROUND_COLOR, attributes); } else if (state == State.EDITOR_SELECTED) { setColor(EDITOR_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.LOCATION_BAR_NORMAL) { setColor(LOCATION_BAR_FOREGROUND_COLOR, attributes); } else if (state == State.LOCATION_BAR_SELECTED) { setColor(LOCATION_BAR_SELECTED_FOREGROUND_COLOR, attributes); } else if (state == State.STATUS_BAR) { setColor(STATUS_BAR_FOREGROUND_COLOR, attributes); } else if (state == State.QUICK_LIST_HEADER) { setColor(QUICK_LIST_HEADER_FOREGROUND_COLOR, attributes); } else if (state == State.QUICK_LIST_ITEM_NORMAL) { setColor(QUICK_LIST_ITEM_FOREGROUND_COLOR, attributes); } else if (state == State.QUICK_LIST_ITEM_SELECTED) { setColor(QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR, attributes); } else if (state == State.HEX_VIEWER_NORMAL) { setColor(HEX_VIEWER_HEX_FOREGROUND_COLOR, attributes); } else if (state == State.HEX_VIEWER_SELECTED) { setColor(HEX_VIEWER_SELECTED_DUMP_FOREGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } else if (qName.startsWith(ELEMENT_GROUP)) { if (state != State.FILE_GROUP) { traceIllegalDeclaration(qName); } String groupStr = qName.substring(ELEMENT_GROUP.length()); state = State.values()[State.GROUP_1.ordinal() + Integer.parseInt(groupStr) - 1]; } else if (qName.equals(ELEMENT_HEX_VIEWER)) { state = State.HEX_VIEWER; } else if (ELEMENT_ASCII_FOREGROUND.equals(qName)) { if (state == State.HEX_VIEWER_NORMAL) { setColor(HEX_VIEWER_ASCII_FOREGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } else if (ELEMENT_ASCII_BACKGROUND.equals(qName)) { if (state == State.HEX_VIEWER_SELECTED) { setColor(HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } else if (qName.equals(ELEMENT_OFFSET_FOREGROUND)) { if (state == State.HEX_VIEWER_NORMAL) { setColor(HEX_VIEWER_OFFSET_FOREGROUND_COLOR, attributes); } else { traceIllegalDeclaration(qName); } } else { traceIllegalDeclaration(qName); } } /** * Notifies the reader that the current element declaration is over. */ @Override public void endElement(String uri, String localName, String qName) { // If we're in an unknown element.... if (unknownElement != null) { // If it just closed, resume normal parsing. if (qName.equals(unknownElement)) { unknownElement = null; } else { return; // Ignores all other tags. } } // XML root element. if (ELEMENT_ROOT.equals(qName)) { state = State.UNKNOWN; }// File table declaration. else if (ELEMENT_TABLE.equals(qName)) { state = State.ROOT; } else if (ELEMENT_ALTERNATE.equals(qName)) { state = State.TABLE; } else if (ELEMENT_UNMATCHED.equals(qName)) { state = State.TABLE; } else if (qName.equals(ELEMENT_HIDDEN_FILE)) { state = State.TABLE; } else if (qName.equals(ELEMENT_HIDDEN_FOLDER)) { state = State.TABLE; } else if (ELEMENT_FOLDER.equals(qName)) { state = State.TABLE; } else if (ELEMENT_ARCHIVE.equals(qName)) { state = State.TABLE; } else if (ELEMENT_SYMLINK.equals(qName)) { state = State.TABLE; } else if (ELEMENT_MARKED.equals(qName)) { state = State.TABLE; } else if (ELEMENT_EXECUTABLE.equals(qName)) { state = State.TABLE; } else if (ELEMENT_FILE.equals(qName)) { state = State.TABLE; } else if (ELEMENT_SHELL.equals(qName)) { state = State.ROOT; } else if (ELEMENT_SHELL_HISTORY.equals(qName)) { state = State.ROOT; } else if (ELEMENT_TERMINAL.equals(qName)) { state = State.ROOT; } else if (ELEMENT_EDITOR.equals(qName)) { state = State.ROOT; } else if (ELEMENT_LOCATION_BAR.equals(qName)) { state = State.ROOT; } else if (ELEMENT_QUICK_LIST.equals(qName)) { state = State.ROOT; } else if (ELEMENT_STATUS_BAR.equals(qName)) { state = State.ROOT; } else if (ELEMENT_HEADER.equals(qName)) { if (state == State.QUICK_LIST_HEADER) { state = State.QUICK_LIST; } } else if (ELEMENT_ITEM.equals(qName)) { if (state == State.QUICK_LIST_ITEM) { state = State.QUICK_LIST; } } else if (ELEMENT_NORMAL.equals(qName)) { if (state == State.SHELL_NORMAL) { state = State.SHELL; } else if (state == State.SHELL_HISTORY_NORMAL) { state = State.SHELL_HISTORY; } else if (state == State.TERMINAL_NORMAL) { state = State.TERMINAL; } else if (state == State.HIDDEN_FILE_NORMAL) { state = State.HIDDEN_FILE; } else if (state == State.HIDDEN_FOLDER_NORMAL) { state = State.HIDDEN_FOLDER; } else if (state == State.FOLDER_NORMAL) { state = State.FOLDER; } else if (state == State.ARCHIVE_NORMAL) { state = State.ARCHIVE; } else if (state == State.SYMLINK_NORMAL) { state = State.SYMLINK; } else if (state == State.MARKED_NORMAL) { state = State.MARKED; } else if (state == State.EXECUTABLE_NORMAL) { state = State.EXECUTABLE; } else if (state == State.FILE_NORMAL) { state = State.FILE; } else if (state == State.EDITOR_NORMAL) { state = State.EDITOR; } else if (state == State.LOCATION_BAR_NORMAL) { state = State.LOCATION_BAR; } else if (state == State.TABLE_NORMAL) { state = State.TABLE; } else if (state == State.QUICK_LIST_ITEM_NORMAL) { state = State.QUICK_LIST_ITEM; } else if (state == State.HEX_VIEWER_NORMAL) { state = State.HEX_VIEWER; } } // Selected element declaration. else if (ELEMENT_SELECTED.equals(qName)) { if (state == State.SHELL_SELECTED) { state = State.SHELL; } else if (state == State.SHELL_HISTORY_SELECTED) { state = State.SHELL_HISTORY; } else if (state == State.TERMINAL_SELECTED) { state = State.TERMINAL; } else if (state == State.HIDDEN_FILE_SELECTED) { state = State.HIDDEN_FILE; } else if (state == State.HIDDEN_FOLDER_SELECTED) { state = State.HIDDEN_FOLDER; } else if (state == State.FOLDER_SELECTED) { state = State.FOLDER; } else if (state == State.ARCHIVE_SELECTED) { state = State.ARCHIVE; } else if (state == State.SYMLINK_SELECTED) { state = State.SYMLINK; } else if (state == State.MARKED_SELECTED) { state = State.MARKED; } else if (state == State.EXECUTABLE_SELECTED) { state = State.EXECUTABLE; } else if (state == State.FILE_SELECTED) { state = State.FILE; } else if (state == State.EDITOR_SELECTED) { state = State.EDITOR; } else if (state == State.LOCATION_BAR_SELECTED) { state = State.LOCATION_BAR; } else if (state == State.TABLE_SELECTED) { state = State.TABLE; } else if (state == State.QUICK_LIST_ITEM_SELECTED) { state = State.QUICK_LIST_ITEM; } else if (state == State.HEX_VIEWER_SELECTED) { state = State.HEX_VIEWER; } } else if (qName.startsWith(ELEMENT_GROUP)) { state = State.FILE_GROUP; } else if (ELEMENT_HEX_VIEWER.equals(qName)) { state = State.ROOT; } else if (ELEMENT_FILE_GROUPS.equals(qName)) { state = State.ROOT; } } // - Helper methods ------------------------------------------------------ // ----------------------------------------------------------------------- /** * Checks whether the specified font is available on the system. * @param font name of the font to check for. * @return true if the font is available, false otherwise. */ private static boolean isFontAvailable(String font) { // Looks for the specified font. // TODO very slow operation (for first execution) !!!! String[] availableFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); for (String availableFont : availableFonts) { if (availableFont.equalsIgnoreCase(font)) { return true; } } // Font doesn't exist on the system. return false; } /** * Creates a font from the specified XML attributes. *

    * Ignored attributes will be set to their default values. * * @param attributes XML attributes describing the font to use. * @return the resulting Font instance. */ private static Font createFont(Attributes attributes) { // Computes the font style. int style = getFontStyle(attributes); // Computes the font size. String buffer = attributes.getValue(ATTRIBUTE_SIZE); if (buffer == null) { getLogger().debug("Missing font size attribute in theme, ignoring."); return null; } int size = Integer.parseInt(buffer); // Computes the font family. buffer = attributes.getValue(ATTRIBUTE_FAMILY); if (buffer == null) { getLogger().debug("Missing font family attribute in theme, ignoring."); return null; } // Looks through the list of declared fonts to find one that is installed on the system. StringTokenizer parser = new StringTokenizer(buffer, ","); while (parser.hasMoreTokens()) { buffer = parser.nextToken().trim(); // Font was found, use it. if (isFontAvailable(buffer)) { return new Font(buffer, style, size); } } // No font was found, instructs the ThemeManager to use the system default. getLogger().debug("Requested font families are not installed on the system, using default."); return null; } @MagicConstant(flags = {Font.BOLD, Font.ITALIC}) private static int getFontStyle(Attributes attributes) { int style = 0; String buffer = attributes.getValue(ATTRIBUTE_BOLD); if (VALUE_TRUE.equals(buffer)) { style |= Font.BOLD; } buffer = attributes.getValue(ATTRIBUTE_ITALIC); if (VALUE_TRUE.equals(buffer)) { style |= Font.ITALIC; } return style; } /** * Creates a color from the specified XML attributes. * @param attributes XML attributes describing the font to use. * @return the resulting Color instance. */ private static Color createColor(Attributes attributes) { String buffer = attributes.getValue(ATTRIBUTE_COLOR); // Retrieves the color attribute's value. if (buffer == null) { getLogger().debug("Missing color attribute in theme, ignoring."); return null; } int color = Integer.parseInt(buffer, 16); // Retrieves the transparency attribute's value.. buffer = attributes.getValue(ATTRIBUTE_ALPHA); if (buffer == null) { return new Color(color); } return new Color(color | (Integer.parseInt(buffer, 16) << 24), true); } /** * Set color helper method * @param id color id * @param attributes xml attributes */ private void setColor(int id, Attributes attributes) { template.setColorFast(id, createColor(attributes)); } /** * Set font helper method * @param id font id * @param attributes xml attributes */ private void setFont(int id, Attributes attributes) { template.setFontFast(id, createFont(attributes)); } // - Error generation methods -------------------------------------------- // ----------------------------------------------------------------------- private void traceIllegalDeclaration(String element) { System.out.println("Unexpected start of element " + element + " for state " + state + ", ignoring."); new Exception().printStackTrace(); unknownElement = element; getLogger().debug("Unexpected start of element " + element + " for state " + state + ", ignoring."); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(ThemeReader.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeWriter.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; import com.mucommander.utils.xml.XmlAttributes; import com.mucommander.utils.xml.XmlWriter; import java.awt.Color; import java.awt.Font; import java.io.IOException; import java.io.OutputStream; /** * Class used to save themes in XML format. * @author Nicolas Rinaudo */ class ThemeWriter implements ThemeXmlConstants, ThemeId { private ThemeWriter() {} /** * Saves the specified theme to the specified output stream. * @param theme theme to save. * @param stream where to write the theme to. * @throws IOException thrown if any IO related error occurs. */ public static void write(ThemeData theme, OutputStream stream) throws IOException { XmlWriter out = new XmlWriter(stream); out.startElement(ELEMENT_ROOT); out.println(); // - File table description ------------------------------------------------------ // ------------------------------------------------------------------------------- out.startElement(ELEMENT_TABLE); out.println(); // Global values. writeColor(theme, out, FILE_TABLE_BORDER_COLOR, ELEMENT_BORDER); writeColor(theme, out, FILE_TABLE_INACTIVE_BORDER_COLOR, ELEMENT_INACTIVE_BORDER); writeColor(theme, out, FILE_TABLE_SELECTED_OUTLINE_COLOR, ELEMENT_OUTLINE); writeColor(theme, out, FILE_TABLE_INACTIVE_SELECTED_OUTLINE_COLOR, ELEMENT_INACTIVE_OUTLINE); writeFont(theme, out, FILE_TABLE_FONT, ELEMENT_FONT); // Normal background colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, FILE_TABLE_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, FILE_TABLE_INACTIVE_BACKGROUND_COLOR, ELEMENT_INACTIVE_BACKGROUND); out.endElement(ELEMENT_NORMAL); // Selected background colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, FILE_TABLE_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, FILE_TABLE_INACTIVE_SELECTED_BACKGROUND_COLOR, ELEMENT_INACTIVE_BACKGROUND); writeColor(theme, out, FILE_TABLE_INACTIVE_SELECTED_SECONDARY_BACKGROUND_COLOR, ELEMENT_INACTIVE_SECONDARY_BACKGROUND); writeColor(theme, out, FILE_TABLE_SELECTED_SECONDARY_BACKGROUND_COLOR, ELEMENT_SECONDARY_BACKGROUND); out.endElement(ELEMENT_SELECTED); // Alternate background colors. out.startElement(ELEMENT_ALTERNATE); out.println(); writeColor(theme, out, FILE_TABLE_ALTERNATE_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, FILE_TABLE_INACTIVE_ALTERNATE_BACKGROUND_COLOR, ELEMENT_INACTIVE_BACKGROUND); out.endElement(ELEMENT_ALTERNATE); // Unmatched colors. out.startElement(ELEMENT_UNMATCHED); out.println(); writeColor(theme, out, FILE_TABLE_UNMATCHED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, FILE_TABLE_UNMATCHED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_UNMATCHED); // Hidden folders. out.startElement(ELEMENT_HIDDEN_FOLDER); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, HIDDEN_FOLDER_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, HIDDEN_FOLDER_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, HIDDEN_FOLDER_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, HIDDEN_FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_HIDDEN_FOLDER); // Hidden files. out.startElement(ELEMENT_HIDDEN_FILE); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, HIDDEN_FILE_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, HIDDEN_FILE_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, HIDDEN_FILE_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, HIDDEN_FILE_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_HIDDEN_FILE); // Folders. out.startElement(ELEMENT_FOLDER); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, FOLDER_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, FOLDER_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, FOLDER_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, FOLDER_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_FOLDER); // Archives. out.startElement(ELEMENT_ARCHIVE); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, ARCHIVE_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, ARCHIVE_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, ARCHIVE_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, ARCHIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_ARCHIVE); // Symlink. out.startElement(ELEMENT_SYMLINK); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, SYMLINK_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, SYMLINK_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, SYMLINK_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, SYMLINK_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_SYMLINK); // Marked files. out.startElement(ELEMENT_MARKED); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, MARKED_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, MARKED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, MARKED_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, MARKED_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_MARKED); // Executable files. out.startElement(ELEMENT_EXECUTABLE); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, EXECUTABLE_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, EXECUTABLE_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, EXECUTABLE_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, EXECUTABLE_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_EXECUTABLE); // Plain files. out.startElement(ELEMENT_FILE); out.println(); out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, FILE_INACTIVE_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, FILE_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, FILE_INACTIVE_SELECTED_FOREGROUND_COLOR, ELEMENT_INACTIVE_FOREGROUND); writeColor(theme, out, FILE_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_FILE); out.endElement(ELEMENT_TABLE); // File groups out.startElement(ELEMENT_FILE_GROUPS); out.println(); for (int i = 0; i < 10; i++) { out.startElement(ELEMENT_GROUP + (i+1)); out.println(); writeColor(theme, out, FILE_GROUP_1_FOREGROUND_COLOR+i, ELEMENT_NORMAL); out.endElement(ELEMENT_GROUP + (i+1)); } out.endElement(ELEMENT_FILE_GROUPS); // - Shell description ---------------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_SHELL); out.println(); writeFont(theme, out, SHELL_FONT, ELEMENT_FONT); // Normal colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, SHELL_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, SHELL_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, SHELL_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, SHELL_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_SHELL); // - Shell history description --------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_SHELL_HISTORY); out.println(); writeFont(theme, out, SHELL_HISTORY_FONT, ELEMENT_FONT); // Normal colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, SHELL_HISTORY_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, SHELL_HISTORY_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, SHELL_HISTORY_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, SHELL_HISTORY_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_SHELL_HISTORY); // - Terminal description --------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_TERMINAL); out.println(); writeFont(theme, out, TERMINAL_FONT, ELEMENT_FONT); // Normal colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, TERMINAL_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, TERMINAL_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, TERMINAL_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, TERMINAL_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_TERMINAL); // - Editor description ---------------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_EDITOR); out.println(); writeFont(theme, out, EDITOR_FONT, ELEMENT_FONT); // Normal colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, EDITOR_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, EDITOR_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, EDITOR_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, EDITOR_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); // Current line out.startElement(ELEMENT_CURRENT); out.println(); writeColor(theme, out, EDITOR_CURRENT_BACKGROUND_COLOR, ELEMENT_BACKGROUND); out.endElement(ELEMENT_CURRENT); out.endElement(ELEMENT_EDITOR); // - Hex viewer description ------------------------------------------------------ // ------------------------------------------------------------------------------- out.startElement(ELEMENT_HEX_VIEWER); out.println(); writeFont(theme, out, HEX_VIEWER_FONT, ELEMENT_FONT); // Normal colors out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, HEX_VIEWER_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, HEX_VIEWER_HEX_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR, ELEMENT_SECONDARY_BACKGROUND); writeColor(theme, out, HEX_VIEWER_ASCII_FOREGROUND_COLOR, ELEMENT_ASCII_FOREGROUND); writeColor(theme, out, HEX_VIEWER_OFFSET_FOREGROUND_COLOR, ELEMENT_OFFSET_FOREGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, HEX_VIEWER_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, HEX_VIEWER_SELECTED_DUMP_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR, ELEMENT_ASCII_BACKGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_HEX_VIEWER); // - Location bar description ---------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_LOCATION_BAR); out.println(); writeFont(theme, out, LOCATION_BAR_FONT, ELEMENT_FONT); writeColor(theme, out, LOCATION_BAR_PROGRESS_COLOR, ELEMENT_PROGRESS); // Normal colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, LOCATION_BAR_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, LOCATION_BAR_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, LOCATION_BAR_SELECTED_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, LOCATION_BAR_SELECTED_FOREGROUND_COLOR, ELEMENT_FOREGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_LOCATION_BAR); // - Volume label description ---------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_STATUS_BAR); out.println(); // Font. writeFont(theme, out, STATUS_BAR_FONT, ELEMENT_FONT); // Colors. writeColor(theme, out, STATUS_BAR_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, STATUS_BAR_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, STATUS_BAR_BORDER_COLOR, ELEMENT_BORDER); writeColor(theme, out, STATUS_BAR_OK_COLOR, ELEMENT_OK); writeColor(theme, out, STATUS_BAR_WARNING_COLOR, ELEMENT_WARNING); writeColor(theme, out, STATUS_BAR_CRITICAL_COLOR, ELEMENT_CRITICAL); out.endElement(ELEMENT_STATUS_BAR); // - Quick list label description ---------------------------------------------------- // ------------------------------------------------------------------------------- out.startElement(ELEMENT_QUICK_LIST); out.println(); // Quick list header out.startElement(ELEMENT_HEADER); out.println(); // Font. writeFont(theme, out, QUICK_LIST_HEADER_FONT, ELEMENT_FONT); // Colors. writeColor(theme, out, QUICK_LIST_HEADER_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, QUICK_LIST_HEADER_BACKGROUND_COLOR, ELEMENT_BACKGROUND); writeColor(theme, out, QUICK_LIST_HEADER_SECONDARY_BACKGROUND_COLOR, ELEMENT_SECONDARY_BACKGROUND); out.endElement(ELEMENT_HEADER); // Quick list item out.startElement(ELEMENT_ITEM); out.println(); // Font. writeFont(theme, out, QUICK_LIST_ITEM_FONT, ELEMENT_FONT); // Colors. // Normal colors. out.startElement(ELEMENT_NORMAL); out.println(); writeColor(theme, out, QUICK_LIST_ITEM_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, QUICK_LIST_ITEM_BACKGROUND_COLOR, ELEMENT_BACKGROUND); out.endElement(ELEMENT_NORMAL); // Selected colors. out.startElement(ELEMENT_SELECTED); out.println(); writeColor(theme, out, QUICK_LIST_SELECTED_ITEM_FOREGROUND_COLOR, ELEMENT_FOREGROUND); writeColor(theme, out, QUICK_LIST_SELECTED_ITEM_BACKGROUND_COLOR, ELEMENT_BACKGROUND); out.endElement(ELEMENT_SELECTED); out.endElement(ELEMENT_ITEM); out.endElement(ELEMENT_QUICK_LIST); out.endElement(ELEMENT_ROOT); } // - Helper methods ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Returns the XML attributes describing the specified font. * @param font font to described as XML attributes. * @return the XML attributes describing the specified font. */ private static XmlAttributes getFontAttributes(Font font) { XmlAttributes attributes = new XmlAttributes(); // Stores the font's description. // Font family and size. attributes.add(ATTRIBUTE_FAMILY, font.getFamily()); attributes.add(ATTRIBUTE_SIZE, Integer.toString(font.getSize())); // Font style. if (font.isBold()) { attributes.add(ATTRIBUTE_BOLD, VALUE_TRUE); } if (font.isItalic()) { attributes.add(ATTRIBUTE_ITALIC, VALUE_TRUE); } return attributes; } /** * Returns the XML attributes describing the specified color. * @param color color to described as XML attributes. * @return the XML attributes describing the specified color. */ private static XmlAttributes getColorAttributes(Color color) { StringBuilder buffer = new StringBuilder(); // Used to build the color's string representation. // Red component. if (color.getRed() < 16) { buffer.append('0'); } buffer.append(Integer.toString(color.getRed(), 16)); // Green component. if (color.getGreen() < 16) { buffer.append('0'); } buffer.append(Integer.toString(color.getGreen(), 16)); // Blue component. if (color.getBlue() < 16) { buffer.append('0'); } buffer.append(Integer.toString(color.getBlue(), 16)); // Builds the XML attributes. XmlAttributes attributes = new XmlAttributes(); // Stores the color's description. attributes.add(ATTRIBUTE_COLOR, buffer.toString()); if (color.getAlpha() != 255) { buffer.setLength(0); if (color.getAlpha() < 16) { buffer.append('0'); } buffer.append(Integer.toString(color.getAlpha(), 16)); attributes.add(ATTRIBUTE_ALPHA, buffer.toString()); } return attributes; } private static void writeColor(ThemeData theme, XmlWriter out, int colorId, String name) throws IOException { if (theme.isColorSet(colorId)) { out.writeStandAloneElement(name, getColorAttributes(theme.getColor(colorId))); } } private static void writeFont(ThemeData theme, XmlWriter out, int fontId, String name) throws IOException { if (theme.isFontSet(fontId)) { out.writeStandAloneElement(name, getFontAttributes(theme.getFont(fontId))); } } } ================================================ FILE: src/main/java/com/mucommander/ui/theme/ThemeXmlConstants.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.theme; /** * Defines the format of the XML theme files. * @author Nicolas Rinaudo */ interface ThemeXmlConstants { // - Main elements ------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** XML theme file root element. */ String ELEMENT_ROOT = "theme"; /** File table description element. */ String ELEMENT_TABLE = "file_table"; /** Shell description element. */ String ELEMENT_SHELL = "shell"; /** File editor description element. */ String ELEMENT_EDITOR = "editor"; /** Location bar description element. */ String ELEMENT_LOCATION_BAR = "location_bar"; /** Shell history description element. */ String ELEMENT_SHELL_HISTORY = "shell_history"; /** Volume label description element. */ String ELEMENT_STATUS_BAR = "status_bar"; /** Quick list label description element. */ String ELEMENT_QUICK_LIST = "quick_list"; String ELEMENT_FILE_GROUPS = "file_groups"; String ELEMENT_GROUP = "group"; String ELEMENT_TERMINAL = "terminal"; String ELEMENT_HEX_VIEWER = "hex_viewer"; // - Status element ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** Item normal state description element. */ String ELEMENT_NORMAL = "normal"; /** Item selected state description element. */ String ELEMENT_SELECTED = "selected"; /** Item alternate state description element. */ String ELEMENT_ALTERNATE = "alternate"; /** Item unmatched state description element. */ String ELEMENT_UNMATCHED = "unmatched"; /** Item current line state description element. */ String ELEMENT_CURRENT = "current"; // - Quick list element ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** Quick list header state description element. */ String ELEMENT_HEADER = "header"; /** Quick list item state description element. */ String ELEMENT_ITEM = "item"; // - Font element -------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Font description element. */ String ELEMENT_FONT = "font"; /** Font family attribute. */ String ATTRIBUTE_FAMILY = "family"; /** Font size attribute. */ String ATTRIBUTE_SIZE = "size"; /** Font bold attribute. */ String ATTRIBUTE_BOLD = "bold"; /** Font italic attribute. */ String ATTRIBUTE_ITALIC = "italic"; /** true value. */ String VALUE_TRUE = "true"; /** false value. */ String VALUE_FALSE = "false"; // - Color elements ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- String ELEMENT_INACTIVE_BACKGROUND = "inactive_background"; String ELEMENT_INACTIVE_SECONDARY_BACKGROUND = "inactive_secondary_background"; String ELEMENT_INACTIVE_FOREGROUND = "inactive_foreground"; String ELEMENT_BACKGROUND = "background"; String ELEMENT_SECONDARY_BACKGROUND = "secondary_background"; String ELEMENT_FOREGROUND = "foreground"; String ELEMENT_HIDDEN_FILE = "hidden_file"; String ELEMENT_HIDDEN_FOLDER = "hidden_folder"; String ELEMENT_FOLDER = "folder"; String ELEMENT_ARCHIVE = "archive"; String ELEMENT_SYMLINK = "symlink"; String ELEMENT_MARKED = "marked"; String ELEMENT_EXECUTABLE = "executable"; String ELEMENT_FILE = "file"; String ELEMENT_PROGRESS = "progress"; String ELEMENT_BORDER = "border"; String ELEMENT_INACTIVE_BORDER = "inactive_border"; String ELEMENT_OUTLINE = "outline"; String ELEMENT_INACTIVE_OUTLINE = "inactive_outline"; String ELEMENT_OK = "ok"; String ELEMENT_WARNING = "warning"; String ELEMENT_CRITICAL = "critical"; String ELEMENT_ASCII_FOREGROUND = "ascii_foreground"; String ELEMENT_ASCII_BACKGROUND = "ascii_background"; String ELEMENT_OFFSET_FOREGROUND = "offset_foreground"; String ATTRIBUTE_COLOR = "color"; String ATTRIBUTE_ALPHA = "alpha"; } ================================================ FILE: src/main/java/com/mucommander/ui/theme/package.html ================================================ API used to customise the fonts and colors of a Swing UI. ================================================ FILE: src/main/java/com/mucommander/ui/tools/ToolsEnvironment.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tools; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * @author Oleg Trifonov * Created on 13/05/16. */ public class ToolsEnvironment { private static final Map customEnvironment = new HashMap<>(); public static void load() throws IOException { AbstractFile configPath = PlatformManager.getPreferencesFolder().getChild("env.properties"); if (configPath != null && configPath.exists()) { try (InputStream is = configPath.getInputStream()) { Properties properties = new Properties(); properties.load(is); for (Object key: properties.keySet()) { String name = key.toString(); customEnvironment.put(name, properties.getProperty(name)); } } } } public static Map getCustomEnvironment() { return customEnvironment; } public static String getEnv(String name) { String result = customEnvironment.get(name); if (result != null) { return result; } return System.getenv(name); } } ================================================ FILE: src/main/java/com/mucommander/ui/tools/ToolsSetupDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.tools; import com.mucommander.ui.dialog.FocusDialog; import java.awt.Frame; /** * @author Oleg Trifonov * Created on 09/02/16. */ public class ToolsSetupDialog extends FocusDialog { public ToolsSetupDialog(Frame owner) { super(owner, i18n("tools_setup_dialog.title"), null); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/EditorFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import com.mucommander.commons.file.AbstractFile; /** * A common interface for instantiating {@link FileEditor} implementations, and finding out if a editor is capable * of editing a particular file. * * @author Nicolas Rinaudo, Maxence Bernard */ public interface EditorFactory { /** * Returns true if this factory can create a file editor for the specified file. *

    * The FileEditor may base its decision strictly upon the file's name and its extension or may wish to read some of * the file and compare it to a magic number. * * @param file file for which a editor must be created. * @throws WarnUserException if the specified file can be edited after the warning message contained in the * exception is displayed to the end user. * @return true if this factory can create a file editor for the specified file. */ boolean canEditFile(AbstractFile file) throws WarnUserException; /** * Returns a new instance of {@link FileEditor}. * @return a new instance of {@link FileEditor}. */ FileEditor createFileEditor(); /** * Returns a name for EditAs list * */ String getName(); } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/EditorFrame.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import java.awt.Dimension; import java.awt.Image; import com.mucommander.commons.file.AbstractFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; /** * A specialized JFrame that displays a {@link FileEditor} for a given file and provides some common * editing functionalities. The {@link FileEditor} instance is provided by {@link EditorRegistrar}. * * @author Maxence Bernard, Arik Hadas */ public class EditorFrame extends FileFrame { private FileEditor editor; private final static Dimension MIN_DIMENSION = new Dimension(500, 360); /** * Creates a new EditorFrame to start viewing the given file. * *

    This constructor has package access only, EditorFrame can to be created by * {@link EditorRegistrar#createEditorFrame(MainFrame,AbstractFile,Image)}. */ EditorFrame(MainFrame mainFrame, AbstractFile file, Image icon) { super(mainFrame, icon); initContentPane(file); } @Override public Dimension getMinimumSize() { return MIN_DIMENSION; } @Override public void dispose() { // Returns true if the file does not have any unsaved change or if the user refused to save the changes if (editor == null || editor.askSave()) { super.dispose(); } } @Override protected FilePresenter createFilePresenter(AbstractFile file) throws UserCancelledException { return editor = EditorRegistrar.createFileEditor(file, EditorFrame.this); } @Override protected String getGenericErrorDialogTitle() { return Translator.get("file_editor.edit_error_title"); } @Override protected String getGenericErrorDialogMessage() { return Translator.get("file_editor.edit_error"); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/EditorRegistrar.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import java.awt.Frame; import java.awt.Image; import java.io.IOException; import java.util.ArrayList; import java.util.List; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.viewer.text.TextEditor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import javax.swing.*; /** * EditorRegistrar maintains a list of registered file editors and provides methods to dynamically register file editors * and create appropriate FileEditor (Panel) and EditorFrame (Window) instances for a given AbstractFile. * * @author Maxence Bernard */ @Slf4j public class EditorRegistrar { /** List of registered file editors */ private final static List editorFactories = new ArrayList<>(); static { registerFileEditor(new com.mucommander.ui.viewer.text.TextFactory()); } /** * Registers a FileEditor. * @param factory file editor factory to register. */ private static void registerFileEditor(EditorFactory factory) { editorFactories.add(factory); } /** * Creates and returns an EditorFrame to start viewing the given file. The EditorFrame will be monitored * so that if it is the last window on screen when it is closed by the user, it will trigger the shutdown sequence. * * @param mainFrame the parent MainFrame instance * @param file the file that will be displayed by the returned EditorFrame * @param icon editor frame's icon. * @param createListener postponed action */ public static void createEditorFrame(MainFrame mainFrame, AbstractFile file, Image icon, FileFrameCreateListener createListener) { // Check if this file is already opened if (showEditorIfAlreadyOpen(file, createListener)) { return; } new FilePreloadWorker(file, mainFrame, () -> { EditorFrame frame = new EditorFrame(mainFrame, file, icon); // Use new Window decorations introduced in Mac OS X 10.5 (Leopard) if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { // Displays the document icon in the window title bar, works only for local files if (file.getURL().getScheme().equals(FileProtocols.FILE)) { frame.getRootPane().putClientProperty("Window.documentFile", file.getUnderlyingFileObject()); } } // WindowManager will listen to window closed events to trigger shutdown sequence // if it is the last window visible frame.addWindowListener(WindowManager.getInstance()); if (createListener != null) { createListener.onCreate(frame); } }).execute(); } private static boolean showEditorIfAlreadyOpen(AbstractFile file, FileFrameCreateListener createListener) { for (FileViewersList.FileRecord fr: FileViewersList.getFiles()) { if (fr.fileName.equals(file.getAbsolutePath()) && fr.viewerClass != null) { Class viewerClass = fr.viewerClass; if (viewerClass.equals(TextEditor.class)) { FileFrame openedFrame = fr.fileFrameRef.get(); if (openedFrame != null) { openedFrame.toFront(); if (createListener != null) { createListener.onCreate(openedFrame); } } return true; } } } return false; } public static void createEditorFrame(MainFrame mainFrame, AbstractFile file, Image icon) { createEditorFrame(mainFrame, file, icon, null); } /** * Creates and returns an appropriate FileEditor for the given file type. * * @param file the file that will be displayed by the returned FileEditor * @param frame the frame in which the FileEditor is shown * @return the created FileEditor, or null if no suitable editor was found * @throws UserCancelledException if the user has been asked to confirm the operation and canceled */ static FileEditor createFileEditor(AbstractFile file, EditorFrame frame) throws UserCancelledException { FileEditor editor = createFileEditor(file); if (editor != null) { editor.setFrame(frame); } return editor; } @Nullable private static FileEditor createFileEditor(AbstractFile file) throws UserCancelledException { for (EditorFactory factory : editorFactories) { try { if (factory.canEditFile(file)) { return factory.createFileEditor(); } } catch (WarnUserException e) { showQuestionDialog(file, e); // User confirmed the operation return factory.createFileEditor(); } } return null; } private static void showQuestionDialog(AbstractFile file, WarnUserException e) throws UserCancelledException { QuestionDialog dialog = new QuestionDialog((Frame)null, Translator.get("warning"), Translator.get(e.getMessage()), null, new String[] {Translator.get("file_editor.open_anyway"), Translator.get("cancel")}, new int[] {0, 1}, 0); int ret = dialog.getActionValue(); if (ret == 1 || ret == -1) { // User canceled the operation try { file.closePushbackInputStream(); } catch (IOException e1) { log.error("IO error", e); } throw new UserCancelledException(); } } public static List getAllEditors(AbstractFile file) { List result = new ArrayList<>(); for (EditorFactory factory : editorFactories) { try { if (!factory.canEditFile(file)) { continue; } } catch (WarnUserException ignore) {} result.add(factory); } return result; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FileEditor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import com.mucommander.commons.file.*; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.job.FileCollisionChecker; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.dialog.file.FileCollisionDialog; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.utils.text.Translator; import ru.trolsoft.ui.TMenuSeparator; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; /** * An abstract class to be subclassed by file editor implementations. * *

    Warning: the file viewer/editor API may soon receive a major overhaul. * * @author Maxence Bernard, Arik Hadas */ public abstract class FileEditor extends FilePresenter implements ActionListener { /** Menu items */ protected JMenu menuFile; private JMenuItem miSave; private JMenuItem miSaveAs; private JMenuItem miClose; /** Serves to indicate if saving is needed before closing the window, value should only be modified using the setSaveNeeded() method */ private boolean saveNeeded; /** * Creates a new FileEditor. */ protected FileEditor() { } protected void setSaveNeeded(boolean saveNeeded) { if (getFrame() == null || this.saveNeeded == saveNeeded) { return; } if (!this.saveNeeded) { getStatusBar().setStatusMessage(Translator.get("text_editor.modified")); } this.saveNeeded = saveNeeded; // Marks/unmarks the window as dirty under Mac OS X (symbolized by a dot in the window closing icon) if (OsFamily.MAC_OS_X.isCurrent()) { getFrame().getRootPane().putClientProperty("windowModified", saveNeeded); } } private boolean trySaveAs() { AbstractFile destFile = choiceFileToSave(); if (destFile != null && checkCollision(destFile) && trySave(destFile)) { setCurrentFile(destFile); return true; } return false; } private AbstractFile choiceFileToSave() { JFileChooser fileChooser = new JFileChooser(); AbstractFile currentFile = getCurrentFile(); // Sets selected file in JFileChooser to current file if (currentFile.getURL().getScheme().equals(FileProtocols.FILE)) { fileChooser.setSelectedFile(new File(currentFile.getAbsolutePath())); } fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); int ret = fileChooser.showSaveDialog(getFrame()); if (ret == JFileChooser.APPROVE_OPTION) { AbstractFile destFile; try { destFile = FileFactory.getFile(fileChooser.getSelectedFile().getAbsolutePath(), true); } catch (IOException e) { InformationDialog.showErrorDialog(getFrame(), Translator.get("write_error"), Translator.get("file_editor.cannot_write")); return null; } if (checkCollision(destFile) && trySave(destFile)) { setCurrentFile(destFile); return destFile; } } return null; } private boolean checkCollision(AbstractFile destFile) { // Check for file collisions, i.e. if the file already exists in the destination int collision = FileCollisionChecker.checkForCollision(null, destFile); if (collision != FileCollisionChecker.NO_COLLISION) { // File already exists in destination, ask the user what to do (cancel, overwrite,...) but // do not offer the multiple files mode options such as 'skip' and 'apply to all'. int action = new FileCollisionDialog(getFrame(), getFrame()/*mainFrame*/, collision, null, destFile, false, false).getActionValue(); // User chose to overwrite the file return action == FileCollisionDialog.OVERWRITE_ACTION; // User chose to cancel or closed the dialog } return true; } // Returns false if an error occurred while saving the file. private boolean trySave(AbstractFile destFile) { try { saveAs(destFile); return true; } catch (IOException e) { if (e instanceof FileNotFoundException) { if (!destFile.getPermissions().getBitValue(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION)) { return trySaveReadOnly(destFile); } } getStatusBar().setStatusMessage(Translator.get("text_editor.cant_save_file")); InformationDialog.showErrorDialog(getFrame(), Translator.get("write_error"), Translator.get("file_editor.cannot_write")); return false; } } private void forceOverwriteFile(AbstractFile file) throws IOException { FilePermissions savedPermissions = file.getPermissions(); file.changePermission(PermissionAccesses.USER_ACCESS, PermissionTypes.WRITE_PERMISSION, true); saveAs(file); file.changePermissions(savedPermissions); } private boolean trySaveReadOnly(AbstractFile destFile) { QuestionDialog dialog = new QuestionDialog((Frame)null, Translator.get("warning"), Translator.get("file_editor.overwrite_readonly"), getFrame(), new String[] {Translator.get("file_editor.save_anyway"), Translator.get("file_editor.save_as"), Translator.get("cancel")}, new int[] {0, 1, JOptionPane.CLOSED_OPTION}, 0); int ret = dialog.getActionValue(); if (ret == 0) { // Overwrite try { forceOverwriteFile(destFile); return true; } catch (IOException e) { getStatusBar().setStatusMessage(Translator.get("text_editor.cant_save_file")); InformationDialog.showErrorDialog(getFrame(), Translator.get("write_error"), Translator.get("file_editor.cannot_write")); } } else if (ret == 1) { // Save as... if (trySaveAs() ) { return true; } } getStatusBar().setStatusMessage(Translator.get("text_editor.cant_save_file")); return false; } // Returns true if the file does not have any unsaved change or if the user refused to save the changes, // false if the user canceled the dialog or the save failed. protected boolean askSave() { if (!saveNeeded) { return true; } QuestionDialog dialog = new QuestionDialog(getFrame(), null, Translator.get("file_editor.save_warning"), getFrame(), new String[] {Translator.get("save"), Translator.get("dont_save"), Translator.get("cancel")}, new int[] {JOptionPane.YES_OPTION, JOptionPane.NO_OPTION, JOptionPane.CANCEL_OPTION}, 0); int ret = dialog.getActionValue(); if ((ret == JOptionPane.YES_OPTION && trySave(getCurrentFile())) || ret == JOptionPane.NO_OPTION) { setSaveNeeded(false); return true; } return false; // User canceled or the file couldn't be properly saved } /** * Returns the menu bar that controls the editor's frame. The menu bar should be retrieved using this method and * not by calling {@link JFrame#getJMenuBar()}, which may return null. * * @return the menu bar that controls the editor's frame. */ public JMenuBar getMenuBar() { JMenuBar menuBar = new JMenuBar(); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // File menu menuFile = MenuToolkit.addMenu(Translator.get("file_editor.file_menu"), mnemonicHelper, null); if (OsFamily.MAC_OS_X.isCurrent()) { miSave = MenuToolkit.addMenuItem(menuFile, Translator.get("file_editor.save"), mnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.META_DOWN_MASK), this); } else { miSave = MenuToolkit.addMenuItem(menuFile, Translator.get("file_editor.save"), mnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK), this); } miSaveAs = MenuToolkit.addMenuItem(menuFile, Translator.get("file_editor.save_as"), mnemonicHelper, null, this); menuFile.add(new TMenuSeparator()); miClose = MenuToolkit.addMenuItem(menuFile, Translator.get("file_editor.close"), mnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), this); menuBar.add(menuFile); return menuBar; } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // File menu if (source == miSave) { trySave(getCurrentFile()); } else if (source == miSaveAs) { trySaveAs(); } else if (source == miClose) { getFrame().dispose(); } } /** * This method is invoked when the user asked to save current file to the specified file. * * * @param saveAsFile the file which should be used to save the file currently being edited * (path can be different from current file if the user chose 'Save as'). * @throws IOException if an I/O error occurs. */ protected abstract void saveAs(AbstractFile saveAsFile) throws IOException; } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FileFrame.java ================================================ package com.mucommander.ui.viewer; import java.awt.*; import java.awt.event.KeyEvent; import java.io.IOException; import javax.swing.*; import com.mucommander.cache.WindowsStorage; import com.mucommander.ui.macosx.IMacOsWindow; import com.mucommander.ui.quicklist.QuickListContainer; import org.fife.ui.StatusBar; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.helper.FocusRequester; import com.mucommander.ui.layout.AsyncPanel; import com.mucommander.ui.main.MainFrame; /** * This class is used as an abstraction for the {@link EditorFrame} and {@link ViewerFrame}. * * @author Arik Hadas */ public abstract class FileFrame extends JFrame implements QuickListContainer, IMacOsWindow { private static final Logger LOGGER = LoggerFactory.getLogger(FileFrame.class); /** * The file presenter within this frame */ private FilePresenter filePresenter; /** * The main frame from which this frame was initiated */ private final MainFrame mainFrame; private Component returnFocusTo; FileFrame(MainFrame mainFrame, Image icon) { this.mainFrame = mainFrame; setIconImage(icon); initLookAndFeel(); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setResizable(true); //FileViewersList.update(); //initContentPane(file); } void initContentPane(final AbstractFile file) { try { filePresenter = createFilePresenter(file); } catch (UserCancelledException e) { LOGGER.error("Operation canceled by user", e); // May get a UserCancelledException if the user canceled (refused to confirm the operation after a warning) return; } // If not suitable presenter was found for the given file if (filePresenter == null) { showGenericErrorDialog(); return; } JComponent asyncPanel = createAsyncPanel(file); // Add the AsyncPanel to the content pane JPanel contentPane = new JPanel(new BorderLayout()); contentPane.add(asyncPanel, BorderLayout.CENTER); //contentPane.add(filePresenter, BorderLayout.CENTER); // Add status bar if exists StatusBar statusBar = filePresenter.getStatusBar(); if (statusBar != null) { contentPane.add(statusBar, BorderLayout.SOUTH); } setContentPane(contentPane); //setSize(WAIT_DIALOG_SIZE); //setFullScreenSize(); //setFullScreen(true); if (!WindowsStorage.getInstance().init(this, filePresenter.getClass().getCanonicalName(), true)) { setSize(800, 600); DialogToolkit.centerOnWindow(this, mainFrame.getJFrame()); } setVisible(true); FileViewersList.update(); } @NotNull private JComponent createAsyncPanel(AbstractFile file) { if (file.isLocalFile()) { try { filePresenter.open(file); setJMenuBar(filePresenter.getMenuBar()); initializeAfterLoad(); } catch (Exception e) { LOGGER.debug("Exception caught", e); showGenericErrorDialog(); dispose(); } return filePresenter; } return new AsyncPanel() { @Override public void initTargetComponent() throws Exception { openPresenter(file, this::cancel); } @Override public JComponent getTargetComponent(Exception e) { if (e != null) { LOGGER.debug("Exception caught", e); showGenericErrorDialog(); dispose(); return filePresenter == null ? new JPanel() : filePresenter; } setJMenuBar(filePresenter.getMenuBar()); return filePresenter; } @Override protected void updateLayout() { super.updateLayout(); initializeAfterLoad(); } }; } private void openPresenter(AbstractFile file, Runnable onCancel) throws IOException { final KeyEventDispatcher keyEventDispatcher = e -> { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { e.consume(); if (onCancel != null) { onCancel.run(); } setVisible(false); dispose(); } return false; }; KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(keyEventDispatcher); try { filePresenter.open(file); } finally { KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(keyEventDispatcher); } } private void initializeAfterLoad() { // Sets panel to preferred size, without exceeding a maximum size and with a minimum size //pack(); WindowsStorage.getInstance().init(FileFrame.this, filePresenter.getClass().getCanonicalName(), true); // Request focus on the viewer when it is visible FocusRequester.requestFocus(filePresenter); // Restore (caret position, scroll position etc.) filePresenter.restoreStateOnStartup(); } private void showGenericErrorDialog() { InformationDialog.showErrorDialog(mainFrame.getJFrame(), getGenericErrorDialogTitle(), getGenericErrorDialogMessage()); } // /** // * Sets this file presenter to full screen // */ // public void setFullScreen(boolean on) { // int currentExtendedState = getExtendedState(); // setExtendedState(on ? currentExtendedState | Frame.MAXIMIZED_BOTH : currentExtendedState & ~Frame.MAXIMIZED_BOTH); //} /** * Returns whether this frame is set to be displayed in full screen mode * * @return true if the frame is set to full screen, false otherwise */ private boolean isFullScreen() { return (getExtendedState() & Frame.MAXIMIZED_BOTH) != 0; } @Override public void pack() { if (!isFullScreen()) { super.pack(); DialogToolkit.fitToScreen(this); DialogToolkit.fitToMinDimension(this, getMinimumSize()); DialogToolkit.centerOnWindow(this, mainFrame.getJFrame()); } } @Override public void dispose() { try { filePresenter.saveStateOnClose(); WindowsStorage.getInstance().put(this, filePresenter.getClass().getCanonicalName()); } catch (Throwable ignore) {} super.dispose(); try { if (returnFocusTo != null) { FocusRequester.requestFocus(returnFocusTo); if (returnFocusTo instanceof FileFrame) { //SwingUtilities.invokeLater(() -> FocusRequester.requestFocus(((FileFrame)returnFocusTo).filePresenter)); FocusRequester.requestFocus(((FileFrame)returnFocusTo).filePresenter); //((FileFrame)returnFocusTo).filePresenter } } FileViewersList.update(); } catch (Throwable ignore) {} } public FileFrame returnFocusTo(Component returnFocusTo) { this.returnFocusTo = returnFocusTo; return this; } public Component getReturnFocusTo() { return returnFocusTo; } public void setSearchedText(String searchedText) { filePresenter.setSearchedText(searchedText); } public void setSearchedBytes(byte[] searchedBytes) { filePresenter.setSearchedBytes(searchedBytes); } public FilePresenter getFilePresenter() { return filePresenter; } @Override public void setTitle(String title) { super.setTitle(title); FileViewersList.update(); } public MainFrame getMainFrame() { return mainFrame; } public Point calcQuickListPosition(Dimension dim) { int x = Math.max((getWidth() - (int)dim.getWidth()) / 2, 0); int y = Math.max((getHeight() - (int)dim.getHeight()) / 2, 0); return new Point(x, y); } public Component containerComponent() { return this; } public Component nextFocusableComponent() { return this; } protected abstract String getGenericErrorDialogTitle(); protected abstract String getGenericErrorDialogMessage(); protected abstract FilePresenter createFilePresenter(AbstractFile file) throws UserCancelledException; } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FileFrameCreateListener.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; /** * @author Oleg Trifonov * Created on 05/07/16. */ public interface FileFrameCreateListener { void onCreate(FileFrame fileFrame); } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FilePreloadWorker.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import com.mucommander.commons.HasProgress; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.io.EncodingDetector; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.statusbar.TaskWidget; import org.jetbrains.annotations.NotNull; import javax.swing.SwingWorker; import java.io.PushbackInputStream; import java.util.List; /** * @author Oleg Trifonov * Created on 07/07/16. */ public class FilePreloadWorker extends SwingWorker { private final AbstractFile file; private final MainFrame mainFrame; private final TaskWidget taskWidget; private final Runnable onFinish; private volatile Throwable readException; private volatile int progress; private boolean taskWidgetAttached; FilePreloadWorker(AbstractFile file, MainFrame mainFrame, Runnable onFinish) { this.file = file; this.mainFrame = mainFrame; this.onFinish = onFinish; this.taskWidget = new TaskWidget(); taskWidget.setText(file.getName()); } @Override protected Void doInBackground() { try { publish(); final PushbackInputStream is = file.getPushBackInputStream(EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE); if (is instanceof HasProgress) { buildProgressThread((HasProgress) is).start(); } } catch (Throwable e) { readException = e; } return null; } @NotNull private Thread buildProgressThread(HasProgress is) { Thread progressThread = new Thread(() -> { while (true) { progress = is.getProgress(); publish(); if (progress >= 100 || readException != null || progress < 0) { progress = -1; publish(); break; } try { Thread.sleep(10); } catch (InterruptedException ignored) {} } }); progressThread.setName("ProgressInputStreamThread"); return progressThread; } @Override protected void process(List chunks) { if (!taskWidgetAttached) { mainFrame.getStatusBar().getTaskPanel().addTask(taskWidget); mainFrame.getStatusBar().revalidate(); mainFrame.getStatusBar().repaint(); taskWidgetAttached = true; } taskWidget.setProgress(progress); } @Override protected void done() { taskWidget.removeFromPanel(); if (readException != null) { mainFrame.getStatusBar().setStatusInfo(Translator.get("text_viewer.open_file_error")); } else if (onFinish != null) { onFinish.run(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FilePresenter.java ================================================ package com.mucommander.ui.viewer; import java.awt.Component; import java.awt.event.*; import java.io.IOException; import javax.swing.*; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import org.fife.ui.StatusBar; /** * Abstract class that serves as a common base for the file presenter objects (FileViewer, FileEditor). * * @author Arik Hadas */ public abstract class FilePresenter extends JScrollPane { /** FileFrame instance that contains this presenter (maybe null). */ private FileFrame frame; /** File currently being presented. */ private AbstractFile file; protected final static String CUSTOM_FULL_SCREEN_EVENT = "CUSTOM_FULL_SCREEN_EVENT"; private final static String CUSTOM_DISPOSE_EVENT = "CUSTOM_DISPOSE_EVENT"; FilePresenter() { super(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); addFocusListener(new FocusListener() { public void focusLost(FocusEvent e) {} public void focusGained(FocusEvent e) { // Delegate the focus to the JComponent that actually present the file Component component = FilePresenter.this.getViewport().getComponent(0); if (component != null) { component.requestFocus(); } } }); // Catch Apple+W keystrokes under Mac OS X to close the window if (OsFamily.MAC_OS_X.isCurrent()) { getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_DOWN_MASK), CUSTOM_DISPOSE_EVENT); getActionMap().put(CUSTOM_DISPOSE_EVENT, new AbstractAction() { public void actionPerformed(ActionEvent e){ getFrame().dispose(); } }); } } /** * Set component to be presented in the ScrollPane viewport * * @param component the component to be presented */ public void setComponentToPresent(JComponent component) { getViewport().removeAll(); getViewport().add(component); } /** * Returns the frame which contains this presenter. *

    * This method may return nullif the presenter is not inside a FileFrame. * * @return the frame which contains this presenter. * @see #setFrame(FileFrame) */ protected FileFrame getFrame() { return frame; } /** * Sets the FileFrame (separate window) that contains this FilePresenter. * @param frame frame that contains this FilePresenter. * @see #getFrame() */ public void setFrame(FileFrame frame) { this.frame = frame; } /** * Returns a description of the file currently being presented which will be used as a window title. * This method returns the file's name, but it can be overridden to provide more information. * @return this dialog's title. */ protected String getTitle() { return file.getAbsolutePath(); } /** * Returns the file that is being presented. * * @return the file that is being presented. */ public AbstractFile getCurrentFile() { return file; } /** * Sets the file that is to be presented. * This method will automatically be called after a file presenter is created and should not be called directly. * * @param file file that is to be presented. */ protected final void setCurrentFile(AbstractFile file) { this.file = file; // Update frame's title FileFrame frame = getFrame(); if (frame != null) { frame.setTitle(getTitle()); } } /** * Open a given AbstractFile for display. * * @param file the file to be presented * @throws IOException in case of an I/O problem */ public void open(AbstractFile file) throws IOException { show(file); setCurrentFile(file); } /** * This method is invoked when the specified file is about to be opened. * This method should retrieve the file and do the necessary so that this component can be displayed. * * @param file the file that is about to be viewed. * @throws IOException if an I/O error occurs. */ protected abstract void show(AbstractFile file) throws IOException; /** * Returns the menu bar that controls the presenter's frame. The menu bar should be retrieved using this method and * not by calling {@link JFrame#getJMenuBar()}, which may return null. * * @return the menu bar that controls the presenter's frame. */ protected abstract JMenuBar getMenuBar(); /** * Returns the status bar for presenter frame. Can return null if the viewer doesn't have status bar. */ protected abstract StatusBar getStatusBar(); /** * Executed before editor/viewer closed to save sate (cursor position, syntax type etc.) */ protected abstract void saveStateOnClose(); /** * Executed before editor/viewer shows to restore saved state */ protected abstract void restoreStateOnStartup(); public void setSearchedText(String searchedText) { } public void setSearchedBytes(byte[] searchedBytes) { } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FileViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.utils.text.Translator; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.HashMap; import java.util.Map; /** * An abstract class to be subclassed by file viewer implementations. * *

    * Warning: the file viewer/editor API may soon receive a major overhaul. * * @author Maxence Bernard, Arik Hadas */ public abstract class FileViewer extends FilePresenter implements ActionListener { /** * This map used to fix java issues with some menu hot-keys - some accelerators (etc. F2, arrows, Enter, Escape) doesn't work * properly in menu */ private Map menuKeyStrokes; protected JMenu menuFile; /** Close menu item */ private JMenuItem miClose; /** * Creates a new FileViewer. */ public FileViewer() {} /** * Returns the menu bar that controls the viewer's frame. The menu bar should be retrieved using this method and * not by calling {@link JFrame#getJMenuBar()}, which may return null. * * @return the menu bar that controls the viewer's frame. */ public JMenuBar getMenuBar() { JMenuBar menuBar = new JMenuBar(); MnemonicHelper mnemonicHelper = new MnemonicHelper(); // File menu menuFile = MenuToolkit.addMenu(i18n("file_viewer.file_menu"), mnemonicHelper, null); miClose = MenuToolkit.addMenuItem(menuFile, i18n("file_viewer.close"), mnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), this); menuFile.add(miClose); menuBar.add(menuFile); return menuBar; } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == miClose) { getFrame().dispose(); } } private final KeyListener mainKeyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { KeyStroke keyStroke = KeyStroke.getKeyStroke(e.getKeyCode(), e.getModifiersEx(), false); JMenuItem menuItem = menuKeyStrokes.get(keyStroke); if (menuItem != null) { actionPerformed(new ActionEvent(menuItem, 0, null)); e.consume(); return; } super.keyPressed(e); } }; /** * Set main component that will be listen key codes to fix issue with not workings menu accelerators * * @param comp main component for listing * @param menuBar menu bar */ public void setMainKeyListener(Component comp, JMenuBar menuBar) { fillMenuKeyStrokes(menuBar); comp.addKeyListener(mainKeyListener); } private static boolean isProblemKey(KeyStroke keyStroke) { if (keyStroke == null) { return false; } int keyCode = keyStroke.getKeyCode(); return (keyCode >= KeyEvent.VK_F1 && keyCode <= KeyEvent.VK_F12) || (keyCode >= KeyEvent.VK_LEFT && keyCode <= KeyEvent.VK_DOWN) || keyCode == KeyEvent.VK_ENTER || keyCode == KeyEvent.VK_TAB; } private static boolean isProblemMenuItem(JMenuItem menuItem) { return menuItem != null && isProblemKey(menuItem.getAccelerator()); } /** * Fills map for all keycodes that can be not processed properly in swing * * @param menuBar menu bar */ private void fillMenuKeyStrokes(JMenuBar menuBar) { menuKeyStrokes = new HashMap<>(); for (int menuIndex = 0; menuIndex < menuBar.getMenuCount(); menuIndex++) { JMenu menu = menuBar.getMenu(menuIndex); for (int itemIndex = 0; itemIndex < menu.getItemCount(); itemIndex++) { JMenuItem menuItem = menu.getItem(itemIndex); if (isProblemMenuItem(menuItem)) { menuKeyStrokes.put(menuItem.getAccelerator(), menuItem); } } } } protected static String i18n(String key, String... params) { return Translator.get(key, params); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/FileViewersList.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import com.mucommander.ui.action.TcAction; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.action.impl.ViewAction; import com.mucommander.ui.viewer.text.TextEditor; import javax.swing.Icon; import java.awt.Frame; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * Created on 09/06/16. * @author Oleg Trifonov */ public class FileViewersList { private static final List files = new ArrayList<>(); private static long lastUpdateTime; /** * */ public static class FileRecord { final public String fileName; final public String shortName; final public Class viewerClass; final public WeakReference fileFrameRef; FileRecord(String fileName, FileFrame fileFrame) { this.fileName = fileName; this.shortName = new File(fileName).getName(); this.viewerClass = fileFrame.getFilePresenter().getClass(); this.fileFrameRef = new WeakReference<>(fileFrame); } @Override public String toString() { return fileName; } public Icon getIcon() { //Icon icon = FileIconsCache.getInstance().getIcon(fileRecord.fileName); if (viewerClass == TextEditor.class) { return TcAction.getStandardIcon(EditAction.class); } else { return TcAction.getStandardIcon(ViewAction.class); } } } private static void buildFilesList(List fileNames) { fileNames.clear(); Frame frames[] = Frame.getFrames(); for (Frame frame : frames) { // Test if Frame is not hidden (disposed), Frame.getFrames() returns both active and disposed frames if (frame.isShowing() && (frame instanceof FileFrame)) { // Use frame's window title fileNames.add(new FileRecord(frame.getTitle(), (FileFrame)frame)); } } } public static void update() { buildFilesList(files); lastUpdateTime = System.currentTimeMillis(); } public static List getFiles() { return files; } public static long getLastUpdateTime() { return lastUpdateTime; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/UserCancelledException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; /** * This exception is thrown by {@link com.mucommander.ui.viewer.ViewerRegistrar} and * {@link com.mucommander.ui.viewer.EditorRegistrar} when the user has cancelled the view/edit operation. * * @author Maxence Bernard */ public class UserCancelledException extends Exception { } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/ViewerFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import com.mucommander.commons.file.AbstractFile; /** * A common interface for instanciating {@link FileViewer} implementations, and finding out if a viewer is capable * of viewing a particular file. * * @author Nicolas Rinaudo, Maxence Bernard */ public interface ViewerFactory { /** * Returns true if this factory can create a file viewer for the specified file. *

    * The FileEditor may base its decision strictly upon the file's name and its extension or may wish to read some of * the file and compare it to a magic number. * * @param file file for which a viewer must be created. * @throws WarnUserException if the specified file can be viewed after the warning message contained in the * exception is displayed to the end user. * @return true if this factory can create a file viewer for the specified file. */ boolean canViewFile(AbstractFile file) throws WarnUserException; /** * Returns a new instance of {@link FileViewer}. * * @return a new instance of {@link FileViewer}. */ FileViewer createFileViewer(); /** * Returns a name for ViewAs list * * @return */ String getName(); } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/ViewerFrame.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import java.awt.Dimension; import java.awt.Image; import java.io.IOException; import com.mucommander.commons.file.AbstractFile; import com.mucommander.utils.text.Translator; import com.mucommander.ui.main.MainFrame; /** * A specialized JFrame that displays a {@link FileViewer} for a given file. * The {@link FileViewer} instance is provided by {@link ViewerRegistrar}. * * @author Maxence Bernard, Arik Hadas */ public class ViewerFrame extends FileFrame { private final static Dimension MIN_DIMENSION = new Dimension(500, 360); private final ViewerFactory defaultFactory; private AbstractFile file; /** * Creates a new ViewerFrame to start viewing the given file. * *

    This constructor has package access only, ViewerFrame need to be created can * {@link ViewerRegistrar#createViewerFrame(MainFrame, AbstractFile, Image)}. */ ViewerFrame(MainFrame mainFrame, AbstractFile file, Image icon, ViewerFactory defaultFactory) { super(mainFrame, icon); this.defaultFactory = defaultFactory; initContentPane(file); } //////////////////////// // Overridden methods // //////////////////////// @Override public Dimension getMinimumSize() { return MIN_DIMENSION; } @Override protected FilePresenter createFilePresenter(AbstractFile file) throws UserCancelledException { this.file = file; return ViewerRegistrar.createFileViewer(file, ViewerFrame.this, defaultFactory); } @Override protected String getGenericErrorDialogTitle() { return Translator.get("file_viewer.view_error_title"); } @Override protected String getGenericErrorDialogMessage() { return Translator.get("file_viewer.view_error"); } @Override public void dispose() { if (file != null) { try { file.closePushbackInputStream(); } catch (IOException e) { e.printStackTrace(); } file = null; } super.dispose(); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/ViewerRegistrar.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; import java.awt.Cursor; import java.awt.Frame; import java.awt.Image; import java.util.ArrayList; import java.util.List; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileProtocols; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.QuestionDialog; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.WindowManager; import com.mucommander.ui.viewer.audio.AudioFactory; import com.mucommander.ui.viewer.hex.HexFactory; import com.mucommander.ui.viewer.hex.HexViewer; import com.mucommander.ui.viewer.html.HtmlViewer; import com.mucommander.ui.viewer.pdf.PdfViewer; import com.mucommander.ui.viewer.text.TextViewer; import net.sf.jftp.gui.tasks.ImageViewer; /** * ViewerRegistrar maintains a list of registered file viewers and provides methods to dynamically register file viewers * and create appropriate FileViewer (Panel) and ViewerFrame (Window) instances for a given AbstractFile. * * @author Maxence Bernard, Arik Hadas */ public class ViewerRegistrar { /** List of registered file viewers */ private final static List viewerFactories = new ArrayList<>(); static { registerFileViewer(new com.mucommander.ui.viewer.pdf.PdfFactory()); registerFileViewer(new com.mucommander.ui.viewer.djvu.DjvuFactory()); registerFileViewer(new com.mucommander.ui.viewer.image.ImageFactory()); registerFileViewer(new AudioFactory()); registerFileViewer(new com.mucommander.ui.viewer.html.HtmlFactory()); registerFileViewer(new com.mucommander.ui.viewer.text.TextFactory()); // The HexFactory must be the last FileViewer to be registered (otherwise it would open other factories file types) registerFileViewer(new com.mucommander.ui.viewer.hex.HexFactory()); } /** * Registers a FileViewer. * @param factory file viewer factory to register. */ private static void registerFileViewer(ViewerFactory factory) { viewerFactories.add(factory); } /** * Creates and returns a ViewerFrame to start viewing the given file. The ViewerFrame will be monitored * so that if it is the last window on screen when it is closed by the user, it will trigger the shutdown sequence. * * @param mainFrame the parent MainFrame instance * @param file the file that will be displayed by the returned ViewerFrame * @param icon window's icon. * @param defaultFactory postponed action * @param createListener this lambda will be executed after viewer frame creation */ public static void createViewerFrame(MainFrame mainFrame, AbstractFile file, Image icon, ViewerFactory defaultFactory, FileFrameCreateListener createListener) { // Check if this file is already opened for (FileViewersList.FileRecord fr: FileViewersList.getFiles()) { if (fr.fileName.equals(file.getAbsolutePath()) && fr.viewerClass != null) { Class viewerClass = fr.viewerClass; if (viewerClass.equals(TextViewer.class) || viewerClass.equals(HexViewer.class) || viewerClass.equals(HtmlViewer.class) || viewerClass.equals(ImageViewer.class) || viewerClass.equals(PdfViewer.class)) { FileFrame openedFrame = fr.fileFrameRef.get(); if (openedFrame != null) { openedFrame.toFront(); } if (createListener != null) { createListener.onCreate(openedFrame); } return; } } } new FilePreloadWorker(file, mainFrame, () -> { ViewerFrame frame = new ViewerFrame(mainFrame, file, icon, defaultFactory); // Use new Window decorations introduced in Mac OS X 10.5 (Leopard) if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { // Displays the document icon in the window title bar, works only for local files if (file.getURL().getScheme().equals(FileProtocols.FILE)) { frame.getRootPane().putClientProperty("Window.documentFile", file.getUnderlyingFileObject()); } } // WindowManager will listen to window closed events to trigger shutdown sequence // if it is the last window visible frame.addWindowListener(WindowManager.getInstance()); if (createListener != null) { createListener.onCreate(frame); } }).execute(); /* TaskWidget taskWidget = new TaskWidget(); taskWidget.setText(file.getName()); mainFrame.getStatusBar().getTaskPanel().addTask(taskWidget); mainFrame.getStatusBar().revalidate(); mainFrame.getStatusBar().repaint(); new SwingWorker() { volatile Throwable readException; volatile int progress; @Override protected Void doInBackground() throws Exception { try { publish(); final PushbackInputStream is = file.getPushBackInputStream(EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE); if (is instanceof HasProgress) { Thread progressThread = new Thread() { @Override public void run() { while (true) { progress = ((HasProgress) is).getProgress(); publish(); if (progress >= 100 || readException != null) { progress = -1; publish(); break; } try { Thread.sleep(100); } catch (InterruptedException ignored) {} } } }; progressThread.setName("ProgressInputStreamThread"); progressThread.start(); } is.read(); is.unread(1); } catch (Throwable e) { readException = e; } return null; } @Override protected void process(List chunks) { taskWidget.setProgress(progress); } @Override protected void done() { taskWidget.removeFromPanel(); if (readException != null) { mainFrame.getStatusBar().setStatusInfo(Translator.get("text_viewer.open_file_error")); } ViewerFrame frame = new ViewerFrame(mainFrame, file, icon, defaultFactory); // Use new Window decorations introduced in Mac OS X 10.5 (Leopard) if (OsFamily.MAC_OS_X.isCurrent() && OsVersion.MAC_OS_X_10_5.isCurrentOrHigher()) { // Displays the document icon in the window title bar, works only for local files if (file.getURL().getScheme().equals(FileProtocols.FILE)) { frame.getRootPane().putClientProperty("Window.documentFile", file.getUnderlyingFileObject()); } } // WindowManager will listen to window closed events to trigger shutdown sequence // if it is the last window visible frame.addWindowListener(WindowManager.getInstance()); if (createListener != null) { createListener.onCreate(frame); } } }.execute(); */ } public static void createViewerFrame(MainFrame mainFrame, AbstractFile file, Image icon) { createViewerFrame(mainFrame, file, icon, null, null); } public static void createViewerFrame(MainFrame mainFrame, AbstractFile file, Image icon, FileFrameCreateListener createListener) { createViewerFrame(mainFrame, file, icon, null, createListener); } /** * Creates and returns an appropriate FileViewer for the given file type. * * @param file the file that will be displayed by the returned FileViewer * @param frame the frame in which the FileViewer is shown * @return the created FileViewer, or null if no suitable viewer was found * @throws UserCancelledException if the user has been asked to confirm the operation and canceled */ public static FileViewer createFileViewer(AbstractFile file, ViewerFrame frame, ViewerFactory defaultFactory) throws UserCancelledException { FileViewer viewer = null; MainFrame mainFrame = frame != null ? frame.getMainFrame() : null; for (ViewerFactory factory : viewerFactories) { if (defaultFactory != null && !factory.getName().equals(defaultFactory.getName())) { continue; } try { if (mainFrame != null) { mainFrame.getJFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); } if (factory.canViewFile(file)) { viewer = factory.createFileViewer(); if (mainFrame != null) { mainFrame.getJFrame().setCursor(Cursor.getDefaultCursor()); } break; } } catch (WarnUserException e) { if (mainFrame != null) { mainFrame.getJFrame().setCursor(Cursor.getDefaultCursor()); } // TODO: question the user how does he want to open the file (as image, text..) // Todo: display a proper warning dialog with the appropriate icon QuestionDialog dialog = new QuestionDialog((Frame)null, Translator.get("warning"), Translator.get(e.getMessage()), frame, new String[] {Translator.get("file_viewer.open_anyway"), Translator.get("file_viewer.open_hex"), Translator.get("cancel")}, new int[] {0, 1, 2}, 0); int ret = dialog.getActionValue(); if (ret == 0) { // User confirmed the operation viewer = factory.createFileViewer(); break; } else if (ret == 1) { viewer = new HexFactory().createFileViewer(); break; } else { // User canceled the operation throw new UserCancelledException(); } } catch (Exception e) { if (mainFrame != null) { mainFrame.getJFrame().setCursor(Cursor.getDefaultCursor()); } } } if (viewer != null) { viewer.setFrame(frame); } return viewer; } public static List getAllViewers(AbstractFile file) { List result = new ArrayList<>(); for (ViewerFactory factory : viewerFactories) { try { if (!factory.canViewFile(file)) { continue; } } catch (WarnUserException ignore) {} result.add(factory); } return result; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/WarnUserException.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer; /** * This exception is thrown by {@link com.mucommander.ui.viewer.ViewerFactory} and * {@link com.mucommander.ui.viewer.EditorFactory} when the user should be warned about something before going ahead * with viewing/editing a file. {@link #getMessage()} contains the message to display to the user. * * @author Maxence Bernard */ public class WarnUserException extends Exception { public WarnUserException(String localizedMessage) { super(localizedMessage); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/audio/AudioFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.audio; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.utils.text.Translator; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.ViewerFactory; public class AudioFactory implements ViewerFactory { public final static ExtensionFilenameFilter AUDIO_FILTER = new ExtensionFilenameFilter(new String[] {".wav", ".mp3", ".ogg", ".mid"}); static { AUDIO_FILTER.setCaseSensitive(false); } @Override public boolean canViewFile(AbstractFile file) { return false; // if (file.isDirectory()) { // return false; // } // return AUDIO_FILTER.accept(file); } @Override public FileViewer createFileViewer() { return new AudioViewer(); } @Override public String getName() { return Translator.get("viewer_type.audio"); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/audio/AudioPlayer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.audio; import javax.swing.JButton; import javax.swing.JPanel; /** * Created on 14/03/14. */ public class AudioPlayer extends JPanel { public AudioPlayer() { super(); JButton btnPrev = new JButton("<<"); JButton btnPlay = new JButton(">"); JButton btnStop = new JButton("x"); JButton btnPause = new JButton("||"); JButton btnNext = new JButton(">>"); add(btnPrev); add(btnPlay); add(btnStop); add(btnPause); add(btnNext); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/audio/AudioViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.audio; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.viewer.FileViewer; import org.fife.ui.StatusBar; import javax.sound.sampled.*; public class AudioViewer extends FileViewer { /** * The audio line we'll output sound to; it'll be the default audio device on your system if available */ private static SourceDataLine mLine; @Override protected void show(AbstractFile file) { setComponentToPresent(new AudioPlayer()); System.out.println("PLAY " + file.getURL().toString()); // ToolFactory.setTurboCharged(true); // IMediaReader reader = ToolFactory.makeReader(file.getAbsolutePath()); // reader.addListener(ToolFactory.makeViewer(IMediaViewer.Mode.AUDIO_ONLY)); // while (reader.readPacket() == null) { //System.out.println('.'); // do {} while(false); // } // play(file.getAbsolutePath()); System.out.println("STOP " + file.getURL().toString()); } // private void play(String filename) { // // create a Xuggler container object // IContainer container = IContainer.make(); // // // Open up the container // if (container.open(filename, IContainer.Type.READ, null) < 0) { // throw new IllegalArgumentException("could not open file: " + filename); // } // // // query how many streams the call to open found // int numStreams = container.getNumStreams(); //System.out.println("numStreams " + numStreams); // // and iterate through the streams to find the first audio stream // int audioStreamId = -1; // IStreamCoder audioCoder = null; // for (int i = 0; i < numStreams; i++) { // // Find the stream object // IStream stream = container.getStream(i); // // Get the pre-configured decoder that can decode this stream; // IStreamCoder coder = stream.getStreamCoder(); // // if (coder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) { // audioStreamId = i; // audioCoder = coder; // break; // } // } // if (audioStreamId == -1) { // throw new RuntimeException("could not find audio stream in container: " + filename); // } // // // Now we have found the audio stream in this file. Let's open up our decoder so it can do work. // if (audioCoder.open(null, null ) < 0) { // throw new RuntimeException("could not open audio decoder for container: " + filename); // } // // // And once we have that, we ask the Java Sound System to get itself ready. // openJavaSound(audioCoder); // // // Now, we start walking through the container looking at each packet. // IPacket packet = IPacket.make(); // while (container.readNextPacket(packet) >= 0) { // // Now we have a packet, let's see if it belongs to our audio stream // if (packet.getStreamIndex() == audioStreamId) { // // We allocate a set of samples with the same number of channels as the coder tells us is in this buffer // // We also pass in a buffer size (1024 in our example), although Xuggler will probably allocate more space // // than just the 1024 (it's not important why) // IAudioSamples samples = IAudioSamples.make(1024, audioCoder.getChannels()); // // // A packet can actually contain multiple sets of samples (or frames of samples // // in audio-decoding speak). So, we may need to call decode audio multiple // // times at different offsets in the packet's data. We capture that here. // int offset = 0; // // // Keep going until we've processed all data // while (offset < packet.getSize()) { // int bytesDecoded = audioCoder.decodeAudio(samples, packet, offset); // if (bytesDecoded < 0) { // throw new RuntimeException("got error decoding audio in: " + filename); // } // offset += bytesDecoded; // // // Some decoder will consume data in a packet, but will not be able to construct a full set of samples yet. // // Therefore you should always check if you got a complete set of samples from the decoder // if (samples.isComplete()) { // playJavaSound(samples); // } // } // } else { // // This packet isn't part of our audio stream, so we just silently drop it. // do {} while(false); // } // // } // // Technically since we're exiting anyway, these will be cleaned up by the garbage collector... but because we're // // nice people and want to be invited places for Christmas, we're going to show how to clean up. // closeJavaSound(); // // if (audioCoder != null) { // audioCoder.close(); // audioCoder = null; // } // if (container != null) { // container.close(); // container = null; // } // } // // // private static void openJavaSound(IStreamCoder aAudioCoder) { // AudioFormat audioFormat = new AudioFormat(aAudioCoder.getSampleRate(), // (int)IAudioSamples.findSampleBitDepth(aAudioCoder.getSampleFormat()), // aAudioCoder.getChannels(), // true, // xuggler defaults to signed 16 bit samples // false); // DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); // try { // mLine = (SourceDataLine) AudioSystem.getLine(info); // // if that succeeded, try opening the line // mLine.open(audioFormat); // // And if that succeed, start the line // mLine.start(); // } catch (LineUnavailableException e) { // throw new RuntimeException("could not open audio line"); // } // } // // private static void playJavaSound(IAudioSamples aSamples) { // // We're just going to dump all the samples into the line. // byte[] rawBytes = aSamples.getData().getByteArray(0, aSamples.getSize()); // mLine.write(rawBytes, 0, aSamples.getSize()); // } private static void closeJavaSound() { if (mLine != null) { // Wait for the line to finish playing mLine.drain(); // Close the line mLine.close(); mLine = null; } } @Override protected StatusBar getStatusBar() { return new StatusBar(); } @Override protected void saveStateOnClose() { } @Override protected void restoreStateOnStartup() { } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/audio/StatusBar.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.audio; import org.fife.ui.StatusBarPanel; import javax.swing.JLabel; import java.awt.BorderLayout; import java.awt.GridBagConstraints; /** * Created on 14/03/14. */ public class StatusBar extends org.fife.ui.StatusBar { private final JLabel lbl; public StatusBar() { super(""); lbl = new JLabel(); StatusBarPanel panel = new StatusBarPanel(new BorderLayout(), lbl); // Make the layout such that different items can be different sizes. GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; c.weightx = 0.0; addStatusBarComponent(panel, c); } public void set(String s) { lbl.setText(s); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/djvu/DjvuFactory.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.djvu; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.utils.text.Translator; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.ViewerFactory; /** * @author Oleg Trifonov * Created on 04/08/14. */ public class DjvuFactory implements ViewerFactory { public final static ExtensionFilenameFilter DJVU_FILTER = new ExtensionFilenameFilter(new String[]{".djvu", ".djv"}); static { DJVU_FILTER.setCaseSensitive(false); } @Override public boolean canViewFile(AbstractFile file) { return !file.isDirectory() && DJVU_FILTER.accept(file); } @Override public FileViewer createFileViewer() { return new DjvuViewer(); } @Override public String getName() { return Translator.get("viewer_type.djvu"); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/djvu/DjvuViewer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.djvu; import com.lizardtech.djvubean.DjVuBean; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.viewer.FileViewer; import org.fife.ui.StatusBar; import javax.swing.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.IOException; /** * Created on 04/08/14. */ public class DjvuViewer extends FileViewer { private final DjVuBean djvuBean; private final KeyAdapter keyAdapter = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { boolean shift = e.isShiftDown(); switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: djvuBean.setPage(djvuBean.getPage() - (shift ? 10 : 1)); break; case KeyEvent.VK_RIGHT: djvuBean.setPage(djvuBean.getPage() + (shift ? 10 : 1)); break; } } }; DjvuViewer() { super(); djvuBean = new DjVuBean(); JScrollPane scrollPane = new JScrollPane(djvuBean); djvuBean.addKeyListener(keyAdapter); setComponentToPresent(scrollPane); } @Override protected void show(AbstractFile file) throws IOException { djvuBean.setURL(file.getURL().getJavaNetURL()); } @Override protected StatusBar getStatusBar() { return null; } @Override protected void saveStateOnClose() { } @Override protected void restoreStateOnStartup() { } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/hex/FindDialog.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.hex import com.jidesoft.hints.ListDataIntelliHints import com.mucommander.cache.TextHistory import com.mucommander.ui.dialog.DialogToolkit import com.mucommander.ui.dialog.FocusDialog import com.mucommander.ui.layout.XAlignedComponentPanel import ru.trolsoft.ui.InputField import java.awt.BorderLayout import java.awt.event.ActionEvent import java.awt.event.ActionListener import javax.swing.JButton import javax.swing.JFrame /** * This dialog allows the user to enter a string or hex value to be searched for in the hex editor. * * @author Oleg Trifonov */ abstract class FindDialog internal constructor(frame: JFrame?, encoding: String?) : FocusDialog(frame, i18n("hex_viewer.find"), frame), ActionListener { /** The text field where a search dump can be entered */ private val hexField: InputField /** The text field where a search string can be entered */ private val textField: InputField /** The 'OK' button */ private val okButton: JButton init { val contentPane = getContentPane() // Text fields panel val compPanel = XAlignedComponentPanel() textField = InputField(60, InputField.FilterType.ANY_TEXT).also { it.addActionListener(this) compPanel.addRow(i18n("hex_view.text") + ":", it, 5) val historyText = TextHistory.getInstance().getList(TextHistory.Type.TEXT_SEARCH) // new AutoCompletion(textField, historyText).setStrict(false); ListDataIntelliHints(it, historyText).isCaseSensitive = true it.text = "" } hexField = InputField(60, InputField.FilterType.HEX_DUMP).also { it.addActionListener(this) compPanel.addRow(i18n("hex_viewer.hex") + ":", it, 10) val historyHex: MutableList? = TextHistory.getInstance().getList(TextHistory.Type.HEX_DATA_SEARCH) // new AutoCompletion(hexField, historyHex).setStrict(false); ListDataIntelliHints(it, historyHex).isCaseSensitive = false it.text = "" } textField.bindField(hexField) hexField.bindField(textField) setEncoding(encoding) contentPane.add(compPanel, BorderLayout.CENTER) okButton = JButton(i18n("ok")) val cancelButton = JButton(i18n("cancel")) contentPane.add( DialogToolkit.createOKCancelPanel(okButton, cancelButton, getRootPane(), this), BorderLayout.SOUTH ) // The text field will receive initial focus setInitialFocusComponent(textField) } private fun setEncoding(encoding: String?) { textField.textEncoding = encoding hexField.textEncoding = encoding } var searchBytes: ByteArray? get() = hexField.bytes set(searchBytes) { hexField.setBytes(searchBytes) } override fun actionPerformed(e: ActionEvent) { val source = e.getSource() dispose() doSearch(if (source === okButton || source === hexField || source === textField) this.searchBytes else null) } override fun saveState() { super.saveState() TextHistory.getInstance().add(TextHistory.Type.TEXT_SEARCH, textField.getText(), true) TextHistory.getInstance().add(TextHistory.Type.HEX_DATA_SEARCH, hexField.getText(), true) } /** * Search operation listener * @param bytes nul if the dialog was cancelled */ protected abstract fun doSearch(bytes: ByteArray?) } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/hex/GotoDialog.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.hex import com.mucommander.ui.dialog.DialogToolkit import com.mucommander.ui.dialog.FocusDialog import ru.trolsoft.ui.InputField import java.awt.BorderLayout import java.awt.Frame import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.util.function.LongConsumer import javax.swing.JButton import javax.swing.JLabel /** * Goto address dialog. * @author Oleg Trifonov */ class GotoDialog internal constructor(owner: Frame?, private val maxOffset: Long, private val action: LongConsumer) : FocusDialog(owner, i18n("hex_viewer.goto"), owner), ActionListener { private val edtOffset: InputField private val btnOk: JButton init { val contentPane = getContentPane() contentPane.add(JLabel(i18n("hex_viewer.goto.offset") + ":"), BorderLayout.NORTH) edtOffset = object : InputField(16, FilterType.HEX_LONG) { override fun onChange() { val enabled = !edtOffset.isEmpty && edtOffset.getValue() <= this@GotoDialog.maxOffset btnOk.setEnabled(enabled) } }.also { it.text = "1" it.addActionListener(this) contentPane.add(it, BorderLayout.CENTER) } btnOk = JButton(i18n("ok")) val cancelButton = JButton(i18n("cancel")) contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, cancelButton, getRootPane(), this), BorderLayout.SOUTH) // The text field will receive initial focus setInitialFocusComponent(edtOffset) } override fun actionPerformed(e: ActionEvent) { val source = e.getSource() if ((source === btnOk || source === edtOffset) && btnOk.isEnabled) { action.accept(edtOffset.getValue()) dispose() } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/hex/HexFactory.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.hex import com.mucommander.commons.file.AbstractFile import com.mucommander.ui.viewer.FileViewer import com.mucommander.ui.viewer.ViewerFactory import com.mucommander.utils.text.Translator /** * `ViewerFactory` implementation for creating hex viewers. * * @author Oleg Trifonov */ class HexFactory : ViewerFactory { override fun canViewFile(file: AbstractFile): Boolean = !file.isDirectory() override fun createFileViewer(): FileViewer = HexViewer() override fun getName(): String = Translator.get("viewer_type.hex") } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/hex/HexViewer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.hex; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.theme.ThemeId; import com.mucommander.ui.viewer.FileViewer; import lombok.extern.slf4j.Slf4j; import ru.trolsoft.calculator.CalculatorDialog; import ru.trolsoft.hexeditor.data.AbstractByteBuffer; import ru.trolsoft.hexeditor.data.TrolCommanderByteBuffer; import ru.trolsoft.hexeditor.events.OffsetChangeListener; import ru.trolsoft.hexeditor.search.ByteBufferSearchUtils; import ru.trolsoft.hexeditor.ui.HexTable; import ru.trolsoft.hexeditor.ui.ViewerHexTableModel; import javax.swing.*; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.io.IOException; import java.io.UnsupportedEncodingException; import static com.mucommander.ui.theme.ThemeManager.getCurrentColor; import static com.mucommander.ui.theme.ThemeManager.getCurrentFont; /** * Hex dump viewer * @author Oleg Trifonov */ @Slf4j public class HexViewer extends FileViewer implements ThemeId { private static final String DEFAULT_ENCODING = "windows-1252"; private HexTable hexTable; private ViewerHexTableModel model; private AbstractByteBuffer byteBuffer; private StatusBar statusBar; private final String encoding = DEFAULT_ENCODING; private byte[] lastSearchBytes; private final JMenu menuView; private final JMenuItem gotoItem; private final JMenuItem findItem; private final JMenuItem findNextItem; private final JMenuItem findPrevItem; private final JMenuItem calculatorItem; private GotoDialog dlgGoto; private FindDialog dlgFind; HexViewer() { super(); MnemonicHelper menuMnemonicHelper = new MnemonicHelper(); menuView = MenuToolkit.addMenu(i18n("hex_viewer.view"), menuMnemonicHelper, null); gotoItem = MenuToolkit.addMenuItem(menuView, i18n("hex_viewer.goto"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_G, getCtrlOrMetaMask()), this); findItem = MenuToolkit.addMenuItem(menuView, i18n("hex_viewer.search"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_F, getCtrlOrMetaMask()), this); findNextItem = MenuToolkit.addMenuItem(menuView, i18n("hex_viewer.searchNext"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), this); findPrevItem = MenuToolkit.addMenuItem(menuView, i18n("hex_viewer.searchPrev"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_F3, KeyEvent.SHIFT_DOWN_MASK), this); menuView.addSeparator(); calculatorItem = MenuToolkit.addMenuItem(menuView, i18n("Calculator.label"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0), this); } private int getCtrlOrMetaMask() { return OsFamily.MAC_OS_X.isCurrent() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; } private final OffsetChangeListener offsetChangeListener = new OffsetChangeListener() { @Override public void onChange(long offset) { if (statusBar != null) { statusBar.setOffset(offset); try { if (byteBuffer.getFileSize() > 0 && offset < byteBuffer.getFileSize()) { statusBar.setByteValue(byteBuffer.getByte(offset)); } } catch (IOException e) { log.error("Can't show offset", e); } } } }; @Override protected void show(AbstractFile file) { try { byteBuffer = new TrolCommanderByteBuffer(file); model = new ViewerHexTableModel(byteBuffer); model.load(); hexTable = new HexTable(model); hexTable.setBackground(getCurrentColor(HEX_VIEWER_BACKGROUND_COLOR)); hexTable.setForeground(getCurrentColor(HEX_VIEWER_HEX_FOREGROUND_COLOR)); hexTable.setAlternateBackground(getCurrentColor(HEX_VIEWER_ALTERNATE_BACKGROUND_COLOR)); hexTable.setOffsetColumnColor(getCurrentColor(HEX_VIEWER_OFFSET_FOREGROUND_COLOR)); hexTable.setAsciiColumnColor(getCurrentColor(HEX_VIEWER_ASCII_FOREGROUND_COLOR)); hexTable.setAsciiSelectionBackgroundColor(getCurrentColor(HEX_VIEWER_SELECTED_ASCII_BACKGROUND_COLOR)); hexTable.setSelectionBackground(getCurrentColor(HEX_VIEWER_SELECTED_BACKGROUND_COLOR)); hexTable.setFont(getCurrentFont(HEX_VIEWER_FONT)); hexTable.setAlternateRowBackground(true); hexTable.getTableHeader().setFont(new Font("Monospaced", Font.PLAIN, 12)); hexTable.setSelectionChangeListener((fromAddress, toAddress) -> { long bytesSelected = Math.abs(toAddress - fromAddress) + 1; if (bytesSelected > 1) { statusBar.setStatusMessage("Selected " +bytesSelected + " bytes"); } else { statusBar.setStatusMessage(""); } }); hexTable.setOffsetChangeListener(offsetChangeListener); offsetChangeListener.onChange(0); if (statusBar != null) { statusBar.maxOffset = file.getSize() - 1; statusBar.setOffset(hexTable.getCurrentAddress()); } setComponentToPresent(hexTable); getViewport().setBackground(hexTable.getBackground()); } catch (Exception e) { log.error("Init error", e); } } @Override protected StatusBar getStatusBar() { if (statusBar == null) { statusBar = new StatusBar(); statusBar.setEncoding(encoding); } return statusBar; } @Override public JMenuBar getMenuBar() { JMenuBar menuBar = super.getMenuBar(); menuBar.add(menuView); setMainKeyListener(this, menuBar); return menuBar; } @Override protected void saveStateOnClose() { try { byteBuffer.close(); } catch (IOException e) { log.error("Close buffer error", e); } try { getCurrentFile().closePushbackInputStream(); } catch (IOException e) { log.error("Close stream error", e); } } @Override protected void restoreStateOnStartup() { } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == gotoItem && gotoItem.isEnabled()) { gotoOffset(); } else if (source == findItem && findItem.isEnabled()) { findFirst(); } else if (source == findNextItem && findNextItem.isEnabled()) { findNext(); } else if (source == findPrevItem && findPrevItem.isEnabled()) { findPrev(); } else if (source == calculatorItem && calculatorItem.isEnabled()) { new CalculatorDialog(getFrame()).showDialog(); } else { super.actionPerformed(e); } } private void findFirst() { if (dlgFind != null && dlgFind.isVisible()) { return; } dlgFind = new FindDialog(getFrame(), encoding) { @Override protected void doSearch(byte[] bytes) { doSearchFromPos(bytes, 0, true); } }; dlgFind.setSearchBytes(lastSearchBytes); dlgFind.showDialog(); } private void doSearchFromPos(byte[] bytes, long pos, boolean next) { lastSearchBytes = bytes; try { long lastSearchResult; if (next) { lastSearchResult = ByteBufferSearchUtils.indexOf(byteBuffer, bytes, pos); } else { lastSearchResult = ByteBufferSearchUtils.indexOfBackward(byteBuffer, bytes, pos); } if (lastSearchResult >= 0) { hexTable.gotoOffset(lastSearchResult); clearStatusMessage(); } else { if (statusBar != null) { statusBar.setStatusMessage(i18n("hex_viewer.search_not_found")); } } } catch (IOException e) { log.error("Search error", e); } } private void findNext() { if (lastSearchBytes != null && lastSearchBytes.length > 0) { long pos = hexTable.getCurrentAddress()+1; doSearchFromPos(lastSearchBytes, pos, true); } } private void findPrev() { if (lastSearchBytes != null && lastSearchBytes.length > 0) { long pos = hexTable.getCurrentAddress()-1; doSearchFromPos(lastSearchBytes, pos, false); } } private void gotoOffset() { if (dlgGoto == null) { dlgGoto = new GotoDialog(getFrame(), model.getSize() - 1, (offset) -> hexTable.gotoOffset(offset)); } if (!dlgGoto.isVisible()) { dlgGoto.showDialog(); } } @Override public void setSearchedText(String searchedText) { try { lastSearchBytes = searchedText.getBytes(encoding); } catch (UnsupportedEncodingException e) { log.error("setSearchedText", e); } } @Override public void setSearchedBytes(byte[] searchedBytes) { if (searchedBytes != null) { lastSearchBytes = searchedBytes; } } private void setStatusMessage(String msg) { if (statusBar != null) { statusBar.setStatusMessage(msg); } } private void clearStatusMessage() { setStatusMessage(""); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/hex/StatusBar.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.hex import com.mucommander.ui.main.statusbar.FileWindowsListButton import org.fife.ui.StatusBar import org.fife.ui.StatusBarPanel import ru.trolsoft.utils.StrUtils import java.awt.BorderLayout import java.awt.Font import java.awt.GridBagConstraints import javax.swing.JLabel /** * */ class StatusBar : StatusBar("") { private val lbFiles: FileWindowsListButton = FileWindowsListButton(true) private val lblOffset: JLabel private val lblEncoding: JLabel private val lblValue: JLabel @JvmField var maxOffset: Long = -1 init { val panelWindows = StatusBarPanel(BorderLayout()) panelWindows.add(lbFiles) lblOffset = createLabel() val panelOffset = StatusBarPanel(BorderLayout(), lblOffset) lblEncoding = createLabel() val panelEncoding = StatusBarPanel(BorderLayout(), lblEncoding) lblValue = createLabel() val panelValue = StatusBarPanel(BorderLayout(), lblValue) // Make the layout such that different items can be different sizes. val c = GridBagConstraints().apply { fill = GridBagConstraints.BOTH weightx = 0.0 } addStatusBarComponent(panelWindows, c) addStatusBarComponent(panelOffset, c) addStatusBarComponent(panelValue, c) addStatusBarComponent(panelEncoding, c) } private fun createLabel(): JLabel { val lbl = JLabel() val fnt = lbl.getFont() lbl.setFont(Font(Font.MONOSPACED, fnt.getStyle(), fnt.getSize())) return lbl } fun setOffset(offset: Long) { var str = StrUtils.dwordToHexStr(offset) if (maxOffset >= 0) { str += " / " + StrUtils.dwordToHexStr(maxOffset) } lblOffset.setText(str) } fun setEncoding(encoding: String?) { lblEncoding.setText(encoding) } fun setByteValue(v: Byte) { val s = StrUtils.byteToBinaryStr(v) + " - " + StrUtils.byteToOctalStr(v) lblValue.setText(s) } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/html/HtmlFactory.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.html; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.utils.text.Translator; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.ViewerFactory; /** * ViewerFactory implementation for creating html viewers. * * @author Oleg Trifonov */ public class HtmlFactory implements ViewerFactory { public final static ExtensionFilenameFilter HTML_FILTER = new ExtensionFilenameFilter(new String[] { ".htm", ".html" }); public static Boolean webViewIsAvailable; static { HTML_FILTER.setCaseSensitive(false); } @Override public boolean canViewFile(AbstractFile file) { if (webViewIsAvailable == null) { webViewIsAvailable = isWebViewIsAvailable(); } return webViewIsAvailable && !file.isDirectory() && HTML_FILTER.accept(file); } @Override public FileViewer createFileViewer() { return new HtmlViewer(); } @Override public String getName() { return Translator.get("viewer_type.html"); } private static boolean isWebViewIsAvailable() { try { Class.forName("javafx.application.Platform"); Class.forName("javafx.embed.swing.JFXPanel"); Class.forName("javafx.scene.Group"); Class.forName("javafx.scene.Scene"); Class.forName("javafx.scene.web.WebView"); return true; } catch (Throwable e) { return false; } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/html/HtmlViewer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.html; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.io.StreamUtils; import com.mucommander.ui.viewer.FileViewer; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import org.fife.ui.StatusBar; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; /** * @author Oleg Trifonov */ public class HtmlViewer extends FileViewer { private WebView webView; private String url; private String content; HtmlViewer() { super(); } @Override protected void show(final AbstractFile file) throws IOException { boolean localFile = file.getTopAncestor() instanceof LocalFile; if (localFile) { url = file.getJavaNetURL().toString(); } else { InputStream is = file.getInputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream(); StreamUtils.copyStream(is, out); content = out.toString(); } try { final JFXPanel jfxPanel = new JFXPanel(); setComponentToPresent(jfxPanel); getViewport().addChangeListener(e -> { if (webView != null) { int w = getViewport().getWidth(); int h = getViewport().getHeight(); webView.setMinWidth(w); webView.setMaxWidth(w); webView.setMinHeight(h); webView.setMaxHeight(h); } }); Platform.setImplicitExit(false); Platform.runLater(() -> initFX(jfxPanel)); } catch (Throwable e) { e.printStackTrace(); } } /* Creates a WebView and fires up google.com */ private void initFX(final JFXPanel fxPanel) { try { Group group = new Group(); Scene scene = new Scene(group); fxPanel.setScene(scene); webView = new WebView(); group.getChildren().add(webView); WebEngine webEngine = webView.getEngine(); if (url != null) { webEngine.load(url); } else { webEngine.loadContent(content); } } catch (Throwable t) { t.printStackTrace(); } } @Override protected StatusBar getStatusBar() { return null; } @Override protected void saveStateOnClose() { } @Override protected void restoreStateOnStartup() { } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/image/ImageFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.image; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.utils.text.Translator; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.ViewerFactory; /** * ViewerFactory implementation for creating image viewers. * * @author Nicolas Rinaudo */ public class ImageFactory implements ViewerFactory { /** Used to IMAGE_FILTER out file extensions that the image viewer cannot open. */ public final static ExtensionFilenameFilter IMAGE_FILTER = new ExtensionFilenameFilter(new String[] { ".png", ".gif", ".jpg", ".jpeg", ".bmp", ".wbmp", // java built in formats ".ico", ".psd", ".tga", ".tiff", ".tif", ".pnm", ".pbm", ".pgm", ".ppm", ".svg" // additional formats }); static { IMAGE_FILTER.setCaseSensitive(false); } public ImageFactory() { } @Override public boolean canViewFile(AbstractFile file) { // Do not allow directories if (file.isDirectory()) { return false; } if ("scr".equalsIgnoreCase(file.getExtension()) && file.getSize() == ZxSpectrumScrImage.SCR_IMAGE_FILE_SIZE ) { return true; } return IMAGE_FILTER.accept(file); } public FileViewer createFileViewer() { return new ImageViewer(); } @Override public String getName() { return Translator.get("viewer_type.image"); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/image/ImageViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.image; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import javax.imageio.spi.IIORegistry; import javax.swing.*; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcSnapshot; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeListener; import com.mucommander.ui.theme.ThemeManager; import com.mucommander.ui.viewer.FileFrame; import com.mucommander.ui.viewer.FileViewer; import com.twelvemonkeys.imageio.plugins.bmp.BMPImageReaderSpi; import com.twelvemonkeys.imageio.plugins.bmp.CURImageReaderSpi; import com.twelvemonkeys.imageio.plugins.dds.DDSImageReaderSpi; import com.twelvemonkeys.imageio.plugins.hdr.HDRImageReaderSpi; import com.twelvemonkeys.imageio.plugins.icns.ICNSImageReaderSpi; import com.twelvemonkeys.imageio.plugins.iff.IFFImageReaderSpi; import com.twelvemonkeys.imageio.plugins.jpeg.JPEGImageReaderSpi; import com.twelvemonkeys.imageio.plugins.pcx.PCXImageReaderSpi; import com.twelvemonkeys.imageio.plugins.pnm.PNMImageReaderSpi; import com.twelvemonkeys.imageio.plugins.psd.PSDImageReaderSpi; import com.twelvemonkeys.imageio.plugins.sgi.SGIImageReaderSpi; import com.twelvemonkeys.imageio.plugins.tga.TGAImageReaderSpi; import com.twelvemonkeys.imageio.plugins.tiff.TIFFImageReaderSpi; import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi; import com.twelvemonkeys.imageio.plugins.xwd.XWDImageReaderSpi; import lombok.extern.slf4j.Slf4j; import net.sf.image4j.codec.ico.ICODecoder; import org.apache.batik.transcoder.Transcoder; import org.apache.batik.transcoder.TranscoderException; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import org.apache.commons.imaging.ImageReadException; import ru.trolsoft.ui.TMenuSeparator; /** * A simple image viewer, capable of displaying PNG, GIF and JPEG images. * * @author Maxence Bernard, Arik Hadas, Oleg Trifonov */ @Slf4j class ImageViewer extends FileViewer implements ActionListener { private static final Cursor CURSOR_WAIT = new Cursor(Cursor.WAIT_CURSOR); private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor(); private static final Cursor CURSOR_CROSS = new Cursor(Cursor.CROSSHAIR_CURSOR); private static final Color TRANSPARENT_COLOR_1 = new Color(0x666666); private static final Color TRANSPARENT_COLOR_2 = new Color(0x999999); private static final int TRANSPARENT_GRID_STEP = 8; private BufferedImage image; //private BufferedImage scaledImage; private double zoomFactor; private boolean vectorImage; /** Menu bar */ private final JMenu controlsMenu; // Items // private final JMenuItem prevImageItem; private final JMenuItem nextImageItem; private final JMenuItem zoomInItem; private final JMenuItem zoomOutItem; private final ImageViewerImpl imageViewerImpl; private List filesInDirectory; private int indexInDirectory = -1; private StatusBar statusBar; private boolean waitCursorMode = false; private boolean hasTransparentPixels; /** * Unknown swing issue = MouseMovement events doesn't received on windows show. * To fix it we make windows resize and undo it after start. */ private boolean mouseMovementIssueFixed = false; private static boolean initiated = false; public static void init() { if (initiated) { return; } try { IIORegistry registry = IIORegistry.getDefaultInstance(); registry.registerServiceProvider(new JPEGImageReaderSpi()); registry.registerServiceProvider(new PSDImageReaderSpi()); registry.registerServiceProvider(new TIFFImageReaderSpi()); registry.registerServiceProvider(new BMPImageReaderSpi()); registry.registerServiceProvider(new CURImageReaderSpi()); registry.registerServiceProvider(new DDSImageReaderSpi()); registry.registerServiceProvider(new HDRImageReaderSpi()); registry.registerServiceProvider(new ICNSImageReaderSpi()); registry.registerServiceProvider(new IFFImageReaderSpi()); registry.registerServiceProvider(new PCXImageReaderSpi()); registry.registerServiceProvider(new PNMImageReaderSpi()); registry.registerServiceProvider(new SGIImageReaderSpi()); registry.registerServiceProvider(new TGAImageReaderSpi()); registry.registerServiceProvider(new WebPImageReaderSpi()); registry.registerServiceProvider(new XWDImageReaderSpi()); } catch (Exception e) { log.error("Error registering additional image service providers", e); } initiated = true; } ImageViewer() { init(); imageViewerImpl = new ImageViewerImpl(); setComponentToPresent(imageViewerImpl); // create Go menu MnemonicHelper menuMnemonicHelper = new MnemonicHelper(); controlsMenu = MenuToolkit.addMenu(i18n("image_viewer.controls_menu"), menuMnemonicHelper, null); nextImageItem = MenuToolkit.addMenuItem(controlsMenu, i18n("image_viewer.next_image"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), this); prevImageItem = MenuToolkit.addMenuItem(controlsMenu, i18n("image_viewer.previous_image"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), this); controlsMenu.add(new TMenuSeparator()); if (OsFamily.MAC_OS_X.isCurrent()) { zoomInItem = MenuToolkit.addMenuItem(controlsMenu, i18n("image_viewer.zoom_in"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), this); zoomOutItem = MenuToolkit.addMenuItem(controlsMenu, i18n("image_viewer.zoom_out"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), this); } else { zoomInItem = MenuToolkit.addMenuItem(controlsMenu, i18n("image_viewer.zoom_in"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_ADD, 0), this); zoomOutItem = MenuToolkit.addMenuItem(controlsMenu, i18n("image_viewer.zoom_out"), menuMnemonicHelper, KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT, 0), this); } } @Override public JMenuBar getMenuBar() { JMenuBar menuBar = super.getMenuBar(); menuBar.add(controlsMenu); setMainKeyListener(imageViewerImpl, menuBar); return menuBar; } @Override protected StatusBar getStatusBar() { if (statusBar == null) { statusBar = new StatusBar(); } return statusBar; } @Override protected void saveStateOnClose() { // Run GC for big images if (image != null && image.getWidth()*image.getHeight() > 1024*200) { System.gc(); } } @Override protected void restoreStateOnStartup() { } private synchronized void loadImage(AbstractFile file) throws IOException, ImageReadException { setFrameCursor(CURSOR_WAIT); if (statusBar != null) { statusBar.setFileSize(file.getSize()); statusBar.setDateTime(file.getLastModifiedDate()); } loadImageFile(file); int imageWidth = image.getWidth(); int imageHeight = image.getHeight(); this.hasTransparentPixels = image.getColorModel().hasAlpha(); if (statusBar != null) { statusBar.setImageSize(imageWidth, imageHeight); } this.zoomFactor = 1.0; Dimension screen = TcSnapshot.getScreenSize(); double zoomFactorX = 1.0 * screen.width / imageWidth; double zoomFactorY = 1.0 * screen.height / imageHeight; zoomFactor = Math.min(zoomFactorX, zoomFactorY); if (zoomFactor > 1.0) { zoomFactor = 1.0; } zoom(zoomFactor); fixMouseMovementEventsIssue(); checkNextPrev(); setFrameCursor(CURSOR_DEFAULT); try { file.closePushbackInputStream(); } catch (IOException e) { log.error("Stream close error", e); } } private void loadImageFile(AbstractFile file) throws IOException { final String ext = file.getExtension().toLowerCase(); if ("scr".equals(ext) && file.getSize() == ZxSpectrumScrImage.SCR_IMAGE_FILE_SIZE) { this.image = ZxSpectrumScrImage.load(file.getInputStream()); if (statusBar != null) { statusBar.setImageBpp(4); } } else if ("ico".equals(ext)) { this.image = ICODecoder.read(file.getInputStream()).getFirst(); } else if ("svg".equals(ext)) { this.image = transcodeSvgDocument(file, 0, 0); } else { try (InputStream is = file.getInputStream()) { this.image = ImageIO.read(is); } if (image == null) { throw new IllegalArgumentException("No reader for a given file: " + file); } if (statusBar != null) { statusBar.setImageBpp(image.getColorModel().getPixelSize()); } } vectorImage = "svg".equalsIgnoreCase(ext); } private static byte[] loadFile(AbstractFile file) { byte[] data = new byte[(int) file.getSize()]; try (InputStream is = file.getInputStream()) { int readTotal = 0; while (readTotal < data.length) { int bytesRead = is.read(data, readTotal, data.length - readTotal); if (bytesRead < 0) { break; } readTotal += bytesRead; } } catch (IOException e) { log.error("File load error", e); } return data; } private void setFrameCursor(Cursor cursor) { if (cursor == CURSOR_WAIT) { waitCursorMode = true; } else if (cursor == CURSOR_DEFAULT) { waitCursorMode = false; } else if (cursor == CURSOR_CROSS && waitCursorMode) { return; } FileFrame frame = getFrame(); if (frame != null && frame.getCursor() != cursor) { frame.setCursor(cursor); } } private synchronized void zoom(double factor) { setFrameCursor(CURSOR_WAIT); final int srcWidth = image.getWidth(null); final int srcHeight = image.getHeight(null); final int scaledWidth = (int)(srcWidth*factor); final int scaledHeight = (int)(srcHeight*factor); if (factor != 1.0) { AbstractFile file = filesInDirectory.get(indexInDirectory); if ("svg".equalsIgnoreCase(file.getExtension())) { try { this.image = transcodeSvgDocument(file, scaledWidth, scaledHeight); } catch (IOException e) { log.error("Transcode error", e); } // } else { // this.scaledImage = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_ARGB); // AffineTransform at = new AffineTransform(); // at.scale(factor, factor); // AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); // this.scaledImage = scaleOp.filter(this.image, this.scaledImage); // this.scaledImage = image; } // } else { // this.scaledImage = image; } if (statusBar != null) { statusBar.setZoom(factor); } checkZoom(); setFrameCursor(CURSOR_DEFAULT); } private void fixMouseMovementEventsIssue() { if (mouseMovementIssueFixed) { return; } mouseMovementIssueFixed = true; Runnable task = () -> { try { int w = getFrame().getWidth(); int h = getFrame().getHeight(); getFrame().setSize(w, h-1); getFrame().setSize(w, h); } catch (Exception ignore) { } }; try (var executor = Executors.newSingleThreadScheduledExecutor()) { executor.schedule(task, 1000, TimeUnit.MILLISECONDS); } } private void updateFrame() { FileFrame frame = getFrame(); // Revalidate, pack and repaint should be called in this order frame.setTitle(this.getTitle()); imageViewerImpl.revalidate(); //frame.pack(); frame.getContentPane().repaint(); } private void checkZoom() { // Dimension d = MuSnapshot.getScreenSize(); // zoomInItem.setEnabled(zoomFactor < 1.0 || (2*zoomFactor*image.getWidth(null) < d.width // && 2*zoomFactor*image.getHeight(null) < d.height)); // // zoomOutItem.setEnabled(zoomFactor > 1.0 || (zoomFactor / 2 * image.getWidth(null) > 160 // && zoomFactor / 2 * image.getHeight(null) > 120)); zoomInItem.setEnabled(zoomFactor < 8); zoomOutItem.setEnabled( zoomFactor > 0.1); } private void checkNextPrev() { prevImageItem.setEnabled(getPrevFileIndex() >= 0); nextImageItem.setEnabled(getNextFileIndex() >= 0); } @Override public void show(AbstractFile file) throws IOException { if (filesInDirectory == null) { filesInDirectory = new ArrayList<>(); AbstractFile[] ls = file.getParent().ls(); ImageFactory imageFactory = new ImageFactory(); for (AbstractFile f : ls) { if (imageFactory.canViewFile(f)) { filesInDirectory.add(f); } } for (int i = 0 ; i < filesInDirectory.size(); i++) { AbstractFile f = filesInDirectory.get(i); if (f.equals(file)) { indexInDirectory = i; break; } } if (statusBar != null) { statusBar.setFileNumber(indexInDirectory + 1, filesInDirectory.size()); } } try { loadImage(file); } catch (ImageReadException e) { log.error("Load error", e); throw new IOException("Image parsing error", e); } } @Override public String getTitle() { return filesInDirectory.get(indexInDirectory).toString(); //return file.getAbsolutePath()+" - "+image.getWidth(null)+"x"+image.getHeight(null)+" - "+((int)(zoomFactor*100))+"%"; } @Override public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if (source == zoomInItem && zoomInItem.isEnabled()) { zoomFactor *= 2; zoom(zoomFactor); updateFrame(); } else if(source == zoomOutItem && zoomOutItem.isEnabled()) { zoomFactor /= 2; zoom(zoomFactor); updateFrame(); } else if (source == nextImageItem && nextImageItem.isEnabled()) { gotoNextFile(); } else if (source == prevImageItem && prevImageItem.isEnabled()) { gotoPrevFile(); } else { super.actionPerformed(e); } } private int getNextFileIndex() { return indexInDirectory < filesInDirectory.size() - 1 ? indexInDirectory + 1 : -1; } private void gotoNextFile() { int index = getNextFileIndex(); if (index >= 0) { indexInDirectory = index; gotoFile(); } } private int getPrevFileIndex() { return indexInDirectory > 0 ? indexInDirectory - 1 : -1; } private void gotoPrevFile() { int index = getPrevFileIndex(); if (index >= 0) { indexInDirectory = index; gotoFile(); } } private void gotoFile() { try { show(filesInDirectory.get(indexInDirectory)); if (statusBar != null) { statusBar.setFileNumber(indexInDirectory + 1, filesInDirectory.size()); } updateFrame(); } catch (IOException e) { InformationDialog.showErrorDialog(this, i18n("file_viewer.view_error_title"), i18n("file_viewer.view_error")); log.error("Update close error", e); } } private static String colorToRgbStr(int color) { int a = (color >> 24) & 0xff; int r = (color >> 16) & 0xff; int g = (color >> 8) & 0xff; int b = (color) & 0xff; String result = r + ", " + g + ", " + b; if (a == 0xff) { result = "RGB: (" + result + ")"; } else { result = a + ", " + result; result = "ARGB: (" + result + ")"; } return result; } private static String colorToHexStr(int color) { int a = (color >> 24) & 0xff; if (a == 0xff) { color &= 0x00ffffff; } String result = Integer.toHexString(color); while (result.length() < 6) { result = '0' + result; } if (result.length() == 7) { result = '0' + result; } return '#' + result.toUpperCase(); } private static BufferedImage transcodeSvgDocument(AbstractFile file, float width, float height) throws IOException { // create a PNG transcoder. Transcoder t = new PNGTranscoder(); // Set the transcoding hints. if (width > 0) { t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, width); } if (height > 0) { t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, height); } t.addTranscodingHint(PNGTranscoder.KEY_XML_PARSER_VALIDATING, false); try (InputStream istream = file.getInputStream(); ByteArrayOutputStream ostream = new ByteArrayOutputStream()) { TranscoderInput input = new TranscoderInput(istream); TranscoderOutput output = new TranscoderOutput(ostream); // Save the image. t.transcode(input, output); // Flush and close the stream. ostream.flush(); byte[] imgData = ostream.toByteArray(); // Return the newly rendered image. return ImageIO.read(new ByteArrayInputStream(imgData)); } catch (TranscoderException e) { log.error("SVG transcode error", e); return null; } } private int getScaledWidth() { if (image == null) { return 0; } return vectorImage ? image.getWidth() : (int)(zoomFactor*image.getWidth()); } private int getScaledHeight() { if (image == null) { return 0; } return vectorImage ? image.getHeight() : (int)(zoomFactor*image.getHeight()); } /** * Image viewer panel */ private class ImageViewerImpl extends JPanel implements MouseMotionListener, MouseListener, ThemeListener { private Color backgroundColor; ImageViewerImpl() { backgroundColor = ThemeManager.getCurrentColor(Theme.EDITOR_BACKGROUND_COLOR); ThemeManager.addCurrentThemeListener(this); addMouseListener(this); addMouseMotionListener(this); } @Override public void paint(Graphics g) { int frameWidth = getWidth(); int frameHeight = getHeight(); g.setColor(backgroundColor); g.fillRect(0, 0, frameWidth, frameHeight); final int imageWidth = getScaledWidth(); final int imageHeight = getScaledHeight(); final int x0 = Math.max(0, (frameWidth-imageWidth)/2); final int y0 = Math.max(0, (frameHeight-imageHeight)/2); if (hasTransparentPixels) { int cellW = imageWidth/TRANSPARENT_GRID_STEP; if (imageWidth % TRANSPARENT_GRID_STEP > 0) { cellW++; } int cellH = imageHeight/TRANSPARENT_GRID_STEP; if (imageHeight % TRANSPARENT_GRID_STEP > 0) { cellH++; } int x = x0; int w = TRANSPARENT_GRID_STEP; for (int cellX = 0; cellX < cellW; cellX++) { if (cellX == cellW-1) { w = (imageWidth % TRANSPARENT_GRID_STEP == 0) ? TRANSPARENT_GRID_STEP : (imageWidth % TRANSPARENT_GRID_STEP); } int y = y0; int h = TRANSPARENT_GRID_STEP; for (int cellY = 0; cellY < cellH; cellY++) { g.setColor((cellX + cellY) % 2 == 0 ? TRANSPARENT_COLOR_1 : TRANSPARENT_COLOR_2); if (cellY == cellH-1) { h = (imageHeight % TRANSPARENT_GRID_STEP == 0) ? TRANSPARENT_GRID_STEP : (imageHeight % TRANSPARENT_GRID_STEP); } g.fillRect(x, y, w, h); y += TRANSPARENT_GRID_STEP; } x += TRANSPARENT_GRID_STEP; } } if (image != null) { if (vectorImage) { g.drawImage(image, x0, y0, null); } else { g.drawImage(image, x0, y0, x0 + imageWidth, y0 + imageHeight, 0, 0, image.getWidth(), image.getHeight(), null, null); } } } @Override public synchronized Dimension getPreferredSize() { return image == null ? new Dimension(320, 200) : new Dimension(getScaledWidth(), getScaledHeight()); } /** * Receives theme color changes notifications. */ @Override public void colorChanged(ColorChangedEvent event) { if (event.getColorId() == Theme.EDITOR_BACKGROUND_COLOR) { backgroundColor = event.getColor(); repaint(); } } @Override public void mouseMoved(MouseEvent e) { if (image == null) { return; } int x = e.getX(); int y = e.getY(); final int imageOffsetX = Math.max(0, (imageViewerImpl.getWidth() - getScaledWidth())/2); final int imageOffsetY = Math.max(0, (imageViewerImpl.getHeight() - getScaledHeight())/2); boolean inImageArea = x >= imageOffsetX && y >= imageOffsetY && x < imageOffsetX + getScaledWidth() && y < imageOffsetY + getScaledHeight(); if (inImageArea) { setFrameCursor(CURSOR_CROSS); } else { setFrameCursor(CURSOR_DEFAULT); } } @Override public void mouseClicked(MouseEvent e) { if (image == null) { return; } int x = e.getX(); int y = e.getY(); final int w = getScaledWidth(); final int h = getScaledHeight(); final int imageOffsetX = Math.max(0, (imageViewerImpl.getWidth()-w)/2); final int imageOffsetY = Math.max(0, (imageViewerImpl.getHeight()-h)/2); int pixelX = x - imageOffsetX; if (pixelX < 0 || pixelX >= w) { return; } int pixelY = y - imageOffsetY; if (pixelY < 0 || pixelY >= h) { return; } pixelX = (int)(pixelX/zoomFactor); pixelY = (int)(pixelY/zoomFactor); int color = image.getRGB(pixelX, pixelY); // int r = (color >> 16) & 0xff; // int g = (color >> 8) & 0xff; // int b = (color) & 0xff; if (statusBar != null) { statusBar.setStatusMessage("XY: (" + pixelX + ", " + pixelY + ") " + colorToRgbStr(color) + " HTML: (" + colorToHexStr(color) + ")"); } } @Override public void mouseExited(MouseEvent e) { setFrameCursor(CURSOR_DEFAULT); } /** * Not used, implemented as a no-op. */ @Override public void fontChanged(FontChangedEvent event) {} @Override public void mouseDragged(MouseEvent e) { // mouseMoved(e); } @Override public void mousePressed(MouseEvent e) {} @Override public void mouseReleased(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) {} } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/image/StatusBar.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.image; import com.mucommander.ui.main.statusbar.FileWindowsListButton; import org.fife.ui.StatusBarPanel; import javax.swing.JLabel; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.util.Date; /** * Created on 11/03/14. */ public class StatusBar extends org.fife.ui.StatusBar { private FileWindowsListButton lbFiles; private JLabel lblImageSize; private JLabel lblFileNumber; private JLabel lblZoom; private JLabel lblFileSize; private JLabel lblDateTime; private int imageWidth, imageHeight, imageBpp; public StatusBar() { super(""); lbFiles = new FileWindowsListButton(true); StatusBarPanel panelWindows = new StatusBarPanel(new BorderLayout()); panelWindows.add(lbFiles); lblImageSize = new JLabel(); StatusBarPanel panelImageSize = new StatusBarPanel(new BorderLayout(), lblImageSize); lblFileNumber = new JLabel(); StatusBarPanel panelFileNumber = new StatusBarPanel(new BorderLayout(), lblFileNumber); lblZoom = new JLabel(); StatusBarPanel panelZoom = new StatusBarPanel(new BorderLayout(), lblZoom); lblFileSize = new JLabel(); StatusBarPanel panelFileSize = new StatusBarPanel(new BorderLayout(), lblFileSize); lblDateTime = new JLabel(); StatusBarPanel panelDateTime = new StatusBarPanel(new BorderLayout(), lblDateTime); // Make the layout such that different items can be different sizes. GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; c.weightx = 0.0; addStatusBarComponent(panelWindows, c); addStatusBarComponent(panelImageSize, c); addStatusBarComponent(panelFileNumber, c); addStatusBarComponent(panelZoom, c); addStatusBarComponent(panelFileSize, c); addStatusBarComponent(panelDateTime, c); } public void setImageSize(int width, int height) { imageWidth = width; imageHeight = height; updateSizePanel(); } public void setImageBpp(int bpp) { imageBpp = bpp; updateSizePanel(); } private void updateSizePanel() { lblImageSize.setText(imageWidth + " x " + imageHeight + " x " + imageBpp + " BPP"); } public void setFileNumber(int current, int total) { lblFileNumber.setText(current + " / " + total); } public void setZoom(double zoom) { long zoomInt = Math.round(zoom*100); lblZoom.setText(zoomInt + " %"); } public void setFileSize(long fileSize) { lblFileSize.setText(fileSize + " bytes"); } public void setDateTime(long date) { lblDateTime.setText(new Date(date).toString()); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/image/ZxSpectrumScrImage.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.image; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; /** * Created on 23/02/14. */ public class ZxSpectrumScrImage { private static final int WIDTH = 256; private static final int HEIGHT = 192; private static final int DEFAULT_ZOOM = 1; //private static final int ROWS = static final int SCR_IMAGE_FILE_SIZE = 6912; private static final Color[] PALETTE_BRIGHT_0 = { new Color(0x000000), new Color(0x0000cd), new Color(0xcd0000), new Color(0xff00ff), new Color(0x00cd00), new Color(0x00cdcd), new Color(0xcdcd00), new Color(0xcdcdcd) }; private static final Color[] PALETTE_BRIGHT_1 = { new Color(0x000000), new Color(0x0000ff), new Color(0xff0000), new Color(0xff00ff), new Color(0x00ff00), new Color(0x00ffff), new Color(0xffff00), new Color(0xffffff) }; public static BufferedImage load(InputStream is, int zoomFactor) { if (is == null) { return null; } byte[] data = new byte[SCR_IMAGE_FILE_SIZE]; int readTotal = 0; try { while (readTotal < SCR_IMAGE_FILE_SIZE) { int bytesRead = is.read(data, readTotal, SCR_IMAGE_FILE_SIZE - readTotal); if (bytesRead < 0) { break; } readTotal += bytesRead; } } catch (IOException e) { e.printStackTrace(); } try { is.close(); } catch (IOException e) { e.printStackTrace(); } if (readTotal != SCR_IMAGE_FILE_SIZE) { return null; } return load(data, zoomFactor); } public static BufferedImage load(byte[] data, int zoomFactor) { if (data == null || data.length != SCR_IMAGE_FILE_SIZE) { return null; } if (zoomFactor < 1) { zoomFactor = 1; } BufferedImage result = new BufferedImage(WIDTH*zoomFactor, HEIGHT*zoomFactor, BufferedImage.TYPE_INT_RGB); Graphics2D g = result.createGraphics(); // read colour data (attributes) byte[] ink = new byte[32*24]; byte[] paper = new byte[32*24]; boolean[] bright = new boolean[32*24]; for (int row = 0; row < 24; row++) { for (int col = 0; col < 32; col++) { int offset = WIDTH * HEIGHT / 8 + (row*32)+col; byte val = data[offset]; ink[row*32+col] = (byte)( val & 0b00000111); paper[row*32+col] = (byte)((val & 0b00111000)>>3); bright[row*32+col] = ((val & 0b11000000)>>6) != 0; } } // render pixels for (int y = 0; y < HEIGHT; y++) { // display address 010[L4][L3][R2][R1][R0][L2][L1][L0][C4][C3][C2][C1][C0] int line = y / 8; // line number (0..23, 5 bits) int row = y % 8; // pixel row in line (0..8, 3 bits) final int lPart = ((line & 0x18)<<8)|((line & 0x7)<<5); final int rPart = (row & 0x7)<<8; for (int x = 0; x < WIDTH; x++) { int col = x / 8; // column number (5 bits) int cPart = col & 0x1F; int bit = 7 - (x % 8); int address = (((0x4000 | lPart) | rPart) | cPart) - 16384; int attrIndex = ((y/8)*32)+(x/8); int inkIndex = ink[attrIndex]; int paperIndex = paper[attrIndex]; boolean isBright = bright[attrIndex]; if (((data[address]&(0x1<>bit) != 0) { g.setColor(PALETTE_BRIGHT_0[inkIndex]); } else { g.setColor(isBright ? PALETTE_BRIGHT_1[paperIndex] : PALETTE_BRIGHT_0[paperIndex]); } g.fillRect(x*zoomFactor, y*zoomFactor, zoomFactor, zoomFactor); } } return result; } public static BufferedImage load(byte[] data) { return load(data, DEFAULT_ZOOM); } public static BufferedImage load(InputStream is) { return load(is, DEFAULT_ZOOM); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/image/package.html ================================================ Provides image viewing classes. ================================================ FILE: src/main/java/com/mucommander/ui/viewer/package.html ================================================ Generic classes used to deal with file viewing / editing. ================================================ FILE: src/main/java/com/mucommander/ui/viewer/pdf/PdfFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.pdf; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import com.mucommander.utils.text.Translator; import com.mucommander.ui.viewer.FileViewer; import com.mucommander.ui.viewer.ViewerFactory; /** * ViewerFactory implementation for creating pdf viewers. * * @author Oleg Trifonov */ public class PdfFactory implements ViewerFactory { public final static ExtensionFilenameFilter PDF_FILTER = new ExtensionFilenameFilter(new String[] {".pdf"}); static { PDF_FILTER.setCaseSensitive(false); } @Override public boolean canViewFile(AbstractFile file) { return !file.isDirectory() && PDF_FILTER.accept(file); } @Override public FileViewer createFileViewer() { return new PdfViewer(); } @Override public String getName() { return Translator.get("viewer_type.pdf"); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/pdf/PdfViewer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/trolcommander * Copyright (C) 2014-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.pdf; import com.mucommander.commons.file.AbstractFile; import com.mucommander.ui.viewer.FileViewer; import org.fife.ui.StatusBar; import org.icepdf.ri.common.MyAnnotationCallback; import org.icepdf.ri.common.SwingController; import org.icepdf.ri.common.SwingViewBuilder; import java.io.IOException; import java.io.InputStream; /** * A simple pdf viewer * * @author Oleg Trifonov */ public class PdfViewer extends FileViewer { private final SwingController controller; PdfViewer() { // create a controller and a swing factory controller = new SwingController(); SwingViewBuilder factory = new SwingViewBuilder(controller); // add interactive mouse link annotation support via callback controller.getDocumentViewController().setAnnotationCallback( new org.icepdf.ri.common.MyAnnotationCallback(controller.getDocumentViewController())); // build viewer component and add it to the applet content pane. MyAnnotationCallback myAnnotationCallback = new MyAnnotationCallback( controller.getDocumentViewController()); controller.getDocumentViewController().setAnnotationCallback(myAnnotationCallback); // build the viewer with a menubar //getContentPane().setLayout(new BorderLayout()); //getContentPane().add(factory.buildViewerPanel(), BorderLayout.CENTER); //getContentPane().add(factory.buildCompleteMenuBar(), BorderLayout.NORTH); setComponentToPresent(factory.buildViewerPanel()); } @Override protected void show(AbstractFile file) throws IOException { String description = ""; String path = file.getPath(); try (InputStream is = file.getInputStream()) { controller.openDocument(is, description, path); } } @Override protected StatusBar getStatusBar() { return null; } @Override protected void saveStateOnClose() { org.icepdf.core.util.Library.shutdownThreadPool(); } @Override protected void restoreStateOnStartup() { org.icepdf.core.util.Library.initializeThreadPool(); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/FileType.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2023 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.commons.file.AbstractFile; import lombok.Getter; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.WildcardFileFilter; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import java.io.File; /** * @author Oleg Trifonov * Created on 04/01/14. */ public enum FileType { NONE("None", SyntaxConstants.SYNTAX_STYLE_NONE), ACTIONSCRIPT("ActionScript", SyntaxConstants.SYNTAX_STYLE_ACTIONSCRIPT, "*.as"), ASSEMBLER_X86("Assembler x86", SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_X86, "*.asm"), ASSEMBLER_AVR("Assembler AVR", SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_AVR, "*.lss,*.s"), ASSEMBLER_6502("Assembler 6502", SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_6502, "*.asm"), ASSEMBLER_RISCV("Assembler Risc-V", SyntaxConstants.SYNTAX_STYLE_ASSEMBLER_RISCV, "*.lss,*.s"), AVR_RAT("AVR Rat", SyntaxConstants.SYNTAX_STYLE_AVR_RAT, "*.art,*.arth"), BBCODE("BBCode", SyntaxConstants.SYNTAX_STYLE_BBCODE), C("C", SyntaxConstants.SYNTAX_STYLE_C, "*.c,*.m"), CHIP_TEST("Chip test", SyntaxConstants.SYNTAX_STYLE_CHIP_TEST, "*.ic"), CLOJURE("Clojure", SyntaxConstants.SYNTAX_STYLE_CLOJURE, "*.clj"), CPP("C++", SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS, "*.cpp,*.cc,*.h,*.hpp,*.ino"), CSHARP("C#", SyntaxConstants.SYNTAX_STYLE_CSHARP, "*.cs"), CSS("CSS", SyntaxConstants.SYNTAX_STYLE_CSS, "*.css"), CSV("CSV", SyntaxConstants.SYNTAX_STYLE_CSV, "*.csv"), D("D", SyntaxConstants.SYNTAX_STYLE_D, "*.d"), DOCKERFILE("Dockerfile", SyntaxConstants.SYNTAX_STYLE_DOCKERFILE, "Dockerfile"), DART("Dart", SyntaxConstants.SYNTAX_STYLE_DART, "*.dart"), DTD("DTD", SyntaxConstants.SYNTAX_STYLE_DTD, "*.dtd"), FORTRAN("Fortran", SyntaxConstants.SYNTAX_STYLE_FORTRAN, "*.f,*.for,*.ftn,*.i"), GO("Go", SyntaxConstants.SYNTAX_STYLE_GO, "*.go"), GROOVY("Groovy", SyntaxConstants.SYNTAX_STYLE_GROOVY,"*.groovy,*.gvy,*.gy,*.gsh,*.gradle"), HANDLEBARS("Handlebars", SyntaxConstants.SYNTAX_STYLE_HANDLEBARS), HOSTS("Hosts", SyntaxConstants.SYNTAX_STYLE_HOSTS, "hosts"), HEX("Intel HEX", SyntaxConstants.SYNTAX_STYLE_HEX, "*.hex,*.ihex"), HTACCESS("htaccess", SyntaxConstants.SYNTAX_STYLE_HTACCESS, ".htaccess"), HTML("HTML", SyntaxConstants.SYNTAX_STYLE_HTML, "*.html,*.htm"), INI("ini", SyntaxConstants.SYNTAX_STYLE_INI, "*.ini"), JAVA("Java", SyntaxConstants.SYNTAX_STYLE_JAVA, "*.java"), JAVASCRIPT("JavaScript", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT, "*.js,*.ts"), JSON("Json", SyntaxConstants.SYNTAX_STYLE_JSON, "*.json"), JSON_WITH_COMMENTS("Json with comments", SyntaxConstants.SYNTAX_STYLE_JSON_WITH_COMMENTS, "*.json"), JSP("JSP", SyntaxConstants.SYNTAX_STYLE_JSP, "*.jsp"), KOTLIN("Kotlin", SyntaxConstants.SYNTAX_STYLE_KOTLIN, "*.kt,*.kts"), LATEX("Latex", SyntaxConstants.SYNTAX_STYLE_LATEX, "*.tex"), LISP("Lisp", SyntaxConstants.SYNTAX_STYLE_LISP, "*.lisp,*.lsp"), LUA("Lua", SyntaxConstants.SYNTAX_STYLE_LUA, "*.lua"), MAKEFILE("Makefile", SyntaxConstants.SYNTAX_STYLE_MAKEFILE, "Makefile,*.mk"), MARKDOWN("Markdown", SyntaxConstants.SYNTAX_STYLE_MARKDOWN, "*.md"), MXML("MXML", SyntaxConstants.SYNTAX_STYLE_MXML, "*.mxml"), NSIS("Nsis", SyntaxConstants.SYNTAX_STYLE_NSIS, "*.nsi"), PASCAL("Pascal", SyntaxConstants.SYNTAX_STYLE_DELPHI, "*.pas,*.dpr,*.pp,*.lpr"), PERL("Perl", SyntaxConstants.SYNTAX_STYLE_PERL, "*.pl"), PHP("PHP", SyntaxConstants.SYNTAX_STYLE_PHP, "*.php"), PROTOBUF("Protobuf", SyntaxConstants.SYNTAX_STYLE_PROTO, "*.proto"), PROPERTIES_FILE("Properties file", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE, "*.properties,*.prop,*.conf"), PYTHON("Python", SyntaxConstants.SYNTAX_STYLE_PYTHON, "*.py,make.builder"), RUBY("Ruby", SyntaxConstants.SYNTAX_STYLE_RUBY, "*.rb"), RUST("Rust", SyntaxConstants.SYNTAX_STYLE_RUST, "*.rs,*.rs.in,*.rlib"), SAS("SAS", SyntaxConstants.SYNTAX_STYLE_SAS, "*.sas"), SCALA("Scala", SyntaxConstants.SYNTAX_STYLE_SCALA, "*.scala"), SQL("SQL", SyntaxConstants.SYNTAX_STYLE_SQL, "*.sql"), TCL("TCL", SyntaxConstants.SYNTAX_STYLE_TCL, "*.tcl"), TYPESCRIPT("TypeScript", SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT, "*.ts"), UNIX_SHELL("Unix Shell", SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL, "*.sh,*.zsh,.zshrc,.profile,.bash_profile"), VISUAL_BASIC("VisualBasic", SyntaxConstants.SYNTAX_STYLE_VISUAL_BASIC, "*.bas,*.vb"), WINDOWS_BATCH("Windows bath", SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH, "*.bat,*.cmd"), XML("XML", SyntaxConstants.SYNTAX_STYLE_XML, "*.xml,Info.plist,*.jnlp,*.svg"), YAML("YAML", SyntaxConstants.SYNTAX_STYLE_YAML, "*.yml,*.yaml"); @Getter private final String contentType; private final WildcardFileFilter[] fileFilters; @Getter private final String name; FileType(String name, String contentType, String fileMasks) { this.name = name; this.contentType = contentType; this.fileFilters = buildFileFilters(fileMasks); } FileType(String name, String contentType) { this(name, contentType, null); } public static FileType getFileType(AbstractFile file) { return getFileType(file.toString()); } public static FileType getFileType(String fileName) { File f = new File(fileName); for (FileType ft : FileType.values()) { if (ft.checkFile(f)) { return ft; } } return NONE; } public boolean checkFile(File file) { for (WildcardFileFilter filter : fileFilters) { if (filter.accept(file)) { return true; } } return false; } private static WildcardFileFilter[] buildFileFilters(String fileMasks) { if (fileMasks == null) { return new WildcardFileFilter[0]; } String[] masks = fileMasks.split(","); WildcardFileFilter[] result = new WildcardFileFilter[masks.length]; for (int i = 0; i < masks.length; i++) { result[i] = WildcardFileFilter.builder().setWildcards(masks[i]).setIoCase(IOCase.INSENSITIVE).get(); } return result; } public static FileType getByName(String name) { for (FileType fileType : FileType.values()) { if (fileType.getName().equals(name)) { return fileType; } } return null; } public static FileType getByContentType(String contentType) { for (FileType fileType : FileType.values()) { if (fileType.getContentType().equals(contentType)) { return fileType; } } return null; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/GotoLineDialog.java ================================================ /* * This file is part of trolCommander, http://www.mucommander.com * Copyright (C) 2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import ru.trolsoft.ui.InputField; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.function.IntConsumer; /** * This dialog allows the user to enter a line number to be jumped for in the text editor. * * @author Oleg Trifonov */ public class GotoLineDialog extends FocusDialog implements ActionListener { /** The text field where a search string can be entered */ private InputField edtLineNumber; /** The 'OK' button */ private final JButton btnOk; private final JButton btnCancel; private final IntConsumer action; /** * Creates a new FindDialog and shows it to the screen. * * @param editorFrame the parent editor frame */ GotoLineDialog(JFrame editorFrame, int maxLines, IntConsumer action) { super(editorFrame, i18n("text_viewer.goto_line"), editorFrame); this.action = action; Container contentPane = getContentPane(); contentPane.add(new JLabel(i18n("text_viewer.line")+":"), BorderLayout.NORTH); edtLineNumber = new InputField(16, InputField.FilterType.DEC_LONG) { @Override public void onChange() { boolean enabled = !edtLineNumber.isEmpty() && edtLineNumber.getValue() <= maxLines; btnOk.setEnabled(enabled); } }; edtLineNumber.setText("1"); edtLineNumber.addActionListener(this); contentPane.add(edtLineNumber, BorderLayout.CENTER); btnOk = new JButton(i18n("ok")); btnCancel = new JButton(i18n("cancel")); contentPane.add(DialogToolkit.createOKCancelPanel(btnOk, btnCancel, getRootPane(), this), BorderLayout.SOUTH); // The text field will receive initial focus setInitialFocusComponent(edtLineNumber); fixHeight(); } /////////////////////////////////// // ActionListener implementation // /////////////////////////////////// public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if ( (source == btnOk || source == edtLineNumber) && btnOk.isEnabled() ) { int line = (int)edtLineNumber.getValue(); action.accept(line); dispose(); } else if (source == btnCancel) { cancel(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/StatusBar.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.ui.main.statusbar.FileWindowsListButton; import org.fife.ui.StatusBarPanel; import javax.swing.JLabel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridBagConstraints; /** * * @author Oleg Trifonov * Created on 25/09/14. */ public class StatusBar extends org.fife.ui.StatusBar { private StatusBarPanel panelColor; private StatusBarPanel panelPosition; private StatusBarPanel panelEncoding; private StatusBarPanel panelSyntax; private StatusBarPanel panelWindows; private JLabel lblColor; private JLabel lblPosition; private JLabel lblEncoding; private JLabel lblSyntax; private FileWindowsListButton lbFiles; private int color = -1; private String forcedStatusMessage; private long forcedStatusMessageTimeout; public StatusBar() { super(""); lbFiles = new FileWindowsListButton(true); panelWindows = new StatusBarPanel(new BorderLayout()); panelWindows.add(lbFiles); lblColor = new JLabel(" "); panelColor = new StatusBarPanel(new BorderLayout(), lblColor); lblPosition = new JLabel(); panelPosition = new StatusBarPanel(new BorderLayout(), lblPosition); lblSyntax = new JLabel(); panelSyntax = new StatusBarPanel(new BorderLayout(), lblSyntax); lblEncoding = new JLabel(); panelEncoding = new StatusBarPanel(new BorderLayout(), lblEncoding); // Make the layout such that different items can be different sizes. GridBagConstraints c = new GridBagConstraints(); c.fill = GridBagConstraints.BOTH; c.weightx = 0.0; addStatusBarComponent(panelWindows, c); addStatusBarComponent(panelPosition, c); addStatusBarComponent(panelColor, c); addStatusBarComponent(panelSyntax, c); addStatusBarComponent(panelEncoding, c); } public void setColor(int color) { if (this.color == color) { return; } this.color = color; panelColor.setVisible(color >= 0); panelColor.setBackground(new Color(color)); } void setPosition(int line, int column) { lblPosition.setText(line + " : " + column); } public void setEncoding(String encoding) { lblEncoding.setText(encoding); } public void setSyntax(String syntax) { lblSyntax.setText(syntax); } public void clearStatusMessage() { if (forcedStatusMessage != null) { if (System.currentTimeMillis() > forcedStatusMessageTimeout) { forcedStatusMessage = null; } } setStatusMessage(forcedStatusMessage != null ? forcedStatusMessage : ""); } public void showMessage(String msg, long timeInMillis) { forcedStatusMessage = msg; forcedStatusMessageTimeout = System.currentTimeMillis() + timeInMillis; setStatusMessage(msg); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextArea.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import org.fife.ui.rsyntaxtextarea.RSyntaxDocument; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.TokenMaker; import org.fife.ui.rtextarea.RTextAreaEditorKit; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import java.awt.*; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.Reader; /** * @author Oleg Trifonov * Created on 08/01/14. */ public class TextArea extends RSyntaxTextArea implements DocumentListener { private static final String DIRTY_PROPERTY = "TextEditorPane.dirty"; private boolean painted = false; /** * The #gotoLine(int) method can't be executed successfully if the model is not painted. * In this case the operation wll be postponed after calling #paint() method */ private int postponedCaretPosition = -1; /** * Whether the file is dirty. */ private boolean dirty; TextArea() { dirty = false; getDocument().addDocumentListener(this); } /** * * @param line line number (started from 1) * @param column cursor position in the line * @return true on success */ boolean gotoLine(int line, int column) { try { int pos = getLineStartOffset(line - 1) + column - 1; setCaretPosition(pos); Rectangle2D temp = modelToView2D(pos); if (temp == null) { postponedCaretPosition = pos; } else { forceCurrentLineHighlightRepaint(); } return true; } catch (IllegalArgumentException | BadLocationException e) { System.out.println("Invalid line: " + line + ":" + column + " (" + e.getMessage() + ")"); return false; } } boolean gotoLine(int line) { return gotoLine(line, 1); } /** * * @return current line number (started from 1) */ public int getLine() { int dot = getCaretPosition(); Element map = getDocument().getDefaultRootElement(); return map.getElementIndex(dot) + 1; } void setFileType(FileType fileType) { setSyntaxEditingStyle(fileType.getContentType()); } FileType getFileType() { return FileType.getByContentType(getSyntaxEditingStyle()); } /** * * @return current cursor position in line (started from 1) */ public int getColumn() { Element map = getDocument().getDefaultRootElement(); int dot = getCaretPosition(); int line = map.getElementIndex(dot); int lineStartOffset = map.getElement(line).getStartOffset(); return dot - lineStartOffset + 1; } @Override public void paint(Graphics g) { super.paint(g); // if gotoLine method executed before that model was painted then recall it if (postponedCaretPosition >= 0) { final int pos = postponedCaretPosition; postponedCaretPosition = -1; SwingUtilities.invokeLater(() -> { setCaretPosition(pos); forceCurrentLineHighlightRepaint(); }); } if (!painted) { painted = true; } } @Override public void read(Reader in, Object desc) throws IOException { super.read(in, desc); } @Override public void insertUpdate(DocumentEvent e) { if (!dirty) { setDirty(true); } } @Override public void removeUpdate(DocumentEvent e) { if (!dirty) { setDirty(true); } } @Override public void changedUpdate(DocumentEvent e) { } /** * Returns whether the text in this editor has unsaved changes. * * @return Whether the text has unsaved changes. * @see #setDirty(boolean) */ public boolean isDirty() { return dirty; } /** * Sets whether this text in this editor has unsaved changes. * This fires a property change event of type {@link #DIRTY_PROPERTY}.

    * * Applications will usually have no need to call this method directly; the * only time you might have a need to call this method directly is if you * have to initialize an instance of TextEditorPane with content that does * not come from a file. TextEditorPane automatically sets its * own dirty flag when its content is edited, when its encoding is changed, * or when its line ending property is changed. It is cleared whenever * load(), reload(), save(), or * saveAs() are called. * * @param dirty Whether the text has been modified. * @see #isDirty() */ public void setDirty(boolean dirty) { if (this.dirty != dirty) { this.dirty = dirty; firePropertyChange(DIRTY_PROPERTY, !dirty, dirty); } } /** * Sets the document for this editor. * * @param doc The new document. */ @Override public void setDocument(Document doc) { Document old = getDocument(); if (old != null) { old.removeDocumentListener(this); } try { super.setDocument(doc); } catch (Exception e) { e.printStackTrace(); // Sometime RSyntaxTextArea can crash for python files on code folding parsing setCodeFoldingEnabled(false); super.setDocument(doc); } doc.addDocumentListener(this); } /** * Sets the line separator sequence to use when this file is saved (e.g. * "\n", "\r\n" or "\r"). * Besides parameter checking, this method is preferred over * getDocument().putProperty() because can set the editor's * dirty flag when the line separator is changed. * * @param separator The new line separator. * @param setDirty Whether the dirty flag should be set if the line separator is changed. * @throws NullPointerException If separator is null. * @throws IllegalArgumentException If separator is not one of "\n", "\r\n" or "\r". * @see #getLineSeparator() */ public void setLineSeparator(String separator, boolean setDirty) { if (separator == null) { throw new NullPointerException("terminator cannot be null"); } if (!"\r\n".equals(separator) && !"\n".equals(separator) && !"\r".equals(separator)) { throw new IllegalArgumentException("Invalid line terminator"); } Document doc = getDocument(); Object old = doc.getProperty(RTextAreaEditorKit.EndOfLineStringProperty); if (!separator.equals(old)) { doc.putProperty(RTextAreaEditorKit.EndOfLineStringProperty, separator); if (setDirty) { setDirty(true); } } } /** * Returns the line separator used when writing this file (e.g. * "\n", "\r\n", or "\r").

    * * Note that this value is an Object and not a * String as that is the way the {@link Document} interface * defines its property values. If you always use * {@link #setLineSeparator(String)} to modify this value, then the value * returned from this method will always be a String. * * @return The line separator. If this value is null, then the system default line separator is used * (usually the value of System.getProperty("line.separator")). * @see #setLineSeparator(String) * @see #setLineSeparator(String, boolean) */ public Object getLineSeparator() { return getDocument().getProperty(RTextAreaEditorKit.EndOfLineStringProperty); } /** * Sets the line separator sequence to use when this file is saved (e.g. * "\n", "\r\n" or "\r"). * * Besides parameter checking, this method is preferred over * getDocument().putProperty() because it sets the editor's * dirty flag when the line separator is changed. * * @param separator The new line separator. * @throws NullPointerException If separator is null. * @throws IllegalArgumentException If separator is not one of "\n", "\r\n" or "\r". * @see #getLineSeparator() */ public void setLineSeparator(String separator) { setLineSeparator(separator, true); } String getLineStr(int line) { try { int posStart = getLineStartOffset(line - 1); int posEnd = getLineEndOffset(line - 1); int len = posEnd - posStart; if (len > 2048) { return null; } return getDocument().getText(posStart, len); } catch (Exception ignore) { return null; } } @Override public void setSyntaxEditingStyle(String styleKey) { if (!painted && (styleKey == null || SYNTAX_STYLE_NONE.equals(styleKey))) { return; } super.setSyntaxEditingStyle(styleKey); } @Override // Model that ignores syntax type changes and no mark file as "dirty" protected Document createDefaultModel() { return new RSyntaxDocument(SYNTAX_STYLE_NONE) { private boolean ignoreChangeUpdate; @Override public void setSyntaxStyle(String styleKey) { ignoreChangeUpdate = true; super.setSyntaxStyle(styleKey); ignoreChangeUpdate = false; } @Override protected void fireChangedUpdate(DocumentEvent e) { if (!ignoreChangeUpdate) { super.fireChangedUpdate(e); } else { SwingUtilities.invokeLater(() -> repaint()); } } }; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextEditor.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.*; import java.util.Arrays; import java.util.Stack; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.fife.ui.rtextarea.Gutter; import org.fife.ui.rtextarea.GutterEx; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.utils.text.Translator; import com.mucommander.ui.dialog.DialogOwner; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.encoding.EncodingListener; import com.mucommander.ui.encoding.EncodingMenu; import com.mucommander.ui.viewer.FileEditor; import com.mucommander.ui.viewer.FileFrame; /** * A simple text editor. * * @author Maxence Bernard, Nicolas Rinaudo, Arik Hadas */ public class TextEditor extends FileEditor implements DocumentListener, EncodingListener { private static final Logger LOGGER = LoggerFactory.getLogger(TextEditor.class); //private TextMenuHelper menuHelper; private final TextEditorImpl textEditorImpl; private final TextViewer textViewerDelegate; private StatusBar statusBar; private GutterEx gutter; TextEditor() { textEditorImpl = new TextEditorImpl(true, getStatusBar()); // Font defaultFont = new Font("Monospaced", Font.PLAIN, 12); initGutter(); //gutter.setActiveLineRangeColor(new Color(0,0,255)); setLineNumbersEnabled(TextViewer.isLineNumbers()); // Set miscellaneous properties. setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS); setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); textViewerDelegate = new TextViewer(textEditorImpl) { @Override public void setComponentToPresent(JComponent component) { TextEditor.this.setComponentToPresent(component); } @Override protected void showLineNumbers(boolean show) { setLineNumbersEnabled(show); TextViewer.setLineNumbers(show); //TextEditor.this.setRowHeaderView(show ? new TextLineNumbersPanel(textEditorImpl.getTextArea()) : null); } @Override protected void initMenuBarItems() { menuHelper = new TextMenuHelper(textEditorImpl, true); //menuHelper.initMenu(TextEditor.this, TextEditor.this.getRowHeader().getView() != null); menuHelper.initMenu(TextEditor.this, TextViewer.isLineNumbers()); //menuHelper.setupFileMenu(menuFile, TextEditor.this, getCurrentFile()); textEditorImpl.setMenuHelper(menuHelper); } }; //setComponentToPresent(textEditorImpl.getTextArea()); setComponentToPresent(textEditorImpl.getEditorComponent()); } private void initGutter() { gutter = new GutterEx(textEditorImpl.getTextArea()); gutter.setLineNumberFont(textEditorImpl.getTextArea().getFont()); // TODO gutter.setBackground(Color.LIGHT_GRAY); gutter.setForeground(Color.black); } @Override public void setComponentToPresent(JComponent component) { getViewport().add(component); } private void loadDocument(InputStream in, String encoding, DocumentListener documentListener) throws IOException { textViewerDelegate.loadDocument(in, encoding, documentListener); if (getStatusBar() != null) { getStatusBar().setEncoding(encoding); } } private void write(OutputStream out) throws IOException { //textEditorImpl.write(new BOMWriter(out, textViewerDelegate.getEncoding())); textEditorImpl.write(new OutputStreamWriter(out, textViewerDelegate.getEncoding())); } @Override public JMenuBar getMenuBar() { JMenuBar menuBar = super.getMenuBar(); // Encoding menu EncodingMenu encodingMenu = new EncodingMenu(new DialogOwner(getFrame()), textViewerDelegate.getEncoding()); encodingMenu.addEncodingListener(this); menuBar.add(textViewerDelegate.menuHelper.getEditMenu()); menuBar.add(textViewerDelegate.menuHelper.getSearchMenu()); menuBar.add(textViewerDelegate.menuHelper.getViewMenu()); menuBar.add(textViewerDelegate.menuHelper.getToolsMenu()); menuBar.add(encodingMenu); textEditorImpl.getTextArea().setFocusTraversalKeysEnabled(false); textViewerDelegate.setMainKeyListener(textEditorImpl.getTextArea(), menuBar); textViewerDelegate.menuHelper.setupFileMenu(menuFile, TextEditor.this, getCurrentFile()); return menuBar; } @Override public StatusBar getStatusBar() { if (statusBar == null) { statusBar = new StatusBar(); } return statusBar; } @Override protected void saveStateOnClose() { textViewerDelegate.saveState(getVerticalScrollBar()); if (getCurrentFile() != null) { // possible if loading was interrupted by Esc try { getCurrentFile().closePushbackInputStream(); } catch (IOException e) { LOGGER.error("IO Exception on save state", e); } } } @Override protected void restoreStateOnStartup() { final TextArea textArea = textEditorImpl.getTextArea(); final TextFilesHistory.FileRecord historyRecord = textViewerDelegate.getHistoryRecord(); if (historyRecord != null) { getViewport().setViewPosition(new java.awt.Point(0, historyRecord.getScrollPosition())); textArea.gotoLine(historyRecord.getLine(), historyRecord.getColumn()); } } @Override protected void saveAs(AbstractFile destFile) { // OutputStream out = null; getStatusBar().setStatusMessage(Translator.get("text_editor.writing")); //boolean error; try (OutputStream out = destFile.getOutputStream()) { write(out); } catch (Throwable e) { getStatusBar().setStatusMessage(Translator.get("text_editor.cant_save_file")); LOGGER.error("Exception on save", e); return; } // We get here only if the destination file was updated successfully // so we can set that no further save is needed at this stage setSaveNeeded(false); // Change the parent folder's date to now, so that changes are picked up by folder auto-refresh (see ticket #258) if (destFile.isFileOperationSupported(FileOperation.CHANGE_DATE)) { try { destFile.getParent().setLastModifiedDate(System.currentTimeMillis()); } catch (IOException e) { LOGGER.debug("failed to change the date of {}", destFile, e); // Fail silently } } getStatusBar().setStatusMessage(Translator.get("text_editor.saved")); } @Override public void setFrame(final FileFrame frame) { super.setFrame(frame); textEditorImpl.setFrame(frame); //frame.setFullScreen(TextViewer.isFullScreen()); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK), CUSTOM_FULL_SCREEN_EVENT); // getActionMap().put(CUSTOM_FULL_SCREEN_EVENT, new AbstractAction() { // public void actionPerformed(ActionEvent e){ // TextViewer.setFullScreen(!frame.isFullScreen()); // frame.setFullScreen(TextViewer.isFullScreen()); // } // }); } @Override public void show(AbstractFile file) { TextArea textArea = textEditorImpl.getTextArea(); textArea.discardAllEdits(); textViewerDelegate.menuHelper.updateEditActions(); // TODO SHOULD BE IN SEPARATE THREAD !!! TextFilesHistory.FileRecord historyRecord = textViewerDelegate.initHistoryRecord(file); FileType type = historyRecord.getFileType(); if (type == null) { type = FileType.getFileType(file); historyRecord.setFileType(type); } if (type == FileType.NONE) { type = TextEditorUtils.detectFileFormat(file); } textEditorImpl.prepareForEdit(file); textEditorImpl.setSyntaxType(type); textViewerDelegate.menuHelper.setSyntax(type); textViewerDelegate.startEditing(file, this); textArea.discardAllEdits(); } @Override public void changedUpdate(DocumentEvent e) { textViewerDelegate.menuHelper.updateEditActions(); // ignore change event if it was caused by syntax change // if (!textViewerDelegate.menuHelper.checkWaitChangeSyntaxEvent()) { setSaveNeeded(true); // } } @Override public void insertUpdate(DocumentEvent e) { setSaveNeeded(true); } @Override public void removeUpdate(DocumentEvent e) { setSaveNeeded(true); } @Override public void actionPerformed(ActionEvent e) { if (textViewerDelegate.menuHelper.performAction(e, textViewerDelegate)) { return; } super.actionPerformed(e); } @Override public void encodingChanged(Object source, String oldEncoding, String newEncoding) { if (!askSave()) { return; // Abort if the file could not be saved } // Store caret and scrollbar position before change TextArea textArea = textEditorImpl.getTextArea(); int line = textArea.getLine(); int column = textArea.getColumn(); int horizontalPos = getHorizontalScrollBar().getValue(); int verticalPos = getVerticalScrollBar().getValue(); try { // Reload the file using the new encoding // Note: loadDocument closes the InputStream loadDocument(getCurrentFile().getInputStream(), newEncoding, null); // Restore caret and scrollbar textArea.gotoLine(line, column); getViewport().setViewPosition(new java.awt.Point(horizontalPos, verticalPos)); setSaveNeeded(false); } catch (IOException ex) { InformationDialog.showErrorDialog(getFrame(), Translator.get("read_error"), Translator.get("file_editor.cannot_read_file", getCurrentFile().getName())); } } /** * Ensures the gutter is visible if it's showing anything. */ private void checkGutterVisibility() { int count = gutter.getComponentCount(); if (count == 0) { if (getRowHeader() != null && getRowHeader().getView() == gutter) { setRowHeaderView(null); } } else { if (getRowHeader() == null || getRowHeader().getView() == null) { setRowHeaderView(gutter); } } } /** * Returns the gutter. * * @return The gutter. */ public Gutter getGutter() { return gutter; } /** * Returns true if the line numbers are enabled and visible. * * @return Whether line numbers are visible. * @see #setLineNumbersEnabled(boolean) */ private boolean getLineNumbersEnabled() { return gutter.getLineNumbersEnabled(); } /** * Returns whether the fold indicator is enabled. * * @return Whether the fold indicator is enabled. * @see #setFoldIndicatorEnabled(boolean) */ public boolean isFoldIndicatorEnabled() { return gutter.isFoldIndicatorEnabled(); } /** * Returns whether the icon row header is enabled. * * @return Whether the icon row header is enabled. * @see #setIconRowHeaderEnabled(boolean) */ public boolean isIconRowHeaderEnabled() { return gutter.isIconRowHeaderEnabled(); } /** * Toggles whether the fold indicator is enabled. * * @param enabled Whether the fold indicator should be enabled. * @see #isFoldIndicatorEnabled() */ public void setFoldIndicatorEnabled(boolean enabled) { gutter.setFoldIndicatorEnabled(enabled); checkGutterVisibility(); } /** * Toggles whether the icon row header (used for breakpoints, bookmarks, * etc.) is enabled. * * @param enabled Whether the icon row header is enabled. * @see #isIconRowHeaderEnabled() */ public void setIconRowHeaderEnabled(boolean enabled) { gutter.setIconRowHeaderEnabled(enabled); checkGutterVisibility(); } /** * Toggles whether line numbers are visible. * * @param enabled Whether line numbers should be visible. * @see #getLineNumbersEnabled() */ public void setLineNumbersEnabled(boolean enabled) { gutter.setLineNumbersEnabled(enabled); checkGutterVisibility(); } /** * Sets the view for this scroll pane. This must be an {@link TextArea}. * * @param view The new view. */ @Override public void setViewportView(Component view) { TextArea rtaCandidate; if (!(view instanceof TextArea)) { rtaCandidate = getFirstRTextAreaDescendant(view); if (rtaCandidate == null) { throw new IllegalArgumentException("view must be either an RTextArea or a JLayer wrapping one"); } } else { rtaCandidate = (TextArea)view; } super.setViewportView(view); if (gutter != null) { gutter.setTextArea(rtaCandidate); } } /** * Returns the first descendant of a component that is an * RTextArea. This is primarily here to support * javax.swing.JLayers that wrap RTextAreas. * * @param comp The component to recursively look through. * @return The first descendant text area, or null if none * is found. */ private static TextArea getFirstRTextAreaDescendant(Component comp) { Stack stack = new Stack<>(); stack.add(comp); while (!stack.isEmpty()) { Component current = stack.pop(); if (current instanceof TextArea) { return (TextArea)current; } if (current instanceof Container container) { stack.addAll(Arrays.asList(container.getComponents())); } } return null; } @Override public void setSearchedText(String searchedText) { textEditorImpl.setupSearchContext(searchedText); } @Override public void setSearchedBytes(byte[] searchedBytes) { try { textEditorImpl.setupSearchContext(new String(searchedBytes, textViewerDelegate.getEncoding())); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } public TextArea getTextArea() { return textEditorImpl.getTextArea(); } public void gotoLine(int line, int colump) { getTextArea().gotoLine(line, colump); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextEditorCaretListener.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.commons.file.AbstractFile; import com.mucommander.tools.AvrAssemblerCommandsHelper; import com.mucommander.utils.text.Translator; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import java.io.IOException; import java.util.StringTokenizer; public class TextEditorCaretListener implements CaretListener { private final TextEditorImpl textEditor; TextEditorCaretListener(TextEditorImpl textEditor) { this.textEditor = textEditor; } @Override public void caretUpdate(CaretEvent e) { if (textEditor.replaceDialogMode) { return; } StatusBar statusBar = textEditor.getStatusBar(); TextArea textArea = textEditor.getTextArea(); if (statusBar == null) { return; } int line = textArea.getLine(); int col = textArea.getColumn(); statusBar.setPosition(line, col); statusBar.clearStatusMessage(); // check if we have 6-digit hex-word on cursor (color) String str = textArea.getLineStr(line); if (str == null || str.isEmpty()) { statusBar.setColor(-1); textEditor.selectIncludeFile(null); return; } checkAssemblerInstruction(str); checkColorOnCursor(str, col); checkIncludeInstruction(str, col); } private void checkIncludeInstruction(String str, int col) { if (!str.toLowerCase().contains("include")) { textEditor.selectIncludeFile(null); return; } if (col > 0) { col--; } try { int lastQuote2 = str.indexOf('"', col); int lastQuote1 = str.indexOf('\'', col); int lastBracket = str.indexOf(">", col); String quotedName = null; if (lastQuote2 > 0) { int firstQuote2 = str.lastIndexOf('"', col); if (firstQuote2 > 0) { quotedName = str.substring(firstQuote2 + 1, lastQuote2); if (quotedName.trim().isEmpty()) { quotedName = null; } } } if (quotedName == null && lastBracket > 0) { int firstBracket = str.lastIndexOf('<', col); if (firstBracket > 0) { quotedName = str.substring(firstBracket + 1, lastBracket); if (quotedName.trim().isEmpty()) { quotedName = null; } } } if (quotedName == null && lastQuote1 > 0) { int firstQuote1 = str.lastIndexOf('\'', col); if (firstQuote1 > 0) { quotedName = str.substring(firstQuote1 + 1, lastBracket); if (quotedName.trim().isEmpty()) { quotedName = null; } } } AbstractFile includeFile = getIncludeFile(quotedName); if (includeFile != null) { setStatusMessage("" + Translator.get("text_editor.press_alt_enter_to_open_file") + " " + quotedName + ""); textEditor.selectIncludeFile(includeFile); return; } } catch (StringIndexOutOfBoundsException ignore) {} setStatusMessage(null); } private void checkAssemblerInstruction(String str) { if (!isAvrAssembler()) { return; } StringTokenizer tokenizer = new StringTokenizer(str, " \t\n\r"); boolean found = false; while (tokenizer.hasMoreElements()) { String instruction = tokenizer.nextToken(); if (instruction.endsWith(":") || instruction.startsWith(";") || instruction.startsWith("//")) { continue; } String description = AvrAssemblerCommandsHelper.getCommandDescription(instruction); if (description != null) { setStatusMessage(description); found = true; break; } } if (!found) { setStatusMessage(""); } } private void checkColorOnCursor(String str, int col) { if (str.length() < 6 || col >= str.length()) { clearStatusColor(); return; } char ch = str.charAt(col); if (isHexDigit(ch)) { String word = "" + ch; for (int pos = col-1; pos >= 0; pos--) { char c = str.charAt(pos); if (isHexDigit(c)) { word = c + word; } else { break; } } for (int pos = col+1; pos < str.length(); pos++) { char c = str.charAt(pos); if (isHexDigit(c)) { word = word + c; } else { break; } } if (word.length() == 8) { word = word.substring(2); } if (word.length() == 6) { try { setStatusColor(Integer.parseInt(word, 16)); return; } catch (Exception ignore) { } } } clearStatusColor(); } private static boolean isHexDigit(char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); } private void setStatusMessage(String msg) { StatusBar statusBar = textEditor.getStatusBar(); if (statusBar != null) { statusBar.setStatusMessage(msg); } } private void setStatusColor(int color) { StatusBar statusBar = textEditor.getStatusBar(); if (statusBar != null) { statusBar.setColor(color); } } private void clearStatusColor() { setStatusColor(-1); } private boolean isAvrAssembler() { return textEditor.getTextArea().getFileType() == FileType.ASSEMBLER_AVR; } private AbstractFile getIncludeFile(String fileName) { if (fileName == null || fileName.isEmpty()) { return null; } try { AbstractFile selectedFile = textEditor.getFile().getParent().getChild(fileName); if (selectedFile != null && selectedFile.exists()) { return selectedFile; } } catch (IOException ignore) {} return null; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextEditorImpl.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.cache.TextHistory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.EditAction; import com.mucommander.ui.action.impl.ViewAction; import com.mucommander.ui.main.quicklist.ViewedAndEditedFilesQL; import com.mucommander.ui.theme.*; import com.mucommander.ui.viewer.EditorRegistrar; import com.mucommander.ui.viewer.FileFrame; import com.mucommander.ui.viewer.FileFrameCreateListener; import com.mucommander.ui.viewer.ViewerRegistrar; import com.mucommander.ui.viewer.text.search.FindDialog; import com.mucommander.ui.viewer.text.search.ReplaceDialog; import com.mucommander.ui.viewer.text.search.SearchEvent; import com.mucommander.ui.viewer.text.search.SearchListener; import com.mucommander.ui.viewer.text.tools.ExecPanel; import com.mucommander.ui.viewer.text.tools.ExecUtils; import com.mucommander.ui.viewer.text.tools.ProcessParams; import com.mucommander.utils.text.Translator; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.fife.ui.rtextarea.SearchContext; import org.fife.ui.rtextarea.SearchEngine; import org.fife.ui.rtextarea.SearchResult; import javax.swing.*; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultEditorKit; import javax.swing.text.Document; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.BufferedWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.util.LinkedList; /** * Text editor implementation used by {@link TextViewer} and {@link TextEditor}. * * @author Maxence Bernard, Mariusz Jakubowski, Nicolas Rinaudo, Arik Hadas, Oleg Trifonov */ @Slf4j class TextEditorImpl implements ThemeListener, ThemeId { private static final Insets INSETS = new Insets(4, 3, 4, 3); private static String lastLoadedThemeName; private static EditorTheme editorTheme; @Setter FileFrame frame; private final TextArea textArea; private SearchContext searchContext; /** Indicates whether there is a line separator in the original file */ private boolean lineSeparatorExists; @Getter private StatusBar statusBar; /** * Full path to editable file with slash at end */ private AbstractFile file; /** * If not null then contains included file under cursor that will ber opened by Ctrl(Cmd)+Enter hotkey */ private AbstractFile selectedIncludeFile; private JSplitPane splitPane; private ExecPanel pnlBuild; private int storedSplitDividerSize; private int storedSplitterPos; private ProcessParams buildParams; boolean replaceDialogMode = false; private final KeyListener textAreaKeyListener = new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiersEx() == KeyEvent.ALT_DOWN_MASK) { if (selectedIncludeFile != null) { openOtherFile(selectedIncludeFile); } return; } int mask = OsFamily.MAC_OS_X.isCurrent() ? KeyEvent.ALT_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; if (textArea.isEditable() && e.getKeyChar() == KeyEvent.VK_TAB && e.getModifiersEx() == mask) { ViewedAndEditedFilesQL viewedAndEditedFilesQL = new ViewedAndEditedFilesQL(frame, frame.getFilePresenter().getCurrentFile()); viewedAndEditedFilesQL.show(); e.consume(); } } }; private TextMenuHelper menuHelper; TextEditorImpl(boolean isEditable, StatusBar statusBar) { // Initialize text area this.textArea = createTextArea(isEditable); this.statusBar = statusBar; // Listen to theme changes to update the text area if it is visible ThemeManager.addCurrentThemeListener(this); } private TextArea createTextArea(boolean isEditable) { TextArea textArea = new TextArea() { @Override public Insets getInsets() { return INSETS; } }; textArea.setCurrentLineHighlightColor(ThemeManager.getCurrentColor(EDITOR_CURRENT_BACKGROUND_COLOR)); textArea.setAntiAliasingEnabled(true); textArea.setEditable(isEditable); textArea.addKeyListener(textAreaKeyListener); if (lastLoadedThemeName == null || !lastLoadedThemeName.equals(ThemeManager.getCurrentSyntaxThemeName())) { try { editorTheme = ThemeManager.readEditorTheme(ThemeManager.getCurrentSyntaxThemeName()); lastLoadedThemeName = ThemeManager.getCurrentSyntaxThemeName(); } catch (Exception e) { log.error("Can't load editor theme", e); } } editorTheme.apply(textArea); // Use theme colors and font textArea.setForeground(ThemeManager.getCurrentColor(EDITOR_FOREGROUND_COLOR)); textArea.setCaretColor(ThemeManager.getCurrentColor(EDITOR_FOREGROUND_COLOR)); Color background = ThemeManager.getCurrentColor(EDITOR_BACKGROUND_COLOR); textArea.setBackground(background); for (int i = 1; i <= textArea.getSecondaryLanguageCount(); i++) { textArea.setSecondaryLanguageBackground(i, background); } textArea.setSelectedTextColor(ThemeManager.getCurrentColor(EDITOR_SELECTED_FOREGROUND_COLOR)); textArea.setSelectionColor(ThemeManager.getCurrentColor(EDITOR_SELECTED_BACKGROUND_COLOR)); textArea.setFont(ThemeManager.getCurrentFont(EDITOR_FONT)); textArea.setCodeFoldingEnabled(true); textArea.setWrapStyleWord(true); textArea.addMouseWheelListener(e -> { boolean isCtrlPressed = (e.getModifiersEx() & KeyEvent.CTRL_DOWN_MASK) != 0; if (isCtrlPressed) { Font currentFont = textArea.getFont(); int currentFontSize = currentFont.getSize(); boolean rotationUp = e.getWheelRotation() < 0; if (rotationUp || currentFontSize > 1) { Font newFont = new Font(currentFont.getName(), currentFont.getStyle(), currentFontSize + (rotationUp ? 1 : -1)); textArea.setFont(newFont); } } else { textArea.getParent().dispatchEvent(e); } }); textArea.addCaretListener(new TextEditorCaretListener(this)); return textArea; } void find() { SearchListener searchListener = new SearchListener() { @Override public void searchEvent(SearchEvent e) { searchContext = e.getSearchContext(); String searchString = searchContext.getSearchFor(); TextHistory.getInstance().add(TextHistory.Type.TEXT_SEARCH, searchString, true); SearchResult result = SearchEngine.find(textArea, searchContext); if (!result.wasFound()) { beep(); setStatusMessage(Translator.get("text_editor.text_not_found")); } else { setStatusMessage(Translator.get("text_editor.found") + " " + result.getMarkedCount() + " " + Translator.get("text_editor.matches")); textArea.setCaretPosition(textArea.getSelectionStart()); } // Request the focus on the text area which could be lost after the Find dialog was disposed textArea.requestFocus(); } @Override public String getSelectedText() { return textArea.getSelectedText(); } }; FindDialog dlg = new FindDialog(frame, searchListener); dlg.setSearchString(searchContext != null ? searchContext.getSearchFor() : ""); dlg.showDialog(); } private void findMore(boolean forward) { if (searchContext == null) { beep(); return; } if (!forward && textArea.getCaretPosition() == 0) { beep(); return; } searchContext.setSearchForward(forward); int savedCaretPosition = textArea.getCaretPosition(); try { int pos = textArea.getSelectionStart(); if (forward) { pos += searchContext.getSearchFor().length(); } textArea.setCaretPosition(pos); } catch (IllegalArgumentException ignore) {} SearchResult result = SearchEngine.find(textArea, searchContext); if (!result.wasFound()) { textArea.setCaretPosition(savedCaretPosition); setStatusMessage(Translator.get("text_editor.text_not_found")); beep(); } else { setStatusMessage(""); textArea.setCaretPosition(textArea.getSelectionStart()); } } void findNext() { if (searchContext != null) { findMore(true); return; } String last = FindDialog.getLastSearchStr(); if (last != null) { setupSearchContext(FindDialog.getLastSearchStr()); } find(); } void replace() { SearchListener searchListener = new SearchListener() { @Override public void searchEvent(SearchEvent e) { searchContext = e.getSearchContext(); String searchString = searchContext.getSearchFor(); TextHistory.getInstance().add(TextHistory.Type.TEXT_SEARCH, searchString, true); //frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); search(e); } private void search(SearchEvent e) { SearchResult result; replaceDialogMode = true; result = switch (e.getType()) { case FIND -> SearchEngine.find(textArea, searchContext); case REPLACE -> SearchEngine.replace(textArea, searchContext); case REPLACE_ALL -> SearchEngine.replaceAll(textArea, searchContext); default -> null; }; replaceDialogMode = false; if (result == null) { return; } if (!result.wasFound()) { beep(); setStatusMessage(Translator.get("text_editor.text_not_found")); } else { if (e.getType() == SearchEvent.Type.REPLACE_ALL) { setStatusMessage(Translator.get("text_editor.replaced") + " " + result.getCount() + " " + Translator.get("text_editor.occurrences")); } else { setStatusMessage(Translator.get("text_editor.found") + " " + result.getMarkedCount() + " " + Translator.get("text_editor.matches")); } } } @Override public String getSelectedText() { return textArea.getSelectedText(); } }; ReplaceDialog dlg = new ReplaceDialog(frame, searchListener); dlg.setSearchString(searchContext != null ? searchContext.getSearchFor() : ""); dlg.showDialog(); } void findPrevious() { if (searchContext == null) { String last = FindDialog.getLastSearchStr(); if (last != null) { setupSearchContext(FindDialog.getLastSearchStr()); } find(); } else { findMore(false); } } void gotoLine() { new GotoLineDialog(frame, textArea.getLineCount(), textArea::gotoLine).showDialog(); } public boolean isWrap() { return textArea.getLineWrap(); } void wrap(boolean isWrap) { textArea.setLineWrap(isWrap); textArea.repaint(); } void copy() { textArea.copy(); } void cut() { textArea.cut(); } void paste() { textArea.paste(); } void selectAll() { textArea.selectAll(); } void undo() { textArea.undoLastAction(); } void redo() { textArea.redoLastAction(); } void requestFocus() { textArea.requestFocus(); } TextArea getTextArea() { return textArea; } JComponent getEditorComponent() { // if (splitPane == null) { // splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); // splitPane.add(textArea); // } // return splitPane; return textArea; } void addDocumentListener(DocumentListener documentListener) { textArea.getDocument().addDocumentListener(documentListener); } void read(Reader reader) throws IOException { // Feed the file's contents to text area textArea.read(reader, null); // If there are more than one lines, there is a line separator lineSeparatorExists = textArea.getLineCount() > 1; // Move cursor to the top // textArea.setCaretPosition(0); } void write(Writer writer) throws IOException { Document document = textArea.getDocument(); // According to the documentation in DefaultEditorKit, the line separator is set to be as the system property // if no other line separator exists in the file, but in practice it is not, so this is a workaround for it if (!lineSeparatorExists) document.putProperty(DefaultEditorKit.EndOfLineStringProperty, System.lineSeparator()); try { textArea.getUI().getEditorKit(textArea).write(new BufferedWriter(writer), document, 0, document.getLength()); } catch(BadLocationException e) { throw new IOException(e.getMessage()); } } /** * Receives theme color changes notifications. */ @Override public void colorChanged(ColorChangedEvent event) { switch (event.getColorId()) { case EDITOR_FOREGROUND_COLOR: textArea.setForeground(event.getColor()); break; case EDITOR_BACKGROUND_COLOR: textArea.setBackground(event.getColor()); break; case EDITOR_SELECTED_FOREGROUND_COLOR: textArea.setSelectedTextColor(event.getColor()); break; case EDITOR_SELECTED_BACKGROUND_COLOR: textArea.setSelectionColor(event.getColor()); break; case EDITOR_CURRENT_BACKGROUND_COLOR: textArea.setCurrentLineHighlightColor(event.getColor()); break; } } /** * Receives theme font changes notifications. */ public void fontChanged(FontChangedEvent event) { if (event.getFontId() == EDITOR_FONT) { textArea.setFont(event.getFont()); } } void build() { if (buildParams == null) { return; } showBuildPanel(); ProcessParams params = ExecUtils.getBuilderParams(getFile()); if (params != null) { pnlBuild.runCommand(params.folder, params.command); } } private void showBuildPanel() { if (splitPane == null) { splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); splitPane.add(frame.getFilePresenter()); splitPane.setDividerLocation(frame.getHeight()*2/3); splitPane.setOneTouchExpandable(true); pnlBuild = new ExecPanel(this::closeBuildPanel, this::gotoBuildError); splitPane.add(pnlBuild); frame.getContentPane().remove(frame.getFilePresenter()); frame.getContentPane().add(splitPane, BorderLayout.CENTER); frame.getContentPane().doLayout(); } if (splitPane.getDividerSize() <= 0) { splitPane.setDividerSize(storedSplitDividerSize); } if (storedSplitterPos > 0) { splitPane.setDividerLocation(storedSplitterPos); } pnlBuild.setVisible(true); splitPane.doLayout(); pnlBuild.doLayout(); } private void closeBuildPanel() { if (splitPane == null || pnlBuild == null) { return; } if (splitPane.getDividerSize() > 0) { storedSplitDividerSize = splitPane.getDividerSize(); storedSplitterPos = splitPane.getDividerLocation(); } if (pnlBuild != null) { pnlBuild.setVisible(false); } splitPane.setDividerSize(0); } private void gotoBuildError(AbstractFile file, int line, int column) { if (this.file.getAbsolutePath().equals(file.getAbsolutePath())) { textArea.gotoLine(line, column); } else { TextFilesHistory.FileRecord historyRecord = TextFilesHistory.getInstance().get(file); historyRecord.update(line, line, Math.max(column, 0), historyRecord.getFileType(), historyRecord.getEncoding()); openOtherFile(file, fileFrame -> { fileFrame.returnFocusTo(fileFrame); TextArea newTextArea = ((TextEditor)fileFrame.getFilePresenter()).getTextArea(); SwingUtilities.invokeLater(() -> newTextArea.gotoLine(line, column)); }); } } void setStatusBar(StatusBar statusBar) { this.statusBar = statusBar; } void setSyntaxType(FileType fileType) { textArea.setFileType(fileType); if (statusBar != null) { statusBar.setSyntax(fileType.getName()); } } void setStatusMessage(String message) { if (getStatusBar() != null) { getStatusBar().setStatusMessage(message); } } private static void beep() { // The beep method is called from a separate thread because this method seems to lock until the beep has // been played entirely. If the 'Find next' shortcut is left pressed, a series of beeps will be played when // the end of the file is reached, and we don't want those beeps to played one after the other as to: // 1/ not lock the event thread // 2/ have those beeps to end rather sooner than later new Thread(() -> Toolkit.getDefaultToolkit().beep()).start(); } void setupSearchContext(String searchStr) { searchContext = new SearchContext(searchStr); } void prepareForEdit(AbstractFile file) { this.file = file; this.buildParams = ExecUtils.getBuilderParams(file); if (menuHelper != null) { menuHelper.setBuildable(buildParams != null); } } void prepareForView(AbstractFile file) { this.file = file; } void selectIncludeFile(AbstractFile file) { this.selectedIncludeFile = file; } AbstractFile getFile() { return file; } void setMenuHelper(TextMenuHelper menuHelper) { this.menuHelper = menuHelper; } void addCurrentFileToBookmarks() { AbstractFile currentFile = getFile(); getBookmarkFilesList().addLast(currentFile.getURL().toString()); TextHistory.getInstance().save(TextHistory.Type.EDITOR_BOOKMARKS); } void removeCurrentFileFromBookmarks() { AbstractFile currentFile = getFile(); getBookmarkFilesList().remove(currentFile.getURL().toString()); TextHistory.getInstance().save(TextHistory.Type.EDITOR_BOOKMARKS); } void showFilesQuickList() { AbstractFile currentFile = getFile(); ViewedAndEditedFilesQL viewedAndEditedFilesQL = new ViewedAndEditedFilesQL(frame, currentFile); viewedAndEditedFilesQL.show(); } private static LinkedList getBookmarkFilesList() { return TextHistory.getInstance().getList(TextHistory.Type.EDITOR_BOOKMARKS); } private static AbstractFile getFileWithSameNameAndExt(AbstractFile file, String ...extensions) { String fileName = file.getNameWithoutExtension(); for (String ext : extensions) { try { AbstractFile selectedFile = file.getParent().getChild(fileName + ext); if (selectedFile != null && selectedFile.exists()) { return selectedFile; } } catch (IOException ignore) {} } return null; } void switchBetweenHeaderAndSource() { AbstractFile currentFile = getFile(); String ext = file.getExtension().toLowerCase(); boolean isSource = "c".equals(ext) || "cpp".equals(ext) || "cc".equals(ext); boolean isHeader = "h".equals(ext) || "hpp".equals(ext); AbstractFile fileToOpen; if (isSource) { fileToOpen = getFileWithSameNameAndExt(currentFile, ".h", ".hpp"); } else if (isHeader) { fileToOpen = getFileWithSameNameAndExt(currentFile, ".c", ".cpp", ".cc"); } else { return; } if (fileToOpen != null) { openOtherFile(fileToOpen); } } private void openOtherFile(AbstractFile path, FileFrameCreateListener createListener) { if (textArea.isEditable()) { EditorRegistrar.createEditorFrame(frame.getMainFrame(), path, ActionProperties.getActionIcon(EditAction.Descriptor.ACTION_ID).getImage(), createListener); } else { ViewerRegistrar.createViewerFrame(frame.getMainFrame(), path, ActionProperties.getActionIcon(ViewAction.Descriptor.ACTION_ID).getImage(), createListener); } } private void openOtherFile(AbstractFile path) { openOtherFile(path, null); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextEditorUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.StreamUtils; import com.mucommander.commons.io.bom.BOMInputStream; import com.mucommander.ui.viewer.text.utils.CodeFormatException; import com.mucommander.ui.viewer.text.utils.CodeFormatter; import com.mucommander.utils.text.Translator; import lombok.extern.slf4j.Slf4j; import java.io.IOException; import java.io.PushbackInputStream; @Slf4j class TextEditorUtils { static FileType detectFileFormat(AbstractFile file) { byte[] bytes = BufferPool.getByteArray(256); int readBytes; try { PushbackInputStream is = file.getPushBackInputStream(256); BOMInputStream bomIs = new BOMInputStream(is); readBytes = StreamUtils.readUpTo(bomIs, bytes); is.unread(bytes, 0, readBytes); } catch (IOException e) { BufferPool.releaseByteArray(bytes); log.error("detectFileFormat read error", e); try { file.closePushbackInputStream(); } catch (IOException e1) { log.error("detectFileFormat close error", e); } return FileType.NONE; } String str = new String(bytes, 0, readBytes).trim().toLowerCase(); BufferPool.releaseByteArray(bytes); if (readBytes < 5) { return FileType.NONE; } if (str.startsWith(" CodeFormatter.formatXml(src); case JSON -> CodeFormatter.formatJson(src); default -> null; }; if (formatted != null && !formatted.equals(src)) { textArea.setText(formatted); } } static void formatCode(TextEditorImpl textEditor) { try { formatTextArea(textEditor.getTextArea()); textEditor.setStatusMessage(""); } catch (CodeFormatException e) { if (e.getLine() > 0 ) { if (e.getRow() > 0) { textEditor.getTextArea().gotoLine(e.getLine(), e.getRow()); } else { textEditor.getTextArea().gotoLine(e.getLine()); } } textEditor.setStatusMessage(e.getLocalizedMessage()); } catch (Exception e) { textEditor.setStatusMessage(Translator.get("error")); } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextFactory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.io.BinaryDetector; import com.mucommander.commons.io.EncodingDetector; import com.mucommander.utils.text.Translator; import com.mucommander.ui.viewer.*; import java.io.IOException; import java.io.PushbackInputStream; /** * ViewerFactory and EditorFactory implementation for creating text viewers and editors. * * @author Nicolas Rinaudo */ public class TextFactory implements ViewerFactory, EditorFactory { private static final long FILE_SIZE_WARNING_THRESHOLD = 10*1024*1024; public boolean canViewFile(AbstractFile file) throws WarnUserException { return doGenericChecks(file); } public boolean canEditFile(AbstractFile file) throws WarnUserException { return doGenericChecks(file); } public FileViewer createFileViewer() { return new TextViewer(); } @Override public String getName() { return Translator.get("viewer_type.text"); } public FileEditor createFileEditor() { return new TextEditor(); } private boolean doGenericChecks(AbstractFile file) throws WarnUserException { // Do not allow directories if (file.isDirectory()) { return false; } // Warn the user if the file looks like a binary file if (checkBinaryFile(file)) { return false; } // Warn the user if the file is large that a certain size as the whole file is loaded into memory // (in a JTextArea) if (file.getSize() > FILE_SIZE_WARNING_THRESHOLD) { throw new WarnUserException(Translator.get("file_viewer.large_file_warning")); } return true; } private boolean checkBinaryFile(AbstractFile file) { try { PushbackInputStream is = file.getPushBackInputStream(EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE); if (BinaryDetector.guessBinary(is)) { return true; } } catch (IOException e) { e.printStackTrace(); try { file.closePushbackInputStream(); } catch (IOException e1) { e1.printStackTrace(); } } return false; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextFilesHistory.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2013-2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.PlatformManager; import com.mucommander.commons.DummyDecoratedFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileURL; import java.io.*; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.List; /** * Stores history for viewed and edited files - last position */ public class TextFilesHistory { /** Default text file history file name */ private static final String DEFAULT_HISTORY_FILE_NAME = "textfiles.history"; private static final int MAX_NUMBER_OF_RECORDS = 1000; private static WeakReference instance; private final List records = new ArrayList<>(); public static class FileRecord { private final String fileName; private int scrollPosition; private int line, column; private FileType fileType; private String encoding; FileRecord(String fileName, int firstLine, int row, int column, FileType fileType, String encoding) { this.fileName = fileName; update(firstLine, row, column, fileType, encoding); } FileRecord(String fileName) { this.fileName = fileName; } public void update(int firstLine, int line, int column, FileType fileType, String encoding) { setScrollPosition(firstLine); setLine(line); setColumn(column); setFileType(fileType); setEncoding(encoding); } public void update(FileRecord source) { this.scrollPosition = source.scrollPosition; this.line = source.line; this.column = source.column; this.fileType = source.fileType; this.encoding = source.encoding; } public int getLine() { return line <= 0 ? 1 : line; } public void setLine(int line) { this.line = line; } public FileType getFileType() { return fileType; } public void setFileType(FileType fileType) { this.fileType = fileType; } public int getScrollPosition() { return scrollPosition; } public void setScrollPosition(int scrollPosition) { this.scrollPosition = scrollPosition; } public int getColumn() { return column > 0 ? column : 1; } public void setColumn(int column) { this.column = column; } public String getEncoding() { return encoding; } public void setEncoding(String encoding) { this.encoding = "null".equals(encoding) ? null : encoding; } @Override public String toString() { return fileName + '=' + scrollPosition + ',' + getLine() + ',' + getColumn() + ',' + getFileType() + ',' + getEncoding(); } } public static TextFilesHistory getInstance() { TextFilesHistory textFilesHistory = instance == null ? null : instance.get(); if (textFilesHistory == null) { textFilesHistory = new TextFilesHistory(); instance = new WeakReference<>(textFilesHistory); try { textFilesHistory.load(); } catch (IOException ignore) {} } return textFilesHistory; } // - History file access -------------------------------------------------- // ------------------------------------------------------------------------- /** * Returns the path to the history file. *

    * Will return the default, system dependant bookmarks file. * * @return the path to the bookmark file. * @throws IOException if there was a problem locating the default history file. */ private static synchronized AbstractFile getHistoryFile() throws IOException { return PlatformManager.getPreferencesFolder().getChild(DEFAULT_HISTORY_FILE_NAME); } private void load() throws IOException { load(getHistoryFile()); } private void load(AbstractFile file) { records.clear(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { if (line.isEmpty() || line.startsWith("#")) { continue; } FileRecord rec = parseRecord(line); if (rec != null) { records.add(rec); } } } catch (Exception e) { e.printStackTrace(); } } private static FileRecord parseRecord(String s) { int index = s.indexOf('='); if (index < 0) { return null; } String fileName = s.substring(0, index); String[] props = s.substring(index + 1).split(","); try { for (int i = 0; i < props.length; i++) { props[i] = props[i].trim(); } FileType type; try { type = FileType.valueOf(props[3]); } catch (Exception e) { type = null; } return new FileRecord(fileName, Integer.parseInt(props[0]), Integer.parseInt(props[1]), Integer.parseInt(props[2]), type, props[4]); } catch (Exception e) { e.printStackTrace(); return null; } } public void save() { try { save(getHistoryFile()); } catch (IOException e) { e.printStackTrace(); } } public void save(AbstractFile file) throws IOException { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(file.getOutputStream()))) { for (FileRecord rec : records) { writer.write(rec.toString()); writer.write('\n'); } } } public FileRecord get(AbstractFile file) { return get(file.getAbsolutePath()); } public FileRecord get(String fileName) { int index = findRecord(fileName); return index >= 0 ? records.get(index) : new FileRecord(fileName); } private int findRecord(String fileName) { for (int i = 0; i < records.size(); i++) { FileRecord rec = records.get(i); if (rec.fileName.equals(fileName)) { return i; } } return -1; } TextFilesHistory updateRecord(FileRecord record) { int index = findRecord(record.fileName); if (index >= 0) { records.remove(index); } records.addFirst(record); while (records.size() > MAX_NUMBER_OF_RECORDS) { records.removeLast(); } return this; } public List getLastList(int maxCount) { List result = new ArrayList<>(); for (FileRecord rec : records) { try { // result.add(FileFactory.getFile(rec.fileName)); // result.add(FileFactory.getFile(FileURL.getFileURL(rec.fileName))); FileURL fileUrl = FileURL.getFileURL(rec.fileName); result.add(new DummyDecoratedFile(fileUrl)); } catch (MalformedURLException e) { e.printStackTrace(); } if (result.size() >= maxCount) { break; } } return result; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextLineNumbersPanel.java ================================================ package com.mucommander.ui.viewer.text; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.Map; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Element; import javax.swing.text.JTextComponent; import javax.swing.text.StyleConstants; import javax.swing.text.Utilities; /** * Panel in which the line numbers at a given text component are presented. * it is used in JScrollPane as a row header. * * @author Arik Hadas */ public class TextLineNumbersPanel extends JPanel implements CaretListener, DocumentListener, PropertyChangeListener { private final static int HEIGHT = Integer.MAX_VALUE - 1000000; public enum ALIGNMENT{ LEFT, CENTER, RIGHT } // Text component this TextTextLineNumber component is in sync with private final JTextComponent component; // Properties that can be changed private Color currentLineForeground; private int minimumDisplayDigits; private double digitAlignment; // Keep history information to reduce the number of times the component needs to be repainted private int lastDigits; private int lastHeight; private int lastLine; private Map fonts; /** * create a line number component for a text component. This minimum * display width will be based on 3 digits. * * @param component the related text component */ public TextLineNumbersPanel(JTextComponent component) { this(component, 3); } /** * create a line number component for a text component. * * @param component the related text component * @param minimumDisplayDigits the number of digits used to calculate * the minimum width of the component */ public TextLineNumbersPanel(JTextComponent component, int minimumDisplayDigits) { this(component, minimumDisplayDigits, new EmptyBorder(0, 0, 0, 2), 4, ALIGNMENT.CENTER); } public TextLineNumbersPanel(JTextComponent component, int minimumDisplayDigits, Border border, int borderGap, ALIGNMENT alignment) { this.component = component; setBackground(Color.LIGHT_GRAY); setForeground(Color.black); setCurrentLineForeground(new Color(0,0,255)); setDigitAlignment(alignment); setBorder(border, borderGap); setMinimumDisplayDigits( minimumDisplayDigits); setFont(component.getFont()); component.getDocument().addDocumentListener(this); component.addPropertyChangeListener("font", this); component.addCaretListener(this); } /** * Set the alignment of the line numbers strings within the panel * * @param alignment the line numbers alignment */ private void setDigitAlignment(ALIGNMENT alignment) { switch(alignment) { case LEFT: digitAlignment = 0; case RIGHT: digitAlignment = 1; case CENTER: digitAlignment = 0.5; } } /** * If we'll want to highlight current line , we should * set the current line number color using this method * * @param currentLineForeground current line number color */ private void setCurrentLineForeground( Color currentLineForeground ) { this.currentLineForeground = currentLineForeground; } /** * The border gap is used in calculating the left and right insets of the * border. Default value is 5. * * @param borderGap the gap in pixels */ private void setBorder(Border border, int borderGap) { Border inner = new EmptyBorder(0, borderGap, 0, borderGap); setBorder( new CompoundBorder(border, inner) ); lastDigits = 0; setPreferredWidth(); } /** * Specify the minimum number of digits used to calculate the preferred * width of the component. Default is 3. * * @param minimumDisplayDigits the number digits used in the preferred * width calculation */ private void setMinimumDisplayDigits(int minimumDisplayDigits) { this.minimumDisplayDigits = minimumDisplayDigits; setPreferredWidth(); } /** * Calculate the width needed to display the maximum line number */ private void setPreferredWidth() { Element root = component.getDocument().getDefaultRootElement(); int lines = root.getElementCount(); int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits); // Update sizes when number of digits in the line number changes if (lastDigits != digits) { lastDigits = digits; FontMetrics fontMetrics = getFontMetrics( getFont() ); int width = fontMetrics.charWidth( '0' ) * digits; Insets insets = getInsets(); int preferredWidth = insets.left + insets.right + width; Dimension d = getPreferredSize(); d.setSize(preferredWidth, HEIGHT); setPreferredSize( d ); setSize( d ); } } /* * We need to know if the caret is currently positioned on the line we * are about to paint so the line number can be highlighted. * if the current line foreground is not set, just return false */ private boolean isCurrentLine(int rowStartOffset) { if (currentLineForeground == null) return false; int caretPosition = component.getCaretPosition(); Element root = component.getDocument().getDefaultRootElement(); return root.getElementIndex( rowStartOffset ) == root.getElementIndex(caretPosition); } /** * Draw the line numbers */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); // Determine the width of the space available to draw the line number FontMetrics fontMetrics = component.getFontMetrics( component.getFont() ); Insets insets = getInsets(); int availableWidth = getSize().width - insets.left - insets.right; // Determine the rows to draw within the clipped bounds. Rectangle clip = g.getClipBounds(); int rowStartOffset = component.viewToModel2D( new Point(0, clip.y) ); int endOffset = component.viewToModel2D( new Point(0, clip.y + clip.height) ); while (rowStartOffset <= endOffset) { try { g.setColor(isCurrentLine(rowStartOffset) ? currentLineForeground : getForeground()); // Get the line number as a string and then determine the // "X" and "Y" offsets for drawing the string. String lineNumber = getTextLineNumber(rowStartOffset); int stringWidth = fontMetrics.stringWidth( lineNumber ); int x = getOffsetX(availableWidth, stringWidth) + insets.left; int y = getOffsetY(rowStartOffset, fontMetrics); g.drawString(lineNumber, x, y); // Move to the next row rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1; } catch(Exception e) { e.printStackTrace(); } } } /* * Get the line number to be drawn. The empty string will be returned * when a line of text has wrapped. */ protected String getTextLineNumber(int rowStartOffset) { Element root = component.getDocument().getDefaultRootElement(); int index = root.getElementIndex( rowStartOffset ); Element line = root.getElement( index ); return line.getStartOffset() == rowStartOffset ? String.valueOf(index + 1) : ""; } /* * Determine the X offset to properly align the line number when drawn */ private int getOffsetX(int availableWidth, int stringWidth) { return (int)((availableWidth - stringWidth) * digitAlignment); } /* * Determine the Y offset for the current row */ private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics) throws BadLocationException { // Get the bounding rectangle of the row Rectangle2D r = component.modelToView2D( rowStartOffset ); int lineHeight = fontMetrics.getHeight(); double y = r.getY() + r.getHeight(); int descent = 0; // The text needs to be positioned above the bottom of the bounding // rectangle based on the descent of the font(s) contained on the row. if (Math.round(r.getHeight()) == lineHeight) { // default font is being used descent = fontMetrics.getDescent(); } else { // We need to check all the attributes for font changes if (fonts == null) fonts = new HashMap<>(); Element root = component.getDocument().getDefaultRootElement(); int index = root.getElementIndex( rowStartOffset ); Element line = root.getElement( index ); for (int i = 0; i < line.getElementCount(); i++) { Element child = line.getElement(i); AttributeSet as = child.getAttributes(); String fontFamily = (String)as.getAttribute(StyleConstants.FontFamily); Integer fontSize = (Integer)as.getAttribute(StyleConstants.FontSize); String key = fontFamily + fontSize; FontMetrics fm = fonts.get( key ); if (fm == null) { Font font = new Font(fontFamily, Font.PLAIN, fontSize); fm = component.getFontMetrics( font ); fonts.put(key, fm); } descent = Math.max(descent, fm.getDescent()); } } return (int)Math.round(y - descent); } @Override public void changedUpdate(DocumentEvent e) { documentChanged(); } @Override public void insertUpdate(DocumentEvent e) { documentChanged(); } @Override public void removeUpdate(DocumentEvent e) { documentChanged(); } /* * A document change may affect the number of displayed lines of text. * Therefore, the lines numbers will also change. */ private void documentChanged() { // Preferred size of the component has not been updated at the time // the DocumentEvent is fired SwingUtilities.invokeLater(() -> { int preferredHeight; try { preferredHeight = component.getPreferredSize().height; } catch (Exception e) { e.printStackTrace(); return; } // Document change has caused a change in the number of lines. // Repaint to reflect the new line numbers if (lastHeight != preferredHeight) { setPreferredWidth(); repaint(); lastHeight = preferredHeight; } }); } @Override public void caretUpdate(CaretEvent e) { if (currentLineForeground == null) return; // Get the line the caret is positioned on int caretPosition = component.getCaretPosition(); Element root = component.getDocument().getDefaultRootElement(); int currentLine = root.getElementIndex( caretPosition ); // Need to repaint so the correct line number can be highlighted if (lastLine != currentLine) { repaint(); lastLine = currentLine; } } @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() instanceof Font) { setFont((Font) evt.getNewValue()); lastDigits = 0; setPreferredWidth(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextMenuHelper.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.cache.TextHistory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.ui.action.impl.UserMenuAction; import com.mucommander.ui.helper.MenuToolkit; import com.mucommander.ui.helper.MnemonicHelper; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.main.menu.UserPopupMenu; import com.mucommander.utils.text.Translator; import org.intellij.lang.annotations.MagicConstant; import ru.trolsoft.calculator.CalculatorDialog; import ru.trolsoft.ui.TMenuSeparator; import ru.trolsoft.ui.TRadioButtonMenuItem; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.util.LinkedList; import static javax.swing.KeyStroke.getKeyStroke; /** * Helping class for menu creation in viewer and editor */ public class TextMenuHelper { private final TextEditorImpl textEditorImpl; private final boolean editMode; /** Menu bar */ // Menus private JMenu menuEdit; private JMenu menuView; private JMenu menuViewSyntax; private JMenu menuSearch; private JMenu menuTools; // Items private JMenuItem miFiles; private JMenuItem miMainFrame; private JMenuItem miAddToBookmarks; private JMenuItem miRemoveFromBookmarks; private JMenuItem miGotoHeaderSource; // private JMenuItem miClose; private JMenuItem miUndo; private JMenuItem miRedo; private JMenuItem miCopy; private JMenuItem miCut; private JMenuItem miPaste; private JMenuItem miSelectAll; private JMenuItem miFind; private JMenuItem miFindNext; private JMenuItem miFindPrevious; private JMenuItem miReplace; private JMenuItem miGotoLine; private JMenuItem miToggleLineWrap; private JMenuItem miToggleLineNumbers; private JMenuItem miToggleInvisibleChars; private JMenuItem miCalculator; private JMenuItem miBuild; private JMenuItem miUserMenu; private JMenuItem miFormat; private FileType fileType; private static boolean showInvisibleChars = false; TextMenuHelper(TextEditorImpl textEditorImpl, boolean editMode) { this.textEditorImpl = textEditorImpl; this.editMode = editMode; updateInvisibleChars(); } void initMenu(ActionListener actionListener, boolean lineNumbers) { // Edit menu menuEdit = new JMenu(Translator.get("text_editor.edit")); MnemonicHelper menuItemMnemonicHelper = new MnemonicHelper(); if (editMode) { miUndo = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.undo"), menuItemMnemonicHelper, null, actionListener); miRedo = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.redo"), menuItemMnemonicHelper, null, actionListener); menuEdit.addSeparator(); } miCopy = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.copy"), menuItemMnemonicHelper, null, actionListener); if (editMode) { miCut = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.cut"), menuItemMnemonicHelper, null, actionListener); miPaste = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.paste"), menuItemMnemonicHelper, null, actionListener); } miSelectAll = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.select_all"), menuItemMnemonicHelper, null, actionListener); menuEdit.addSeparator(); menuEdit.addSeparator(); if (editMode) { miFormat = MenuToolkit.addMenuItem(menuEdit, i18n("text_editor.format"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F, KeyEvent.SHIFT_DOWN_MASK|getCtrlOrMetaMask()), actionListener); } // Search menu menuSearch = new JMenu(Translator.get("text_editor.search")); miFind = MenuToolkit.addMenuItem(menuSearch, i18n("text_editor.find"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F, getCtrlOrMetaMask()), actionListener); miFindNext = MenuToolkit.addMenuItem(menuSearch, i18n("text_editor.find_next"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F3, 0), actionListener); miFindPrevious = MenuToolkit.addMenuItem(menuSearch, i18n("text_editor.find_previous"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F3, KeyEvent.SHIFT_DOWN_MASK), actionListener); if (editMode) { menuSearch.addSeparator(); miReplace = MenuToolkit.addMenuItem(menuSearch, i18n("text_editor.replace_menu"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F, getCtrlOrMetaMask()|KeyEvent.ALT_DOWN_MASK), actionListener); } menuSearch.addSeparator(); miGotoLine = MenuToolkit.addMenuItem(menuSearch, i18n("text_viewer.goto_line"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_G, getCtrlOrMetaMask()), actionListener); // View menu menuView = new JMenu(i18n("text_editor.view")); miToggleLineWrap = MenuToolkit.addCheckBoxMenuItem(menuView, i18n("text_editor.line_wrap"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F2, 0), actionListener); miToggleLineWrap.setSelected(textEditorImpl.isWrap()); miToggleLineNumbers = MenuToolkit.addCheckBoxMenuItem(menuView, i18n("text_editor.line_numbers"), menuItemMnemonicHelper, null, actionListener); miToggleLineNumbers.setSelected(lineNumbers); miToggleInvisibleChars = MenuToolkit.addCheckBoxMenuItem(menuView, i18n("text_editor.invisible_chars"), menuItemMnemonicHelper, null, actionListener); miToggleInvisibleChars.setSelected(showInvisibleChars); menuView.addSeparator(); menuViewSyntax = new JMenu(Translator.get("text_editor.syntax")); addSyntaxMenu(actionListener, menuItemMnemonicHelper); // Tools menu addToolsMenu(actionListener, menuItemMnemonicHelper); } void setupFileMenu(JMenu fileMenu, ActionListener actionListener, AbstractFile currentFile) { MnemonicHelper mnemonicHelper = new MnemonicHelper(); JMenuItem lastItem = fileMenu.getItemCount() > 0 ? fileMenu.getItem(fileMenu.getItemCount()-1) : null; int mask = OsFamily.MAC_OS_X.isCurrent() ? KeyEvent.ALT_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; miFiles = MenuToolkit.addMenuItem(fileMenu, i18n("file_editor.files"), mnemonicHelper, getKeyStroke(KeyEvent.VK_TAB, mask), actionListener); miMainFrame = MenuToolkit.addMenuItem(fileMenu, i18n("file_editor.show_file_manager"), mnemonicHelper, getKeyStroke(KeyEvent.VK_1, KeyEvent.CTRL_DOWN_MASK), actionListener); miAddToBookmarks = MenuToolkit.addMenuItem(fileMenu, i18n("file_editor.add_to_bookmark"), mnemonicHelper, null, actionListener); miRemoveFromBookmarks = MenuToolkit.addMenuItem(fileMenu, i18n("file_editor.remove_from_bookmark"), mnemonicHelper, null, actionListener); mask = getCtrlOrMetaMask() | KeyEvent.SHIFT_DOWN_MASK; miGotoHeaderSource = MenuToolkit.addMenuItem(fileMenu, i18n("file_editor.goto_header_source"), mnemonicHelper, getKeyStroke(KeyEvent.VK_A, mask), actionListener); fileMenu.add(new TMenuSeparator()); if (lastItem != null) { fileMenu.add(lastItem); } updateFileBookmarksMenuItems(currentFile); updateGotoHeaderSourceVisibility(); } private void updateFileBookmarksMenuItems(AbstractFile currentFile) { if (currentFile == null) { miAddToBookmarks.setVisible(false); miRemoveFromBookmarks.setVisible(false); return; } boolean inBookmarks = getBookmarkFilesList().contains(currentFile.getURL().toString()); miAddToBookmarks.setVisible(!inBookmarks); miRemoveFromBookmarks.setVisible(inBookmarks); } private void addToolsMenu(ActionListener actionListener, MnemonicHelper menuItemMnemonicHelper) { menuTools = new JMenu(Translator.get("text_editor.tools")); miCalculator = MenuToolkit.addMenuItem(menuTools, Translator.get("Calculator.label"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F6, 0), actionListener); miBuild = MenuToolkit.addMenuItem(menuTools, Translator.get("text_editor.build"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_B, KeyEvent.META_DOWN_MASK), actionListener); miUserMenu = MenuToolkit.addMenuItem(menuTools, Translator.get("UserMenu.label"), menuItemMnemonicHelper, getKeyStroke(KeyEvent.VK_F1, 0), actionListener); } private void addSyntaxMenu(ActionListener actionListener, MnemonicHelper menuItemMnemonicHelper) { menuView.add(menuViewSyntax); ButtonGroup group = new ButtonGroup(); for (FileType fileType : FileType.values()) { MenuToolkit.addRadioButtonMenuItem(menuViewSyntax, fileType.getName(), menuItemMnemonicHelper,null, actionListener, group); } } @MagicConstant(flags = {KeyEvent.META_DOWN_MASK, KeyEvent.CTRL_DOWN_MASK}) private int getCtrlOrMetaMask() { return OsFamily.MAC_OS_X.isCurrent() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; } JMenu getEditMenu() { return menuEdit; } JMenu getViewMenu() { return menuView; } JMenu getSearchMenu() { return menuSearch; } JMenu getToolsMenu() { return menuTools; } public boolean performAction(ActionEvent e, TextViewer textViewerDelegate) { Object source = e.getSource(); if (source == null) { return false; } if (checkSyntaxChangeAction(source)) { return true; } if (source == miFiles) { textEditorImpl.showFilesQuickList(); } else if (source == miMainFrame) { showMainFrame(); } else if (source == miAddToBookmarks) { textEditorImpl.addCurrentFileToBookmarks(); miAddToBookmarks.setVisible(false); miRemoveFromBookmarks.setVisible(true); } else if (source == miRemoveFromBookmarks) { textEditorImpl.removeCurrentFileFromBookmarks(); miAddToBookmarks.setVisible(true); miRemoveFromBookmarks.setVisible(false); } else if (source == miGotoHeaderSource) { textEditorImpl.switchBetweenHeaderAndSource(); // } else if (source == miClose) { // textEditorImpl.frame.dispose(); } else if (source == miCopy) { textEditorImpl.copy(); } else if (source == miCut) { textEditorImpl.cut(); } else if (source == miPaste) { textEditorImpl.paste(); } else if (source == miSelectAll) { textEditorImpl.selectAll(); } else if (source == miFind) { textEditorImpl.find(); } else if (source == miReplace) { textEditorImpl.replace(); } else if (source == miFindNext) { textEditorImpl.findNext(); } else if (source == miFindPrevious) { textEditorImpl.findPrevious(); } else if (source == miToggleLineWrap) { if (e.getWhen() == 0) { miToggleLineWrap.setSelected(!miToggleLineWrap.isSelected()); } textViewerDelegate.wrapLines(miToggleLineWrap.isSelected()); } else if (source == miToggleLineNumbers) { textViewerDelegate.showLineNumbers(miToggleLineNumbers.isSelected()); } else if (source == miToggleInvisibleChars) { showInvisibleChars = miToggleInvisibleChars.isSelected(); updateInvisibleChars(); } else if (source == miGotoLine) { textEditorImpl.gotoLine(); } else if (source == miUndo) { textEditorImpl.undo(); } else if (source == miRedo) { textEditorImpl.redo(); } else if (source == miFormat) { TextEditorUtils.formatCode(textEditorImpl); } else if (source == miCalculator) { new CalculatorDialog(textEditorImpl.frame).showDialog(); } else if (source == miBuild) { textEditorImpl.build(); } else if (source == miUserMenu) { UserPopupMenu menu = UserMenuAction.createMenu(getMainFrame()); if (menu != null) { menu.show(textEditorImpl.frame); } } else { return false; } updateEditActions(); return true; } private MainFrame getMainFrame() { return textEditorImpl.frame.getMainFrame(); } private void updateInvisibleChars() { textEditorImpl.getTextArea().setWhitespaceVisible(showInvisibleChars); } private boolean checkSyntaxChangeAction(Object source) { if (source instanceof TRadioButtonMenuItem) { for (int i = 0; i < menuViewSyntax.getItemCount(); i++) { JMenuItem item = menuViewSyntax.getItem(i); if (source == item) { FileType fileType = FileType.getByName(item.getText()); textEditorImpl.setSyntaxType(fileType); setSyntax(fileType); return true; } } } return false; } public void setSyntax(FileType fileType) { for (int i = 0; i < menuViewSyntax.getItemCount(); i++) { JMenuItem item = menuViewSyntax.getItem(i); item.setSelected(item.getText().equals(fileType.getName())); } updateEditActions(); this.fileType = fileType; updateGotoHeaderSourceVisibility(); } private void updateGotoHeaderSourceVisibility() { if (miGotoHeaderSource != null) { miGotoHeaderSource.setVisible(fileType == FileType.CPP || fileType == FileType.C); } } /* * Check if last editor change fired by syntax change event ant will be ignored in document listener * @return true if if last editor change fired by syntax change event ant will be ignored in document listener */ // public boolean checkWaitChangeSyntaxEvent() { // boolean result = waitChangeSyntaxEvent; // updateEditActions(); // waitChangeSyntaxEvent = false; // return result; // } void updateEditActions() { if (!editMode) { return; } final TextArea textArea = textEditorImpl.getTextArea(); miUndo.setEnabled(textArea.canUndo()); miRedo.setEnabled(textArea.canRedo()); FileType ft = textArea.getFileType(); miFormat.setVisible(ft == FileType.XML || ft == FileType.JSON); } void setBuildable(boolean canBuild) { miBuild.setEnabled(canBuild); } private static String i18n(String key, String... params) { return Translator.get(key, params); } private static LinkedList getBookmarkFilesList() { return TextHistory.getInstance().getList(TextHistory.Type.EDITOR_BOOKMARKS); } private void showMainFrame() { getMainFrame().toFront(); } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/TextViewer.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.io.EncodingDetector; import com.mucommander.commons.io.bom.BOMInputStream; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcSnapshot; import com.mucommander.ui.dialog.DialogOwner; import com.mucommander.ui.dialog.InformationDialog; import com.mucommander.ui.encoding.EncodingListener; import com.mucommander.ui.encoding.EncodingMenu; import com.mucommander.ui.viewer.FileFrame; import com.mucommander.ui.viewer.FileViewer; import lombok.Getter; import org.fife.ui.rtextarea.GutterEx; import javax.swing.*; import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.*; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Stack; import java.util.function.Consumer; /** * A simple text viewer. Most of the implementation is located in {@link TextEditorImpl}. * * @author Maxence Bernard, Arik Hadas */ public class TextViewer extends FileViewer implements EncodingListener { private final static String CUSTOM_FULL_SCREEN_EVENT = "CUSTOM_FULL_SCREEN_EVENT"; private final TextEditorImpl textEditorImpl; @Getter private static boolean lineWrap = TcConfigurations.getSnapshot().getVariable(TcSnapshot.TEXT_FILE_PRESENTER_LINE_WRAP, TcSnapshot.DEFAULT_LINE_WRAP); @Getter private static boolean lineNumbers = TcConfigurations.getSnapshot().getVariable(TcSnapshot.TEXT_FILE_PRESENTER_LINE_NUMBERS, TcSnapshot.DEFAULT_LINE_NUMBERS); TextMenuHelper menuHelper; private String encoding; private TextFilesHistory.FileRecord historyRecord; private GutterEx gutter; private StatusBar statusBar; TextViewer() { this(new TextEditorImpl(false, null)); textEditorImpl.setStatusBar(getStatusBar()); } TextViewer(TextEditorImpl textEditorImpl) { this.textEditorImpl = textEditorImpl; initGutter(); setComponentToPresent(textEditorImpl.getTextArea()); showLineNumbers(lineNumbers); textEditorImpl.wrap(lineWrap); initMenuBarItems(); } private void initGutter() { // Font defaultFont = new Font("Monospaced", Font.PLAIN, 12); gutter = new GutterEx(textEditorImpl.getTextArea()); gutter.setLineNumberFont(textEditorImpl.getTextArea().getFont()); // TODO gutter.setBackground(Color.LIGHT_GRAY); gutter.setForeground(Color.black); //gutter.setActiveLineRangeColor(new Color(0,0,255)); showLineNumbers(lineNumbers); // Set miscellaneous properties. setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS); setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_AS_NEEDED); } @Override public void setFrame(final FileFrame frame) { super.setFrame(frame); textEditorImpl.setFrame(frame); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_M, InputEvent.CTRL_DOWN_MASK), CUSTOM_FULL_SCREEN_EVENT); } static void setLineWrap(boolean lineWrap) { TextViewer.lineWrap = lineWrap; } static void setLineNumbers(boolean lineNumbers) { TextViewer.lineNumbers = lineNumbers; } void startEditing(AbstractFile file, DocumentListener documentListener) { //initHistoryRecord(file); // Auto-detect encoding try (PushbackInputStream in = file.getPushBackInputStream(EncodingDetector.MAX_RECOMMENDED_BYTE_SIZE)) { String encoding = historyRecord.getEncoding() != null ? historyRecord.getEncoding() : EncodingDetector.detectEncoding(in); if (textEditorImpl.getStatusBar() != null) { textEditorImpl.getStatusBar().setEncoding(encoding); } // Load the file into the text area loadDocument(in, encoding, documentListener); } catch (IOException e) { e.printStackTrace(); } } void loadDocument(InputStream in, final String encoding, DocumentListener documentListener) throws IOException { // If the encoding is UTF-something, wrap the stream in a BOMInputStream to IMAGE_FILTER out the byte-order mark // (see ticket #245) if (encoding != null && encoding.toLowerCase().startsWith("utf")) { in = new BOMInputStream(in); } // If the given encoding is invalid (null or not supported), default to "UTF-8" this.encoding = encoding == null || !Charset.isSupported(encoding) ? "UTF-8" : encoding; if (getStatusBar() != null) { getStatusBar().setEncoding(encoding); } textEditorImpl.read(new BufferedReader(new InputStreamReader(in, this.encoding))); // Listen to document changes if (documentListener != null) { textEditorImpl.addDocumentListener(documentListener); } } @Override public JMenuBar getMenuBar() { JMenuBar menuBar = super.getMenuBar(); // Encoding menu EncodingMenu encodingMenu = new EncodingMenu(new DialogOwner(getFrame()), encoding); encodingMenu.addEncodingListener(this); menuBar.add(menuHelper.getEditMenu()); menuBar.add(menuHelper.getSearchMenu()); menuBar.add(menuHelper.getViewMenu()); menuBar.add(menuHelper.getToolsMenu()); menuBar.add(encodingMenu, menuBar); textEditorImpl.getTextArea().setFocusTraversalKeysEnabled(false); setMainKeyListener(textEditorImpl.getTextArea(), menuBar); menuHelper.setupFileMenu(menuFile, TextViewer.this, getCurrentFile()); return menuBar; } @Override protected StatusBar getStatusBar() { if (statusBar == null) { statusBar = new StatusBar(); } return statusBar; } void saveState(JScrollBar scrollBar) { final TextArea textArea = textEditorImpl.getTextArea(); historyRecord.setLine(textArea.getLine()); historyRecord.setColumn(textArea.getColumn()); historyRecord.setFileType(textArea.getFileType()); historyRecord.setScrollPosition(scrollBar.getValue()); historyRecord.setEncoding(encoding); TextFilesHistory.getInstance().updateRecord(historyRecord).save(); } @Override protected void saveStateOnClose() { saveState(getVerticalScrollBar()); try { AbstractFile currentFile = getCurrentFile(); if (currentFile != null) { currentFile.closePushbackInputStream(); } } catch (IOException e) { e.printStackTrace(); } } @Override protected void restoreStateOnStartup() { final TextArea textArea = textEditorImpl.getTextArea(); textArea.gotoLine(historyRecord.getLine(), historyRecord.getColumn()); getViewport().setViewPosition(new java.awt.Point(0, historyRecord.getScrollPosition())); } String getEncoding() { return encoding; } protected void showLineNumbers(boolean show) { //setRowHeaderView(show ? new TextLineNumbersPanel(textEditorImpl.getTextArea()) : null); gutter.setLineNumbersEnabled(show); checkGutterVisibility(); setLineNumbers(show); } void wrapLines(boolean wrap) { textEditorImpl.wrap(wrap); setLineWrap(wrap); } protected void initMenuBarItems() { menuHelper = new TextMenuHelper(textEditorImpl, false); //menuHelper.initMenu(TextViewer.this, getRowHeader().getView() != null); menuHelper.initMenu(TextViewer.this, lineNumbers); //menuHelper.setupFileMenu(menuFile, TextViewer.this, getCurrentFile()); } @Override public void show(AbstractFile file) { // TODO SHOULD BE IN SEPARATE THREAD !!! initHistoryRecord(file); FileType type = historyRecord.getFileType(); if (type == null) { type = FileType.getFileType(file); historyRecord.setFileType(type); } // detect XML and PHP files if (type == FileType.NONE) { type = TextEditorUtils.detectFileFormat(file); } startEditing(file, null); menuHelper.setSyntax(type); textEditorImpl.prepareForView(file); textEditorImpl.setSyntaxType(type); } @Override public void actionPerformed(ActionEvent e) { if (menuHelper.performAction(e, this)) { return; } super.actionPerformed(e); } @Override public void encodingChanged(Object source, String oldEncoding, String newEncoding) { // Store caret and scrollbar position before change TextArea textArea = textEditorImpl.getTextArea(); int line = textArea.getLine(); int column = textArea.getColumn(); int horizontalPos = getHorizontalScrollBar().getValue(); int verticalPos = getVerticalScrollBar().getValue(); try { // Reload the file using the new encoding // Note: loadDocument closes the InputStream loadDocument(getCurrentFile().getInputStream(), newEncoding, null); // Restore caret and scrollbar textArea.gotoLine(line, column); getViewport().setViewPosition(new java.awt.Point(horizontalPos, verticalPos)); } catch (IOException ex) { InformationDialog.showErrorDialog(getFrame(), i18n("read_error"), i18n("file_editor.cannot_read_file", getCurrentFile().getName())); } } TextFilesHistory.FileRecord initHistoryRecord(AbstractFile file) { historyRecord = TextFilesHistory.getInstance().get(file); return historyRecord; } TextFilesHistory.FileRecord getHistoryRecord() { return historyRecord; } // // // void initHistoryRecord(AbstractFile file, Consumer initializer) { // new Thread(() -> { // historyRecord = new TextFilesHistory.FileRecord(file.getAbsolutePath()); //System.out.println("FILE RECORD LOADED " + gi); // SwingUtilities.invokeLater(() -> initializer.accept(historyRecord)); // }).start(); // } /** * Ensures the gutter is visible if it's showing anything. */ private void checkGutterVisibility() { int count = gutter.getComponentCount(); if (count == 0) { if (getRowHeader() != null && getRowHeader().getView() == gutter) { setRowHeaderView(null); } } else { if (getRowHeader() == null || getRowHeader().getView() == null) { setRowHeaderView(gutter); } } } /** * Returns the first descendant of a component that is an * RTextArea. This is primarily here to support * javax.swing.JLayers that wrap RTextAreas. * * @param comp The component to recursively look through. * @return The first descendant text area, or null if none * is found. */ private static TextArea getFirstRTextAreaDescendant(Component comp) { Stack stack = new Stack<>(); stack.add(comp); while (!stack.isEmpty()) { Component current = stack.pop(); if (current instanceof TextArea) { return (TextArea)current; } if (current instanceof Container container) { stack.addAll(Arrays.asList(container.getComponents())); } } return null; } /** * Sets the view for this scroll pane. This must be an {@link TextArea}. * * @param view The new view. */ @Override public void setViewportView(Component view) { TextArea rtaCandidate; if (!(view instanceof TextArea)) { rtaCandidate = getFirstRTextAreaDescendant(view); if (rtaCandidate == null) { throw new IllegalArgumentException("view must be either an RTextArea or a JLayer wrapping one"); } } else { rtaCandidate = (TextArea)view; } super.setViewportView(view); if (gutter != null) { gutter.setTextArea(rtaCandidate); } } @Override public void setSearchedText(String searchedText) { textEditorImpl.setupSearchContext(searchedText); } @Override public void setSearchedBytes(byte[] searchedBytes) { try { textEditorImpl.setupSearchContext(new String(searchedBytes, encoding)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/package.html ================================================ Provides text files viewing and editing classes. ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/search/AbstractSearchDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.search; import com.jidesoft.hints.ListDataIntelliHints; import com.mucommander.cache.TextHistory; import com.mucommander.ui.dialog.FocusDialog; import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities; import org.fife.ui.rtextarea.SearchContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import javax.swing.event.EventListenerList; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * Base class for all search dialogs (find and replace) * * @author Oleg Trifonov * Created on 21/06/16. */ public class AbstractSearchDialog extends FocusDialog implements ActionListener { private static Logger logger; /** * Listens for properties changing in the search context. */ private class SearchContextListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent e) { handleSearchContextPropertyChanged(e); } } protected SearchContext context; private SearchContextListener contextListener; // Conditions check boxes and the panel they go in. // This should be added in the actual layout of the search dialog. protected JCheckBox cbCaseSensitive; protected JCheckBox cbWholeWord; protected JCheckBox cbRegex; protected JPanel pnlSearchConditions; protected JTextField edtText; protected JButton btnCancel; protected JButton btnFind; protected JRadioButton rbUp; protected JRadioButton rbDown; protected JPanel pnlDirection; protected JLabel lblFind; /** * The "mark all" check box. */ private JCheckBox markAllCheckBox; /** * Folks listening for events in this dialog. */ private EventListenerList listenerList; AbstractSearchDialog(Frame owner, String title, Component locationRelativeComp) { super(owner, title, locationRelativeComp); init(); } private void init() { // The user should set a shared instance between all subclass // instances, but to be safe we set individual ones. contextListener = new SearchContextListener(); setSearchContext(createDefaultSearchContext()); // Make a panel containing the option check boxes. pnlSearchConditions = new JPanel(); pnlSearchConditions.setLayout(new BoxLayout(pnlSearchConditions, BoxLayout.Y_AXIS)); cbCaseSensitive = createCheckBox(i18n("text_viewer.find.case_sensitive")); pnlSearchConditions.add(cbCaseSensitive); cbWholeWord = createCheckBox(i18n("text_viewer.find.whole_word")); pnlSearchConditions.add(cbWholeWord); cbRegex = createCheckBox(i18n("text_viewer.find.regexp")); pnlSearchConditions.add(cbRegex); // Initialize any text fields. edtText = new JTextField(20); edtText.addActionListener(this); List history = TextHistory.getInstance().getList(TextHistory.Type.TEXT_SEARCH); // new AutoCompletion(findField, history).setStrict(false); edtText.setText(""); new ListDataIntelliHints<>(edtText, history).setCaseSensitive(true); // Initialize other stuff. btnCancel = new JButton(i18n("cancel")); btnCancel.addActionListener(this); listenerList = new EventListenerList(); // Make a panel containing the "search up/down" radio buttons. pnlDirection = new JPanel(); pnlDirection.setLayout(new BoxLayout(pnlDirection, BoxLayout.LINE_AXIS)); pnlDirection.setBorder(BorderFactory.createTitledBorder(i18n("text_viewer.find.direction"))); ButtonGroup bg = new ButtonGroup(); rbUp = new JRadioButton(i18n("text_viewer.find.up"), false); rbDown = new JRadioButton(i18n("text_viewer.find.down"), true); rbUp.addActionListener(this); rbDown.addActionListener(this); bg.add(rbUp); bg.add(rbDown); pnlDirection.add(rbUp); pnlDirection.add(rbDown); // Initialize the "mark all" button. markAllCheckBox = createCheckBox(i18n("text_viewer.find.mark_all")); // Rearrange the search conditions panel. pnlSearchConditions.removeAll(); pnlSearchConditions.setLayout(new BorderLayout()); JPanel temp = new JPanel(); temp.setLayout(new BoxLayout(temp, BoxLayout.PAGE_AXIS)); temp.add(cbCaseSensitive); temp.add(cbWholeWord); pnlSearchConditions.add(temp, BorderLayout.LINE_START); temp = new JPanel(); temp.setLayout(new BoxLayout(temp, BoxLayout.PAGE_AXIS)); temp.add(cbRegex); temp.add(markAllCheckBox); pnlSearchConditions.add(temp, BorderLayout.LINE_END); lblFind = new JLabel(i18n("text_viewer.find_button") + ":"); btnFind = new JButton(i18n("text_viewer.find_button")); btnFind.addActionListener(this); btnFind.setDefaultCapable(true); btnFind.setEnabled(false); installKeyboardActions(); fixHeight(); } @Override public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == cbCaseSensitive) { boolean matchCase = cbCaseSensitive.isSelected(); context.setMatchCase(matchCase); } else if (src == cbWholeWord) { boolean wholeWord = cbWholeWord.isSelected(); context.setWholeWord(wholeWord); } else if (src == cbRegex) { boolean useRegEx = cbRegex.isSelected(); context.setRegularExpression(useRegEx); } else if (src == rbUp) { context.setSearchForward(false); } else if (src == rbDown) { context.setSearchForward(true); } else if (src == markAllCheckBox) { boolean checked = markAllCheckBox.isSelected(); context.setMarkAll(checked); } else if (src == btnCancel) { dispose(); } else if (src == btnFind || src == edtText) { // Add the item to the combo box's list, if it isn't already there. context.setSearchFor(getSearchString()); fireSearchEvent(e); // Let parent application know } } private JCheckBox createCheckBox(String name) { JCheckBox cb = new JCheckBox(name); cb.addActionListener(this); return cb; } /** * Returns the default search context to use for this dialog. Applications * that create new subclasses of this class can provide customized * search contexts here. * * @return The default search context. */ protected SearchContext createDefaultSearchContext() { return new SearchContext(); } /** * Makes the "Find text" field active. */ protected void focusFindTextField() { edtText.requestFocusInWindow(); edtText.selectAll(); } /** * Returns the search context used by this dialog. * * @return The search context. * @see #setSearchContext(SearchContext) */ public SearchContext getSearchContext() { return context; } /** * Returns the text to search for. * * @return The text the user wants to search for. */ public String getSearchString() { return edtText.getText(); } /** * Called when the regex checkbox is clicked (or its value is modified * via a change to the search context). Subclasses can override * to add custom behavior, but should call the super implementation. */ protected void handleRegExCheckBoxClicked() { handleToggleButtons(); // "Content assist" support boolean b = cbRegex.isSelected(); } /** * Called whenever a property in the search context is modified. * Subclasses should override if they listen for additional properties. * * @param e The property change event fired. */ protected void handleSearchContextPropertyChanged(PropertyChangeEvent e) { // A property changed on the context itself. String prop = e.getPropertyName(); if (SearchContext.PROPERTY_SEARCH_FORWARD.equals(prop)) { boolean newValue = (Boolean) e.getNewValue(); JRadioButton button = newValue ? rbDown : rbUp; button.setSelected(true); } else if (SearchContext.PROPERTY_MARK_ALL.equals(prop)) { boolean newValue = (Boolean) e.getNewValue(); markAllCheckBox.setSelected(newValue); } else if (SearchContext.PROPERTY_MATCH_CASE.equals(prop)) { boolean newValue = (Boolean) e.getNewValue(); cbCaseSensitive.setSelected(newValue); } else if (SearchContext.PROPERTY_MATCH_WHOLE_WORD.equals(prop)) { boolean newValue = (Boolean) e.getNewValue(); cbWholeWord.setSelected(newValue); } else if (SearchContext.PROPERTY_USE_REGEX.equals(prop)) { boolean newValue = (Boolean) e.getNewValue(); cbRegex.setSelected(newValue); handleRegExCheckBoxClicked(); } else if (SearchContext.PROPERTY_SEARCH_FOR.equals(prop)) { String newValue = (String)e.getNewValue(); String oldValue = getSearchString(); // Prevents IllegalStateExceptions if (!newValue.equals(oldValue)) { setSearchString(newValue); } } } /** * Returns whether any action-related buttons (Find Next, Replace, etc.) * should be enabled. Subclasses can call this method when the "Find What" * or "Replace With" text fields are modified. They can then * enable/disable any components as appropriate. * * @return Whether the buttons should be enabled. */ protected FindReplaceButtonsEnableResult handleToggleButtons() { FindReplaceButtonsEnableResult er; //String text = getSearchString(); String text = edtText.getText(); if (text.isEmpty()) { er = new FindReplaceButtonsEnableResult(false, null); } else if (cbRegex.isSelected()) { try { Pattern.compile(text); er = new FindReplaceButtonsEnableResult(true, null); } catch (PatternSyntaxException pse) { er = new FindReplaceButtonsEnableResult(false, pse.getMessage()); } } else { er = new FindReplaceButtonsEnableResult(true, null); } boolean enable = er.getEnable(); btnFind.setEnabled(enable); // setBackground doesn't show up with XP Look and Feel! //findTextComboBox.setBackground(enable ? // UIManager.getColor("ComboBox.background") : Color.PINK); // edtText.setForeground(enable ? UIManager.getColor("TextField.foreground") : UIUtil.getErrorTextForeground()); // String tooltip = SearchUtil.getToolTip(er); // edtText.setToolTipText(tooltip); // Always set, even if null return er; } boolean matchesSearchFor(String text) { if (text == null || text.isEmpty()) { return false; } String searchFor = edtText.getText(); if (searchFor != null && !searchFor.isEmpty()) { boolean matchCase = cbCaseSensitive.isSelected(); if (cbRegex.isSelected()) { int flags = Pattern.MULTILINE; // '^' and '$' are done per line. flags = RSyntaxUtilities.getPatternFlags(matchCase, flags); Pattern pattern; try { pattern = Pattern.compile(searchFor, flags); } catch (PatternSyntaxException pse) { getLogger().error("Invalid regular expression: " + searchFor, pse); return false; } return pattern.matcher(text).matches(); } else { if (matchCase) { return searchFor.equals(text); } return searchFor.equalsIgnoreCase(text); } } return false; } /** * Returns whether the characters on either side of * substr(searchIn,startPos,startPos+searchStringLength) * are whitespace. * While this isn't the best definition of "whole word", it's the one we're going to use for now. */ public static boolean isWholeWord(CharSequence searchIn, int offset, int len) { boolean wsBefore, wsAfter; try { wsBefore = Character.isWhitespace(searchIn.charAt(offset - 1)); } catch (IndexOutOfBoundsException e) { wsBefore = true; } try { wsAfter = Character.isWhitespace(searchIn.charAt(offset + len)); } catch (IndexOutOfBoundsException e) { wsAfter = true; } return wsBefore && wsAfter; } /** * Initializes the UI in this tool bar from a search context. This is * called whenever a new search context is installed on this tool bar * (which should practically be never). */ protected void refreshUIFromContext() { if (cbCaseSensitive == null || markAllCheckBox == null) { return; // First time through, UI not realized yet } cbCaseSensitive.setSelected(context.getMatchCase()); cbRegex.setSelected(context.isRegularExpression()); cbWholeWord.setSelected(context.getWholeWord()); markAllCheckBox.setSelected(context.getMarkAll()); boolean searchForward = context.getSearchForward(); rbUp.setSelected(!searchForward); rbDown.setSelected(searchForward); } /** * Overridden to ensure the "Find text" field gets focused. */ @Override public void requestFocus() { super.requestFocus(); focusFindTextField(); } /** * Sets the search context for this dialog. You'll usually want to call * this method for all search dialogs and give them the same search * context, so that their options (match case, etc.) stay in sync with one * another. * * @param context The new search context. This cannot be null. * @see #getSearchContext() */ public void setSearchContext(SearchContext context) { if (this.context != null) { this.context.removePropertyChangeListener(contextListener); } this.context = context; this.context.addPropertyChangeListener(contextListener); refreshUIFromContext(); } /** * Sets the java.lang.String to search for. * * @param text The String to put into the search field. */ public void setSearchString(String text) { edtText.setText(text); if (text != null) { edtText.select(0, text.length()); } } /** * Adds a {@link SearchListener} to this dialog. This listener will * be notified when find or replace operations are triggered. For * example, for a Replace dialog, a listener will receive notification * when the user clicks "Find", "Replace", or "Replace All". * * @param l The listener to add. */ public void addSearchListener(SearchListener l) { listenerList.add(SearchListener.class, l); } /** * Notifies all listeners that have registered interest for notification on * this event type. The event instance is lazily created using the * event parameter. * * @param event The ActionEvent object coming from a * child component. */ void fireSearchEvent(ActionEvent event) { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); SearchEvent e = null; // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == SearchListener.class) { // Lazily create the event: if (e == null) { String command = event.getActionCommand(); SearchEvent.Type type = SearchEvent.Type.valueOf(command); e = new SearchEvent(this, type, context); } ((SearchListener)listeners[i+1]).searchEvent(e); } } } /** * Adds extra keyboard actions for Find and Replace dialogs. */ private void installKeyboardActions() { JRootPane rootPane = getRootPane(); InputMap im = rootPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); ActionMap am = rootPane.getActionMap(); int modifier = getToolkit().getMenuShortcutKeyMaskEx(); KeyStroke ctrlF = KeyStroke.getKeyStroke(KeyEvent.VK_F, modifier); im.put(ctrlF, "focusSearchForField"); am.put("focusSearchForField", new AbstractAction() { public void actionPerformed(ActionEvent e) { AbstractSearchDialog.this.requestFocus(); } }); } /** * Used by makeSpringCompactGrid. This is ripped off directly from * SpringUtilities.java in the Sun Java Tutorial. * * @param parent The container whose layout must be an instance of * SpringLayout. * @return The spring constraints for the specified component contained * in parent. */ private static SpringLayout.Constraints getConstraintsForCell(int row, int col, Container parent, int cols) { SpringLayout layout = (SpringLayout) parent.getLayout(); Component c = parent.getComponent(row * cols + col); return layout.getConstraints(c); } /** * This method is ripped off from SpringUtilities.java found * on Sun's Java Tutorial pages. It takes a component whose layout is * SpringLayout and organizes the components it contains into * a nice grid. * Aligns the first rows * cols components of * parent in a grid. Each component in a column is as wide as * the maximum preferred width of the components in that column; height is * similarly determined for each row. The parent is made just big enough * to fit them all. * * @param parent The container whose layout is SpringLayout. * @param rows The number of rows of components to make in the container. * @param cols The number of columns of components to make. * @param initialX The x-location to start the grid at. * @param initialY The y-location to start the grid at. * @param xPad The x-padding between cells. * @param yPad The y-padding between cells. */ protected static void makeSpringCompactGrid(Container parent, int rows, int cols, int initialX, int initialY, int xPad, int yPad) { SpringLayout layout; try { layout = (SpringLayout)parent.getLayout(); } catch (ClassCastException cce) { getLogger().error("The first argument to makeCompactGrid must use SpringLayout."); return; } // Align all cells in each column and make them the same width. Spring x = Spring.constant(initialX); for (int c = 0; c < cols; c++) { Spring width = Spring.constant(0); for (int r = 0; r < rows; r++) { width = Spring.max(width, getConstraintsForCell(r, c, parent, cols).getWidth()); } for (int r = 0; r < rows; r++) { SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, cols); constraints.setX(x); constraints.setWidth(width); } x = Spring.sum(x, Spring.sum(width, Spring.constant(xPad))); } // Align all cells in each row and make them the same height. Spring y = Spring.constant(initialY); for (int r = 0; r < rows; r++) { Spring height = Spring.constant(0); for (int c = 0; c < cols; c++) { height = Spring.max(height, getConstraintsForCell(r, c, parent, cols).getHeight()); } for (int c = 0; c < cols; c++) { SpringLayout.Constraints constraints = getConstraintsForCell(r, c, parent, cols); constraints.setY(y); constraints.setHeight(height); } y = Spring.sum(y, Spring.sum(height, Spring.constant(yPad))); } // Set the parent's size. SpringLayout.Constraints pCons = layout.getConstraints(parent); pCons.setConstraint(SpringLayout.SOUTH, y); pCons.setConstraint(SpringLayout.EAST, x); } private static Logger getLogger() { if (logger == null) { logger = LoggerFactory.getLogger(AbstractSearchDialog.class); } return logger; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/search/FindDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.search; import org.fife.ui.rtextarea.SearchContext; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.ActionEvent; /** * @author Oleg Trifonov * Created on 21/06/16. */ public class FindDialog extends AbstractSearchDialog { /** * Our search listener, cached so we can grab its selected text easily. */ private SearchListener searchListener; private static String lastSearchedStr; public FindDialog(Frame owner, SearchListener listener) { super(owner, i18n("text_viewer.find"), null); this.searchListener = listener; btnFind.setActionCommand(SearchEvent.Type.FIND.toString()); edtText.setActionCommand(SearchEvent.Type.FIND.toString()); // Make a panel containing the "Find" edit box. JPanel enterTextPane = new JPanel(new SpringLayout()); enterTextPane.setBorder(BorderFactory.createEmptyBorder(0,5,5,5)); edtText.getDocument().addDocumentListener(new FindDocumentListener()); JPanel temp = new JPanel(new BorderLayout()); temp.add(edtText, BorderLayout.CENTER); enterTextPane.add(lblFind); enterTextPane.add(temp); makeSpringCompactGrid(enterTextPane, 1, 2, //rows, cols 0,0, //initX, initY 6, 6); //xPad, yPad // Make a panel containing the inherited search direction radio buttons and the inherited search options. JPanel bottomPanel = new JPanel(new BorderLayout()); temp = new JPanel(new BorderLayout()); bottomPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); temp.add(pnlSearchConditions, BorderLayout.LINE_START); temp.add(pnlDirection); bottomPanel.add(temp, BorderLayout.LINE_START); // Now, make a panel containing all the above stuff. JPanel leftPanel = new JPanel(); leftPanel.setLayout(new BoxLayout(leftPanel, BoxLayout.Y_AXIS)); leftPanel.add(enterTextPane); leftPanel.add(bottomPanel); // Make a panel containing the action buttons. JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new GridLayout(2, 1, 5, 5)); buttonPanel.add(btnFind); buttonPanel.add(btnCancel); JPanel rightPanel = new JPanel(); rightPanel.setLayout(new BorderLayout()); rightPanel.add(buttonPanel, BorderLayout.NORTH); // Put everything into a neat little package. JPanel contentPane = new JPanel(new BorderLayout()); contentPane.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 5)); contentPane.add(leftPanel); contentPane.add(rightPanel, BorderLayout.LINE_END); getContentPane().add(contentPane); getRootPane().setDefaultButton(btnFind); setLocationRelativeTo(getParent()); setSearchContext(new SearchContext()); addSearchListener(listener); } @Override public void actionPerformed(ActionEvent e) { super.actionPerformed(e); Object src = e.getSource(); if (src == btnFind || src == edtText) { lastSearchedStr = edtText.getText(); dispose(); } } /** * Overrides JDialog's setVisible method; decides * whether buttons are enabled. * * @param visible Whether the dialog should be visible. */ @Override public void setVisible(boolean visible) { if (visible) { // Select text entered in the UI String text = searchListener.getSelectedText(); if (text != null) { edtText.setText(text); } String selectedItem = edtText.getText(); btnFind.setEnabled(selectedItem != null && !selectedItem.isEmpty()); super.setVisible(true); focusFindTextField(); } else { super.setVisible(false); } } /** * Listens for changes in the text field (find search field). */ private class FindDocumentListener implements DocumentListener { public void insertUpdate(DocumentEvent e) { handleToggleButtons(); } public void removeUpdate(DocumentEvent e) { if (edtText.getDocument().getLength() == 0) { btnFind.setEnabled(false); } else { handleToggleButtons(); } } public void changedUpdate(DocumentEvent e) { } } public static String getLastSearchStr() { return lastSearchedStr; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/search/FindReplaceButtonsEnableResult.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.search; /** * Returns the result of whether the "action" buttons such as "Find" * and "Replace" should be enabled. * * Created on 21/06/16. * @author Oleg Trifonov */ public class FindReplaceButtonsEnableResult { private boolean enable; private final String error; FindReplaceButtonsEnableResult(boolean enable, String error) { this.enable = enable; this.error = error; } public boolean getEnable() { return enable; } public String getError() { return error; } public void setEnable(boolean enable) { this.enable = enable; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/search/ReplaceDialog.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.search; import org.fife.ui.rtextarea.SearchContext; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; /** * @author Oleg Trifonov * Created on 21/06/16. */ public class ReplaceDialog extends AbstractSearchDialog { private final JButton btnReplace; private final JButton btnReplaceAll; private final JTextField edtReplace; /** * Our search listener, cached so we can grab its selected text easily. */ private final SearchListener searchListener; public ReplaceDialog(Frame owner, SearchListener listener) { super(owner, i18n("text_editor.replace"), null); this.searchListener = listener; btnFind.setActionCommand(SearchEvent.Type.FIND.toString()); edtText.setActionCommand(SearchEvent.Type.FIND.toString()); ReplaceDocumentListener replaceDocumentListener = new ReplaceDocumentListener(); // Create a panel for the "Find what" and "Replace with" text fields. JPanel searchPanel = new JPanel(new SpringLayout()); edtText.getDocument().addDocumentListener(replaceDocumentListener); // Create the "Replace with" text field. edtReplace = new JTextField(20); edtReplace.getDocument().addDocumentListener(replaceDocumentListener); // Create the "Replace with" label. JLabel lblReplace = new JLabel(i18n("text_editor.replace_with") + ":"); JPanel temp = new JPanel(new BorderLayout()); temp.add(edtReplace); temp.add(edtText, BorderLayout.CENTER); JPanel temp2 = new JPanel(new BorderLayout()); temp2.add(edtReplace); temp2.add(edtReplace, BorderLayout.CENTER); searchPanel.add(lblFind); searchPanel.add(temp); searchPanel.add(lblReplace); searchPanel.add(temp2); makeSpringCompactGrid(searchPanel, 2, 2, 5, 0, 6, 6); // Make a panel containing the inherited search direction radio // buttons and the inherited search options. JPanel bottomPanel = new JPanel(new BorderLayout()); temp = new JPanel(new BorderLayout()); bottomPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); temp.add(pnlSearchConditions, BorderLayout.LINE_START); temp.add(pnlDirection); bottomPanel.add(temp, BorderLayout.LINE_START); // Now, make a panel containing all the above stuff. JPanel leftPanel = new JPanel(); leftPanel.setLayout(new BoxLayout(leftPanel, BoxLayout.Y_AXIS)); leftPanel.add(searchPanel); leftPanel.add(bottomPanel); // Make a panel containing the action buttons. JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new GridLayout(4, 1, 5, 5)); btnReplace = new JButton(i18n("text_editor.replace_button")); btnReplace.setActionCommand(SearchEvent.Type.REPLACE.name()); btnReplace.addActionListener(this); btnReplace.setEnabled(false); btnReplaceAll = new JButton(i18n("text_editor.replace_all")); btnReplaceAll.setActionCommand(SearchEvent.Type.REPLACE_ALL.name()); btnReplaceAll.addActionListener(this); btnReplaceAll.setEnabled(false); buttonPanel.add(btnFind); buttonPanel.add(btnReplace); buttonPanel.add(btnReplaceAll); buttonPanel.add(btnCancel); JPanel rightPanel = new JPanel(new BorderLayout()); rightPanel.add(buttonPanel, BorderLayout.NORTH); // Put it all together! JPanel contentPane = new JPanel(new BorderLayout()); contentPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 5)); contentPane.add(leftPanel); contentPane.add(rightPanel, BorderLayout.LINE_END); getContentPane().add(contentPane); getRootPane().setDefaultButton(btnFind); setLocationRelativeTo(getParent()); setSearchContext(new SearchContext()); addSearchListener(listener); } @Override public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); if (SearchEvent.Type.REPLACE.name().equals(command) || SearchEvent.Type.REPLACE_ALL.name().equals(command)) { context.setSearchFor(getSearchString()); context.setReplaceWith(edtReplace.getText()); fireSearchEvent(e); // Let parent application know } else { super.actionPerformed(e); if (SearchEvent.Type.FIND.name().equals(command)) { handleToggleButtons(); // Replace button could toggle state } } } @Override protected void handleSearchContextPropertyChanged(PropertyChangeEvent e) { String prop = e.getPropertyName(); if (SearchContext.PROPERTY_REPLACE_WITH.equals(prop)) { String newValue = (String)e.getNewValue(); if (newValue == null) { newValue = ""; } String oldValue = edtReplace.getText(); // Prevents IllegalStateExceptions if (!newValue.equals(oldValue)) { edtReplace.setText(newValue); } } else { super.handleSearchContextPropertyChanged(e); } } @Override protected FindReplaceButtonsEnableResult handleToggleButtons() { FindReplaceButtonsEnableResult er = super.handleToggleButtons(); boolean shouldReplace = er.getEnable(); btnReplaceAll.setEnabled(shouldReplace); // "Replace" is only enabled if text to search for is selected in the UI if (shouldReplace) { String text = searchListener.getSelectedText(); shouldReplace = matchesSearchFor(text); } btnReplace.setEnabled(shouldReplace); return er; } /** * Listens for changes in the text field (find search field). */ private class ReplaceDocumentListener implements DocumentListener { @Override public void insertUpdate(DocumentEvent e) { if (e.getDocument().equals(edtText.getDocument())) { handleToggleButtons(); } } @Override public void removeUpdate(DocumentEvent e) { if (e.getDocument().equals(edtText.getDocument()) && e.getDocument().getLength() == 0) { btnFind.setEnabled(false); btnReplace.setEnabled(false); btnReplaceAll.setEnabled(false); } else { handleToggleButtons(); } } @Override public void changedUpdate(DocumentEvent e) { } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/search/SearchEvent.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.search; import org.fife.ui.rtextarea.SearchContext; import java.util.EventObject; /** * The event fired whenever a user wants to search for or replace text in a * Find or Replace dialog/tool bar. * * Created on 21/06/16. */ public class SearchEvent extends EventObject { private SearchContext context; private Type type; SearchEvent(Object source, Type type, SearchContext context) { super(source); this.type = type; this.context = context; } public Type getType() { return type; } public SearchContext getSearchContext() { return context; } /** * Types of search events. */ public enum Type { /** * The event fired when the text to "mark all" has changed. */ MARK_ALL, /** * The event fired when the user wants to find text in the editor. */ FIND, /** * The event fired when the user wants to replace text in the editor. */ REPLACE, /** * The event fired when the user wants to replace all instances of * specific text with new text in the editor. */ REPLACE_ALL } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/search/SearchListener.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.search; import java.util.EventListener; /** * Listens for events fired from a Find or Replace dialog/tool bar. * * Created on 21/06/16. * */ public interface SearchListener extends EventListener { /** * Callback called whenever a search event occurs. * * @param e The event. */ void searchEvent(SearchEvent e); String getSelectedText(); } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/tools/ExecOutputTextPane.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.tools; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.util.StringUtils; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; import javax.swing.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import static com.mucommander.commons.util.StringUtils.isNullOrBlank; import static com.mucommander.commons.util.StringUtils.isNumber; class ExecOutputTextPane extends JTextPane { private final Style styleDefault = addStyle("Default", null); private final Style styleFilePath = addStyle("Path", null); private final Style stylePosition = addStyle("Position", null); private final Style styleMessage = addStyle("Message", null); private final Style styleError = addStyle("Error", styleMessage); private final Style styleWarning = addStyle("Warning", styleMessage); private final Style styleMarker = addStyle("Marker", styleMessage); ExecOutputTextPane(Runnable onClose, OnClickFileHandler onFileClickHandler) { setCaretPosition(0); setEditable(false); setupKeyListener(onClose); setupMouseListener(onFileClickHandler); setupColors(); setupStyles(); } private void setupKeyListener(Runnable onClose) { addKeyListener(new KeyAdapter() { private boolean pressed; @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { pressed = true; e.consume(); } else { pressed = false; } } @Override public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE && pressed) { if (onClose != null) { onClose.run(); } e.consume(); } } }); } private void setupMouseListener(OnClickFileHandler onFileClickHandler) { addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { String str = getLineForMousePosition(e.getPoint()); if (isNullOrBlank(str)) { return; } String[] parts = str.split(":"); if (parts.length < 3) { return; } if (onFileClickHandler != null && isNumber(parts[1]) && isFilePath(parts[0])) { AbstractFile file = FileFactory.getFile(parts[0]); int line = Integer.parseInt(parts[1]); int column = parts.length > 3 && isNumber(parts[2]) ? Integer.parseInt(parts[2]) : 1; onFileClickHandler.onClick(file, line, column); } } }); } private void setupColors() { setForeground(ThemeManager.getCurrentColor(Theme.SHELL_FOREGROUND_COLOR)); setCaretColor(ThemeManager.getCurrentColor(Theme.SHELL_FOREGROUND_COLOR)); setBackground(ThemeManager.getCurrentColor(Theme.SHELL_BACKGROUND_COLOR)); setSelectedTextColor(ThemeManager.getCurrentColor(Theme.SHELL_SELECTED_FOREGROUND_COLOR)); setSelectionColor(ThemeManager.getCurrentColor(Theme.SHELL_SELECTED_BACKGROUND_COLOR)); setFont(ThemeManager.getCurrentFont(Theme.SHELL_FONT)); } private void setupStyles() { StyleConstants.setForeground(styleFilePath, new Color(0x5555ff)); StyleConstants.setUnderline(styleFilePath, true); StyleConstants.setForeground(stylePosition, Color.YELLOW); StyleConstants.setForeground(styleError, Color.RED); StyleConstants.setForeground(styleWarning, Color.CYAN); StyleConstants.setForeground(styleMarker, Color.WHITE); } private void add(String s, Style style) { StyledDocument doc = getStyledDocument(); try { doc.insertString(doc.getLength(), s, style); } catch (BadLocationException e) { e.printStackTrace(); } } void addLine(String line) { if (isMarker(line)) { add(line, styleMarker); update(); return; } String[] parts = line.split(":"); if (parts.length < 3 || !isNumber(parts[1]) || !isFilePath(parts[0])) { add(line, styleDefault); update(); return; } add(parts[0], styleFilePath); add(":", styleDefault); add(parts[1], stylePosition); add(":", styleDefault); int index; if (isNumber(parts[2])) { add(parts[2], stylePosition); add(":", styleDefault); index = 3; } else { index = 2; } Style defaultStyle = styleMessage; for (int i = index; i < parts.length; i++) { String part = parts[i]; String lower = part.trim().toLowerCase(); if (lower.equals("error")) { add(part, styleError); defaultStyle = styleError; } else if (lower.equals("warning") || lower.equals("note")) { add(part, styleWarning); defaultStyle = styleWarning; } else { add(part, defaultStyle); } if (i < parts.length-1 || line.endsWith(":")) { add(":", defaultStyle); } } update(); } private void update() { setCaretPosition(getText().length()); getCaret().setVisible(true); repaint(); } void clear() { setText(""); setCaretPosition(0); getCaret().setVisible(true); requestFocus(); } private static boolean isMarker(String s) { if (StringUtils.isNullOrBlank(s)) { return false; } for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (" \t^~\n".indexOf(ch) < 0) { return false; } } return true; } private static boolean isFilePath(String s) { return new File(s).exists(); } private String getLineForMousePosition(Point p) { if (p == null) { return null; } int pos = viewToModel2D(p); try { int start = Utilities.getRowStart(this, pos); int end = Utilities.getRowEnd(this, pos); if (start < 0 || end < start) { return null; } return getDocument().getText(start, end - start); } catch (BadLocationException e) { return null; } } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/tools/ExecPanel.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.tools; import com.mucommander.commons.file.AbstractFile; import com.mucommander.process.AbstractProcess; import com.mucommander.process.ProcessListener; import com.mucommander.shell.Shell; import com.mucommander.ui.icon.SpinningDial; import ru.trolsoft.utils.StringStream; import javax.swing.*; import java.awt.*; import java.io.PrintStream; public class ExecPanel extends JPanel implements ProcessListener { private final ExecOutputTextPane outputPane; private final SpinningDial dial; private final JLabel lblDial; private final StringStream stringStream = new StringStream(); /** Stream used to send characters to the process' stdin process. */ private PrintStream processInput; /** Process currently running, null if none. */ private AbstractProcess currentProcess; public ExecPanel(Runnable onClose, OnClickFileHandler onFileClickHandler) { super(); setLayout(new BorderLayout()); outputPane = new ExecOutputTextPane(onClose, onFileClickHandler); JScrollPane scrollPane = new JScrollPane(outputPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); this.add(scrollPane, BorderLayout.CENTER); lblDial = new JLabel(dial = new SpinningDial()); this.add(lblDial, BorderLayout.SOUTH); } public void runCommand(AbstractFile folder, String command) { try { // Starts the spinning dial. dial.setAnimated(true); lblDial.setVisible(true); // Change 'Run' button to 'Stop' //this.btnRunStop.setText(i18n("run_dialog.stop")); // Resets the process outputPane area. outputPane.clear(); stringStream.clear(); // No new command can be entered while a process is running. //inputCombo.setEnabled(false); currentProcess = Shell.execute(command, folder, this); processInput = new PrintStream(currentProcess.getOutputStream(), true); // Repaints the dialog. repaint(); } catch (Exception e) { // Notifies the user that an error occurred and resets to normal state. e.printStackTrace(); outputPane.addLine("generic_error " + e.getMessage()); // TODO // switchToRunState(); } } @Override public void processDied(int returnValue) { dial.setAnimated(false); lblDial.setVisible(false); if (stringStream.hasRemains()) { outputPane.addLine(stringStream.getRemains()); } } @Override public void processOutput(String output) { stringStream.add(output); while (stringStream.hasCompleted()) { outputPane.addLine(stringStream.getNext()); } } @Override public void processOutput(byte[] buffer, int offset, int length) { } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/tools/ExecUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.tools; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.local.LocalFile; import java.io.IOException; public class ExecUtils { public static ProcessParams getBuilderParams(AbstractFile file) { if (file == null || !file.exists() || !(file.getTopAncestor() instanceof LocalFile)) { return null; } AbstractFile folder = file.getParent(); if (folder == null || !folder.exists()) { return null; } if ("py".equalsIgnoreCase(file.getExtension())) { return new ProcessParams(folder, "python " + file.getName()); } while (true) { if (folderContainsBuilder(folder, "Makefile")) { return new ProcessParams(folder, "make"); } else if (folderContainsBuilder(folder, "make.builder")) { return new ProcessParams(folder, "builder"); } else if (folderContainsBuilder(folder, "build.xml")) { return new ProcessParams(folder, "ant"); } if (folder.isRoot()) { return null; } folder = folder.getParent(); } } private static boolean folderContainsBuilder(AbstractFile folder, String fileName) { try { AbstractFile child = folder.getChild(fileName); return child != null && child.exists(); } catch (IOException ignore) {} return false; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/tools/OnClickFileHandler.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.tools; import com.mucommander.commons.file.AbstractFile; public interface OnClickFileHandler { void onClick(AbstractFile file, int line, int column); } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/tools/ProcessParams.java ================================================ package com.mucommander.ui.viewer.text.tools; import com.mucommander.commons.file.AbstractFile; public class ProcessParams { public final AbstractFile folder; public final String command; ProcessParams(AbstractFile folder, String command) { this.folder = folder; this.command = command; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/utils/CodeFormatException.java ================================================ /* * This file is part of trolCommander, http://www.mucommander.com * Copyright (C) 2013-2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.utils; import lombok.Getter; /** * @author Oleg Trifonov */ @Getter public class CodeFormatException extends Exception { private final int line; private final int row; CodeFormatException(String message, int line, int row, Throwable cause) { super(message, cause); this.line = line; this.row = row; } } ================================================ FILE: src/main/java/com/mucommander/ui/viewer/text/utils/CodeFormatter.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.viewer.text.utils import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.JsonParser import com.google.gson.JsonSyntaxException import org.w3c.dom.Document import org.w3c.dom.ls.DOMImplementationLS import org.xml.sax.InputSource import org.xml.sax.SAXException import org.xml.sax.SAXParseException import java.io.IOException import java.io.StringReader import java.io.StringWriter import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.ParserConfigurationException /** * Created by trol on 18/09/14. * */ object CodeFormatter { private val GSON: Gson = GsonBuilder().setPrettyPrinting().create() private val JSON_PARSER = JsonParser() @JvmStatic @Throws(CodeFormatException::class) fun formatXml(unformattedXml: String): String { if (unformattedXml.trim { it <= ' ' }.isEmpty()) { throw CodeFormatException("XML input is null or empty", 0, 0, null) } try { val document = parseXmlFile(unformattedXml) val domImpl = document.implementation as DOMImplementationLS val serializer = domImpl.createLSSerializer().apply { domConfig.setParameter("format-pretty-print", true) } val writer = StringWriter() val output = domImpl.createLSOutput().apply { encoding = "UTF-8" characterStream = writer } serializer.write(document, output) return writer.toString() } catch (e: CodeFormatException) { throw e } catch (e: Exception) { throw CodeFormatException("Failed to format XML: " + e.message, 0, 0, e) } } @Throws(CodeFormatException::class) private fun parseXmlFile(src: String): Document { try { val dbf = DocumentBuilderFactory.newInstance().apply { setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) setFeature("http://xml.org/sax/features/external-general-entities", false) setFeature("http://xml.org/sax/features/external-parameter-entities", false) isNamespaceAware = true } val stream = InputSource(StringReader(src)) return dbf.newDocumentBuilder().parse(stream) } catch (e: SAXParseException) { throw CodeFormatException(e.message, e.lineNumber, e.columnNumber, e) } catch (e: ParserConfigurationException) { throw CodeFormatException("XML Parser configuration error: " + e.message, 0, 0, e) } catch (e: SAXException) { throw CodeFormatException("Failed to parse XML: " + e.message, 0, 0, e) } catch (e: IOException) { throw CodeFormatException("Failed to parse XML: " + e.message, 0, 0, e) } } @JvmStatic @Throws(CodeFormatException::class) fun formatJson(json: String): String? { if (json.trim { it <= ' ' }.isEmpty()) { throw CodeFormatException("JSON input is null or empty", 0, 0, null) } try { val el = JSON_PARSER.parse(json) return GSON.toJson(el) } catch (e: JsonSyntaxException) { throw CodeFormatException("Invalid JSON syntax: " + e.message, 0, 0, e) } catch (e: Exception) { throw CodeFormatException("Failed to format JSON: " + e.message, 0, 0, e) } } } ================================================ FILE: src/main/java/com/mucommander/ui/widgets/render/BasicComboBoxRenderer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2025 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.ui.widgets.render; import javax.swing.Icon; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.ListCellRenderer; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import java.awt.Component; import java.awt.Dimension; /** * Renderer for ComboBox * * @author Oleg Trifonov * Created on 05/01/14. */ public class BasicComboBoxRenderer extends JLabel implements ListCellRenderer { /** * An empty Border. This field might not be used. To change the * Border used by this renderer directly set it using * the setBorder method. */ protected static final Border NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1); @Override public Component getListCellRendererComponent(JList list, T value, int index, boolean isSelected, boolean cellHasFocus) { if (isSelected) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } else { setBackground(list.getBackground()); setForeground(list.getForeground()); } setFont(list.getFont()); if (value instanceof Icon) { setIcon((Icon) value); } else { setText(value == null ? "" : value.toString()); } return this; } public BasicComboBoxRenderer() { super(); setOpaque(true); setBorder(NO_FOCUS_BORDER); } public Dimension getPreferredSize() { Dimension size; if (getText() == null || getText().isEmpty()) { setText(" "); size = super.getPreferredSize(); setText(""); } else { size = super.getPreferredSize(); } return size; } } ================================================ FILE: src/main/java/com/mucommander/updates/NightlyChecker.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2021 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.updates; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; public class NightlyChecker { public static final String ROOT_URL = "http://trolsoft.ru/content/soft/trolcommander/"; private static final String BUILD_CODE_URL = ROOT_URL + "nightly/buildcode"; private static String getNightlyBuildCode() { AbstractFile file = FileFactory.getFile(BUILD_CODE_URL); if (file == null) { return null; } try (InputStream is = file.getInputStream(); DataInputStream reader = new DataInputStream(is)) { return reader.readUTF(); } catch (IOException e) { e.printStackTrace(); return null; } } public static boolean hasUpdates() { String current = RuntimeConstants.BUILD_CODE; if (current == null) { return false; } String nightly = getNightlyBuildCode(); if (nightly == null) { return false; } try { int currentCode = Integer.parseInt(current); int nightlyCode = Integer.parseInt(nightly); return nightlyCode > currentCode; } catch (NumberFormatException e) { e.printStackTrace(); return false; } } } ================================================ FILE: src/main/java/com/mucommander/updates/SelfUpdateUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2021 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.updates; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.process.ExecutionFinishListener; import com.mucommander.process.ExecutorUtils; import ru.trolsoft.utils.FileUtils; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; public class SelfUpdateUtils { private static final String RESTARTER_LOCAL_PATH = "restarter"; public static boolean isUpdaterAvailable() { return getRestarterJarPath() != null; } private static boolean prepareUpdater() { return extractRestarter() && checkRestarter(); } private static String execRestarter(boolean waitComplete, String... args) { System.out.println("restarter exec '" + (args != null && args.length > 0 ? args[0] : "") + "'"); String jarPath = FileUtils.getJarPath(); AbstractFile root = FileFactory.getFile(jarPath); AtomicBoolean done = new AtomicBoolean(false); StringBuffer outStr = new StringBuffer(); ExecutionFinishListener finishListener = (exitCode, output) -> { done.set(true); outStr.append(output); }; try { if (args == null || args.length == 0) { ExecutorUtils.executeAndGetOutput(jarPath + "/" + RESTARTER_LOCAL_PATH, root, finishListener); } else { String[] cmd = new String[args.length+1]; cmd[0] = jarPath + "/" + RESTARTER_LOCAL_PATH; System.arraycopy(args, 0, cmd, 1, args.length); ExecutorUtils.executeAndGetOutput(cmd, root, finishListener); } } catch (IOException | InterruptedException e) { e.printStackTrace(); return null; } if (!waitComplete) { return null; } for (int i = 0; i < 20; i++) { if (done.get()) { break; } try { Thread.sleep(100); } catch (InterruptedException ignore) {} } System.out.println("RESTARTER RESULT '" + outStr + "'"); return outStr.toString(); } public static boolean checkRestarter() { String out = execRestarter(true); return out != null && out.startsWith("restarter"); } private static int getPid() { String out = execRestarter(true, "ppid"); if (out == null) { return -1; } try { return Integer.parseInt(out.trim()); } catch (NumberFormatException e) { e.printStackTrace(); return -1; } } public static String getTcExecutionCommand() { if (!OsFamily.getCurrent().isUnixBased()) { return null; } int pid = getPid(); String out = execRestarter(true, "ps -p " + pid + " -o args"); if (out == null) { return out; } if (out.toLowerCase().startsWith("args")) { return out.substring(4).trim(); } return out; } public static void updateAndRestart() { String cmd = getTcExecutionCommand(); new Thread(() -> execRestarter(false, "update", cmd)).start(); } private static String getRestarterJarPath() { switch (OsFamily.getCurrent()) { case MAC_OS_X: return "/bin/macos/restarter"; case LINUX: return "/bin/linux/restarter"; } return null; } public static boolean extractRestarter() { String jarPath = FileUtils.getJarPath(); String outPath = jarPath + "/" + RESTARTER_LOCAL_PATH; try { FileUtils.copyFileFromJar(getRestarterJarPath(), outPath, true); AbstractFile file = FileFactory.getFile(outPath); if (file == null) { return false; } file.changePermissions(FilePermissions.DEFAULT_EXECUTABLE_PERMISSIONS); return true; } catch (IOException e) { e.printStackTrace(); return false; } } } ================================================ FILE: src/main/java/com/mucommander/updates/VersionChecker.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.updates; import com.mucommander.RuntimeConstants; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.SAXParserFactory; import java.io.InputStream; /** * Retrieves information about the latest release of trolCommander. *

    * The {@link com.mucommander.RuntimeConstants#VERSION_URL} URL contains information * about the latest release of trolCommander:
    * - date of latest release.
    * - latest official version.
    * - where to download the latest version from.
    * This class is used to access that information and compare them with what is known * of the current one, making it possible to notify users of new releases. *

    * Checking for new releases is a fairly straightforward process, and can be done * with a few lines of code: *

     * VersionChecker version;
     *
     * try {
     *     version = VersionChecker.getInstance();
     *     if(version.isNewVersionAvailable())
     *         System.out.println("A new version of trolCommander is available");
     *     else
     *         System.out.println("You've got the latest trolCommander version");
     *    }
     * catch(Exception e) {System.err.println("An error occurred.");}
     * 
    * *

    * trolCommander is considered up to date if:
    * - the {@link com.mucommander.RuntimeConstants#VERSION local version} is * not smaller than the remote one.
    * - the {@link com.mucommander.RuntimeConstants#BUILD_DATE local release date} is * not smaller than the remote one.
    * While comparing release dates seems a bit odd - after all, if a new version is release, * a new version number should be created. However, it's possible to download development * versions of the current release, and those might be updated almost daily. Comparing dates * makes it possible to automate the whole process without having to worry about out version * numbers growing silly. * * @author Maxence Bernard, Nicolas Rinaudo */ @Slf4j public class VersionChecker extends DefaultHandler { // - XML structure ---------------------------------------------------------- // -------------------------------------------------------------------------- /** Root XML element. */ public static final String ROOT_ELEMENT = "trolcommander"; /** Version XML element. */ public static final String VERSION_ELEMENT = "latest_version"; /** Download URL XML element. */ public static final String DOWNLOAD_URL_ELEMENT = "download_url"; /** JAR URL XML element. */ public static final String JAR_URL_ELEMENT = "jar_url"; /** Date XML element. */ public static final String DATE_ELEMENT = "release_date"; // - XML parsing states ----------------------------------------------------- // -------------------------------------------------------------------------- /** Currently parsing the version tag. */ private static final int STATE_VERSION = 1; /** Currently parsing the download URL tag. */ private static final int STATE_DOWNLOAD_URL = 2; /** Currently parsing the download URL tag. */ private static final int STATE_JAR_URL = 3; /** Currently parsing the date tag. */ private static final int STATE_DATE = 4; /** We're not quite sure what we're parsing. */ private static final int STATE_UNKNOWN = 5; /** Remote version number. * -- GETTER -- * Returns the version number of the latest trolCommander release. */ @Getter private String latestVersion; /** Where to download the latest version. * -- GETTER -- * Returns the URL at which the latest version of trolCommander can be downloaded. */ @Getter private String downloadURL; /** URL to the latest JAR file. * -- GETTER -- * Returns the URL to the latest JAR file, null if not available. */ @Getter private String jarURL; /** Remote release date. * The date at which the latest version of trolCommander has been released. *

    * The date format is YYYYMMDD. */ @Getter private String releaseDate; /** Current state the parser is in. */ private int state; /** * Creates a new version checker instance. */ private VersionChecker() {} /** * Retrieves a description of the latest released version of trolCommander. * @return a description of the latest released version of trolCommander. * @exception Exception thrown if any error happens while retrieving the remote version. */ public static VersionChecker getInstance() throws Exception { log.info("Opening connection to {}", RuntimeConstants.VERSION_URL); // Parses the remote XML file using UTF-8 encoding. AbstractFile file = FileFactory.getFile(RuntimeConstants.VERSION_URL); if (file == null) { return null; } VersionChecker instance; try (InputStream in = file.getInputStream()) { try { SAXParserFactory.newInstance().newSAXParser().parse(in, instance = new VersionChecker()); } catch(Exception e) { log.debug("Failed to read version XML file at {}", RuntimeConstants.VERSION_URL, e); throw e; } } // Makes sure we retrieved the information we were looking for. // We're not checking the release date as older version of trolCommander // didn't use it. if (instance.latestVersion == null || instance.latestVersion.isEmpty() || instance.downloadURL == null || instance.downloadURL.isEmpty()) { throw new Exception(); } return instance; } // - Remote version information --------------------------------------------- // -------------------------------------------------------------------------- /** * Checks whether the remote version is newer than the current one. * @return true if the remote version is newer than the current one, false otherwise. */ public boolean isNewVersionAvailable() { String buildDate = RuntimeConstants.BUILD_DATE.replace("-", ""); // If the local and remote versions are the same, compares release dates. if (latestVersion.equals(RuntimeConstants.VERSION.trim().toLowerCase())) { // This ensures backward compatibility - if the remote version file does not contain release date information, ignore it. if (releaseDate.isEmpty()) { return true; } // Checks whether the remote release date is later than the current release date. return releaseDate.compareTo(buildDate) > 0; } return !releaseDate.isEmpty() && releaseDate.compareTo(buildDate) > 0; } /** * Called when the XML document parsing has started. */ @Override public void startDocument() { latestVersion = ""; downloadURL = ""; jarURL = ""; releaseDate = ""; } /** * Notifies the parser of CDATA. */ @Override public void characters(char[] ch, int offset, int length) { if (state == STATE_VERSION) { latestVersion += new String(ch, offset, length); } else if (state == STATE_DOWNLOAD_URL) { downloadURL += new String(ch, offset, length); } else if (state == STATE_JAR_URL) { jarURL += new String(ch, offset, length); } else if (state == STATE_DATE) { releaseDate += new String(ch, offset, length); } } /** * Notifies the parser that a new tag is starting. */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) { // Checks whether we know the tag and updates the current state. switch (qName) { case VERSION_ELEMENT: state = STATE_VERSION; break; case DOWNLOAD_URL_ELEMENT: state = STATE_DOWNLOAD_URL; break; case JAR_URL_ELEMENT: state = STATE_JAR_URL; break; case DATE_ELEMENT: state = STATE_DATE; break; default: state = STATE_UNKNOWN; break; } } /** * Notifies the parser that the current element is finished. */ @Override public void endElement(String uri, String localName, String qName) {state = STATE_UNKNOWN;} /** * Notifies the parser that XML parsing is finished. */ @Override public void endDocument() { // Make sure we're not keep meaningless whitespace characters in the data. latestVersion = latestVersion.toLowerCase().trim(); downloadURL = downloadURL.trim(); jarURL = jarURL.trim(); if (jarURL.isEmpty()) { jarURL = null; } releaseDate = releaseDate.trim(); // Logs the data if in debug mode. log.debug("download URL: {}", downloadURL); log.debug("jar URL: {}", jarURL); log.debug("latestVersion: {}", latestVersion); log.debug("releaseDate: {}", releaseDate); } } ================================================ FILE: src/main/java/com/mucommander/utils/Convert.java ================================================ package com.mucommander.utils; import java.text.DecimalFormat; /** * Created by snouhaud on 25/05/15. */ public class Convert { private static final String[] UNITS = new String[] { "B", "kB", "MB", "GB", "TB" }; public static String readableFileSize(long size) { if (size <= 0) { return "0"; } int digitGroups = (int) (Math.log10(size)/Math.log10(1024)); return new DecimalFormat("#,##0.#").format(size/Math.pow(1024, digitGroups)) + " " + UNITS[digitGroups]; } } ================================================ FILE: src/main/java/com/mucommander/utils/FileIconsCache.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.utils import com.mucommander.commons.file.AbstractFile import com.mucommander.commons.file.FileFactory import com.mucommander.commons.file.FileURL import com.mucommander.ui.icon.FileIcons import ru.trolsoft.macosx.RetinaImageIcon import java.awt.GraphicsEnvironment import java.awt.Image import java.net.MalformedURLException import java.util.* import javax.swing.Icon import javax.swing.ImageIcon import kotlin.concurrent.Volatile /** * Created on 07.01.15. * @author Oleg Trifonov * * Cache of system file icons */ class FileIconsCache { private val icons: MutableMap = HashMap() private val files = LinkedList() /** * Get icon from cache or get it from system and add to cache */ fun getIcon(file: AbstractFile): Icon { val path = file.absolutePath val result = icons[path] if (result != null) { // move record to top files.remove(path) files.addFirst(path) return result } return addIcon(file, path) } fun getIcon(path: String?): Icon { val result = icons[path] if (result != null) { // move record to top files.remove(path) files.addFirst(path) return result } var file: AbstractFile? = null try { file = FileFactory.getFile(FileURL.getFileURL(path)) } catch (e: MalformedURLException) { e.printStackTrace() } return addIcon(file, path) } fun getImageIcon(file: AbstractFile): Image? = when (val icon = getIcon(file)) { is RetinaImageIcon -> icon.getImage() is ImageIcon -> icon.getImage() else -> iconToImage(icon) } /** * Request file icon from OS */ private fun loadIcon(file: AbstractFile?): Icon = FileIcons.getFileIcon(file) /** * Loads icon and adds it to cache * @param path absolute path of file * @return loaded icon */ private fun addIcon(file: AbstractFile?, path: String?): Icon { val icon = loadIcon(file) icons[path] = icon files.addFirst(path) // remove oldest record if the cache is full if (files.size > CACHE_SIZE) { icons.remove(files.removeLast()) } return icon } fun clear() { icons.clear() files.clear() } companion object { /** * Default cache size */ private const val CACHE_SIZE = 1000 @JvmStatic @Volatile var instance: FileIconsCache? = null get() { if (field == null) { synchronized(FileIconsCache::class.java) { if (field == null) { field = FileIconsCache() } } } return field } private set private fun iconToImage(icon: Icon): Image { val w = icon.iconWidth val h = icon.iconHeight val ge = GraphicsEnvironment.getLocalGraphicsEnvironment() val gd = ge.defaultScreenDevice val gc = gd.defaultConfiguration val image = gc.createCompatibleImage(w, h) val g = image.createGraphics() icon.paintIcon(null, g, 0, 0) g.dispose() return image } } } ================================================ FILE: src/main/java/com/mucommander/utils/TcLogging.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.utils; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.StackTraceElementProxy; import ch.qos.logback.core.*; import ch.qos.logback.core.encoder.LayoutWrappingEncoder; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import com.mucommander.ui.dialog.debug.DebugConsoleAppender; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.SimpleDateFormat; import java.util.Date; /** * This class manages logging issues within trolCommander * * @author Maxence Bernard, Arik Hadas */ public class TcLogging { /** * Levels of log printings */ public enum LogLevel { OFF, ERROR, WARNING, INFO, CONFIG, DEBUG, FINER, TRACE; /** * This method maps logback levels to trolCommander log levels * * @param logbackLevel logback log level * @return LogLevel corresponding to the given logback log level */ public static LogLevel valueOf(Level logbackLevel) { return switch (logbackLevel.toInt()) { case Level.OFF_INT -> LogLevel.OFF; case Level.ERROR_INT -> LogLevel.ERROR; case Level.WARN_INT -> LogLevel.WARNING; case Level.INFO_INT -> LogLevel.INFO; case Level.DEBUG_INT -> LogLevel.DEBUG; case Level.TRACE_INT -> LogLevel.TRACE; default -> LogLevel.OFF; }; } /** * This method maps trolCommander log levels to logback levels * * @return logback level corresponding to this LogLevel */ public Level toLogbackLevel() { return switch (this) { case ERROR -> Level.ERROR; case WARNING -> Level.WARN; case INFO, CONFIG -> Level.INFO; case DEBUG, FINER -> Level.DEBUG; case TRACE -> Level.TRACE; default -> Level.OFF; }; } } /** * Appender that writes log printings to the standard console */ private static ConsoleAppender consoleAppender; /** * Appender that writes log printings to the debug console dialog */ private static DebugConsoleAppender debugConsoleAppender; /** * Sets the level of all muCommander loggers. * * @param level the new log level */ private static void updateLogLevel(LogLevel level) { ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); logger.setLevel(level.toLogbackLevel()); } /** * Returns the log level, in mucommander terms, that match the level of a given logback logging event * * @param loggingEvent logback logging event * @return log level, in mucommander terms, that match the level of the given logback logging event */ public static LogLevel getLevel(ILoggingEvent loggingEvent) { return LogLevel.valueOf(loggingEvent.getLevel()); } /** * Returns the current log level used by all org.slf4j loggers. * * @return the current log level used by all org.slf4j loggers. */ public static LogLevel getLogLevel() { return LogLevel.valueOf(TcConfigurations.getPreferences().getVariable(TcPreference.LOG_LEVEL, TcPreferences.DEFAULT_LOG_LEVEL)); } /** * Sets the new log level to be used by all org.slf4j loggers, and persists it in the * application preferences. * * @param level the new log level to be used by all org.slf4j loggers. */ public static void setLogLevel(LogLevel level) { TcConfigurations.getPreferences().setVariable(TcPreference.LOG_LEVEL, level.toString()); updateLogLevel(level); } public static DebugConsoleAppender getDebugConsoleAppender() { return debugConsoleAppender; } public static ConsoleAppender getConsoleAppender() { return consoleAppender; } public static void configureLogging() { // Get root logger ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); // we are not interested in auto-configuration LoggerContext loggerContext = rootLogger.getLoggerContext(); loggerContext.reset(); // Remove default appenders rootLogger.detachAndStopAllAppenders(); // and add custom consoleAppender = createConsoleAppender(loggerContext, new CustomLoggingLayout(OsFamily.getCurrent().isUnixBased())); debugConsoleAppender = createDebugConsoleAppender(loggerContext, new CustomLoggingLayout(false)); if (!"false".equals(System.getProperty("log.stdout"))) { rootLogger.addAppender(consoleAppender); } rootLogger.addAppender(debugConsoleAppender); // Set the log level to the value defined in the configuration updateLogLevel(getLogLevel()); } private static ConsoleAppender createConsoleAppender(LoggerContext loggerContext, Layout layout) { ConsoleAppender consoleAppender = new ConsoleAppender<>(); LayoutWrappingEncoder encoder = new LayoutWrappingEncoder<>(); encoder.setContext(loggerContext); encoder.setLayout(layout); encoder.start(); consoleAppender.setContext(loggerContext); consoleAppender.setEncoder(encoder); consoleAppender.start(); return consoleAppender; } private static DebugConsoleAppender createDebugConsoleAppender(LoggerContext loggerContext, Layout layout) { DebugConsoleAppender debugConsoleAppender = new DebugConsoleAppender(layout); debugConsoleAppender.setContext(loggerContext); debugConsoleAppender.start(); return debugConsoleAppender; } private static class CustomLoggingLayout extends LayoutBase { private final boolean colored; CustomLoggingLayout(boolean colored) { this.colored = colored; } private final static SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private static final String RESET = "\u001B[0m"; private static final String RED = "\u001B[31m"; private static final String YELLOW = "\u001B[33m"; private static final String GREEN = "\u001B[32m"; private static final String MAGENTA = "\u001B[35m"; private static final String CYAN = "\u001B[36m"; private static final String BLUE = "\u001B[34m"; private static final String BOLD_RED = "\u001B[1;31m"; private static final String BOLD_YELLOW = "\u001B[1;33m"; private static final String BOLD_GREEN = "\u001B[1;32m"; private static final String BOLD_WHITE = "\u001B[1;37m"; private static final String BOLD_CYAN = "\u001B[1;36m"; private static final String BOLD_BLUE = "\u001B[1;34m"; private static final String HI_RED = "\u001B[0;91m"; private static final String HI_GREEN = "\u001B[0;92m"; private static final String HI_BLUE = "\u001B[0;94m"; private static final String HI_WHITE = "\u001B[0;97m"; private static final String BOLD_HI_WHITE = "\u001B[1;97m"; public String doLayout(ILoggingEvent event) { StackTraceElement stackTraceElement = event.getCallerData()[0]; StringBuilder sb = new StringBuilder(128); sb.append("["); if (colored) sb.append(HI_BLUE); sb.append(SIMPLE_DATE_FORMAT.format(new Date(event.getTimeStamp()))); if (colored) sb.append(RESET); sb.append("] "); if (colored) sb.append(getColorForLevel(getLevel(event))); sb.append(getLevel(event)); if (colored) { while (sb.length() < 50) { sb.append(" "); } } sb.append(" "); if (colored) sb.append(MAGENTA); sb.append(stackTraceElement.getFileName()); if (colored) sb.append(RESET); sb.append(":"); if (colored) sb.append(HI_WHITE); sb.append(stackTraceElement.getLineNumber()); if (colored) sb.append(RESET); sb.append("#"); if (colored) sb.append(CYAN); sb.append(stackTraceElement.getMethodName()); sb.append(" "); if (colored) { while (sb.length() < 130) { sb.append(" "); } } if (colored) sb.append(BOLD_WHITE); sb.append(event.getFormattedMessage()); if (colored) sb.append(RESET); sb.append(CoreConstants.LINE_SEPARATOR); if (event.getThrowableProxy() != null) { if (colored) { sb.append(HI_RED); } convertThrowable(sb, event.getThrowableProxy(), ""); if (colored) { sb.append(RESET); } } return sb.toString(); } private String getColorForLevel(LogLevel level) { return switch (level) { case ERROR -> BOLD_RED; case WARNING -> BOLD_YELLOW; case INFO, CONFIG -> BOLD_GREEN; case DEBUG -> BOLD_CYAN; case FINER, TRACE -> BOLD_CYAN; default -> ""; }; } private void convertThrowable(StringBuilder sb, ch.qos.logback.classic.spi.IThrowableProxy throwableProxy, String prefix) { sb.append(prefix); sb.append(throwableProxy.getClassName()); sb.append(": "); sb.append(throwableProxy.getMessage()); sb.append(CoreConstants.LINE_SEPARATOR); StackTraceElementProxy[] stackTraceElementProxies = throwableProxy.getStackTraceElementProxyArray(); if (stackTraceElementProxies != null) { for (StackTraceElementProxy step : stackTraceElementProxies) { sb.append(prefix); sb.append("\t"); String sstep = step.toString(); if (colored) { sstep = sstep.replace("(", BOLD_BLUE +"(" + MAGENTA).replace(")", BOLD_BLUE + ")" + HI_RED); } sb.append(sstep); sb.append(CoreConstants.LINE_SEPARATOR); } } ch.qos.logback.classic.spi.IThrowableProxy[] suppressed = throwableProxy.getSuppressed(); if (suppressed != null) { for (ch.qos.logback.classic.spi.IThrowableProxy suppressedThrowable : suppressed) { sb.append(prefix); sb.append("\t... suppressed exception(s):"); sb.append(CoreConstants.LINE_SEPARATOR); convertThrowable(sb, suppressedThrowable, prefix + "\t"); } } ch.qos.logback.classic.spi.IThrowableProxy cause = throwableProxy.getCause(); if (cause != null) { sb.append(prefix); sb.append("\t... caused by:"); sb.append(CoreConstants.LINE_SEPARATOR); convertThrowable(sb, cause, prefix); } } } } ================================================ FILE: src/main/java/com/mucommander/utils/text/CustomDateFormat.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.utils.text; import com.mucommander.commons.conf.ConfigurationEvent; import com.mucommander.commons.conf.ConfigurationListener; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; import com.mucommander.conf.TcPreferences; import java.text.SimpleDateFormat; import java.util.Date; /** * CustomDateFormat allows custom date formatting, according to the date format stored in the preferences. * * @author Maxence Bernard */ public class CustomDateFormat implements ConfigurationListener { /** Singleton instance */ private static CustomDateFormat singleton; /** Custom SimpleDateFormat instance */ private static SimpleDateFormat dateFormat; /** * Creates a new CustomDateFormat instance. */ private CustomDateFormat() {} /** * Forces static fields to be initialized */ public static void init() { // create a singleton instance and keep it as a static member of this class. // Not doing it so would cause the garbage collector to GC it as MuConfiguration holds // weak references of its listeners. singleton = new CustomDateFormat(); TcConfigurations.addPreferencesListener(singleton); dateFormat = createDateFormat(); } /** * Replace the default '/' separator in the given format string, by the given custom separator. * * @return the given format string with '/' separator characters replaced by the given separator character. */ public static String replaceDateSeparator(String dateFormatString, String separator) { if (separator == null || separator.equals("/")) { return dateFormatString; } else { return dateFormatString.replace("/",separator); } } /** * Returns the date format stored in the preferences and used by this class to format dates. * The format of the returned string is the one used by the java.text.SimpleDateFormat class. */ public static String getDateFormatString() { return replaceDateSeparator( TcConfigurations.getPreferences().getVariable(TcPreference.DATE_FORMAT, TcPreferences.DEFAULT_DATE_FORMAT), TcConfigurations.getPreferences().getVariable(TcPreference.DATE_SEPARATOR, TcPreferences.DEFAULT_DATE_SEPARATOR)) + " " + TcConfigurations.getPreferences().getVariable(TcPreference.TIME_FORMAT, TcPreferences.DEFAULT_TIME_FORMAT); } /** * Forces CustomDateFormat to update the date format by looking it up in the preferences. */ public static synchronized void updateDateFormat() { dateFormat = createDateFormat(); } /** * Creates and returns a SimpleDateFormat instance using the date format stored in the preferences. */ private static SimpleDateFormat createDateFormat() { return new SimpleDateFormat(getDateFormatString()); } /** * Formats the given with custom date format and returns a formatted date string. * * @return a formatted string representing the given date. */ private static synchronized String format(Date date) { // Calls to SimpleDateFormat MUST be synchronized otherwise it will start throwing exceptions (verified that!), // that is why this method is synchronized. // Quote from SimpleDateFormat's Javadoc: "Date formats are not synchronized. It is recommended to create // separate format instances for each thread. If multiple threads access a format concurrently, // it must be synchronized externally." return dateFormat.format(date); } /** * Formats the given timestamp with custom date format and returns a formatted date string. * * @return a formatted string representing the given date. */ public static String format(long date) { return format(new Date(date)); } /** * Listens to some configuration variables. */ @Override public void configurationChanged(ConfigurationEvent event) { String var = event.getVariable(); if (var.equals(TcPreferences.TIME_FORMAT) || var.equals(TcPreferences.DATE_FORMAT) || var.equals(TcPreferences.DATE_SEPARATOR)) updateDateFormat(); } } ================================================ FILE: src/main/java/com/mucommander/utils/text/DurationFormat.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.utils.text; /** * DurationFormat formats duration in milliseconds into localized string representations. * * @author Maxence Bernard */ public class DurationFormat { private final static String SECONDS_KEY = "duration.seconds"; private final static String MINUTES_KEY = "duration.minutes"; private final static String HOURS_KEY = "duration.hours"; private final static String DAYS_KEY = "duration.days"; private final static String MONTHS_KEY = "duration.months"; private final static String YEARS_KEY = "duration.years"; private final static String INFINITE = Translator.get("duration.infinite"); private final static int SECONDS_IN_MINUTE = 60; private final static int SECONDS_IN_HOUR = 3600; private final static int SECONDS_IN_DAY = 86400; private final static int SECONDS_IN_MONTH = 2592000; private final static int SECONDS_IN_YEAR = 31104000; private static final String[] KEYS = new String[] { YEARS_KEY, MONTHS_KEY, DAYS_KEY, HOURS_KEY, MINUTES_KEY }; private static final int[] SECONDS = new int[] { SECONDS_IN_YEAR, SECONDS_IN_MONTH, SECONDS_IN_DAY, SECONDS_IN_HOUR, SECONDS_IN_MINUTE }; public static String format(long durationMs) { if (durationMs/1000 > Integer.MAX_VALUE) { return INFINITE; } int remainderSec = Math.round(((float)durationMs)/1000); StringBuilder s = new StringBuilder(); for (int i = 0; i < SECONDS.length; i++) { int n = remainderSec/SECONDS[i]; if (n > 0) { if (!s.isEmpty()) { s.append(' '); } s.append(Translator.get(KEYS[i], ""+n)); remainderSec = remainderSec % SECONDS[i]; } } // Don't add second part if equal to 0, unless this is the only part if (remainderSec > 0 || s.isEmpty()) { if (remainderSec == 0) { // if (s.length() > 0) { // s.delete(0, s.length() - 1); // } s.append('<').append(Translator.get(SECONDS_KEY, "1")); //s = "<" + Translator.get(SECONDS_KEY, "1"); } else { s.append(s.isEmpty() ? "" : " ").append(Translator.get(SECONDS_KEY, "" + remainderSec)); } } return s.toString(); } /** * Returns the infinite symbol string. */ public static String getInfiniteSymbol() { return INFINITE; } } ================================================ FILE: src/main/java/com/mucommander/utils/text/SizeFormat.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.utils.text; import java.text.DecimalFormat; import java.text.NumberFormat; /** * SizeFormat formats byte sizes into localized string representations. * * @author Maxence Bernard. */ public class SizeFormat { /** Bitmask for short digits, e.g. "15" */ public final static int DIGITS_SHORT = 1; /** Bitmask for medium digits, e.g. "15,2" */ public final static int DIGITS_MEDIUM = 2; /** Bitmask for full digits, e.g. "15,204,405" */ public final static int DIGITS_FULL = 4; /** Bitmask for no unit string */ public final static int UNIT_NONE = 0; /** Bitmask for short unit string, e.g. "b" */ public final static int UNIT_SHORT = 8; /** Bitmask for short unit string, e.g. "bytes" */ public final static int UNIT_LONG = 16; /** Byte unit */ public final static int BYTE_UNIT = 0; /** Kilobyte unit */ public final static int KILOBYTE_UNIT = 1; /** Megabyte unit */ public final static int MEGABYTE_UNIT = 2; /** Gigabyte unit */ public final static int GIGABYTE_UNIT = 3; /** Terabyte unit */ public final static int TERABYTE_UNIT = 4; /** Bitmask to add '/s' (per second) to the returned String */ public final static int UNIT_SPEED = 32; /** Bitmask to include a space character to separate the digits and unit parts */ public final static int INCLUDE_SPACE = 64; /** Bitmask to round any size < 1KB to 1KB (except 0 which will be 0 KB) */ public final static int ROUND_TO_KB = 128; /** One kilobyte: 2^10 */ private final static int KB_1 = 1024; /** Ten kilobytes: (2^10)*10 */ private final static int KB_10 = 10240; /** One megabyte: 2^20 */ private final static int MB_1 = 1048576; /** Ten megabytes: (2^20)*10 */ private final static int MB_10 = 10485760; /** One gigabyte: 2^30 */ private final static int GB_1 = 1073741824; /** Ten gigabytes: (2^10)*10 */ private final static long GB_10 = 10737418240L; /** One terabyte: 2^40 */ private final static long TB_1 = 1099511627776L; /** Ten terabytes: (2^40)*10 */ private final static long TB_10 = 10995116277760L; /** DecimalFormat instance to localize thousands separators */ private final static DecimalFormat DECIMAL_FORMAT = (DecimalFormat)NumberFormat.getInstance(); /** Localized decimal separator */ private final static String DECIMAL_SEPARATOR = ""+DECIMAL_FORMAT.getDecimalFormatSymbols().getDecimalSeparator(); private final static String BYTE = Translator.get("unit.byte"); private final static String BYTES = Translator.get("unit.bytes"); private final static String B = Translator.get("unit.bytes_short"); private final static String KB = Translator.get("unit.kb"); private final static String MB = Translator.get("unit.mb"); private final static String GB = Translator.get("unit.gb"); private final static String TB = Translator.get("unit.tb"); private final static String SPEED_KEY = "unit.speed"; /** * Returns a String representation of the given byte size. * * @param size the size to format * @param format format bitmask, see constant fields for allowed values * @return a String representation of the given byte size */ public static String format(long size, int format) { if (size < 0) { return "?"; } String digitsString; String unitString; // Whether the unit string should be long or not boolean unitLong = (format&UNIT_LONG)!=0; // Whether the unit string should be short or not boolean unitShort = (format&UNIT_SHORT)!=0; // Whether the unit string should be short or not boolean noUnit = !(unitLong||unitShort); // Whether the digits string should be short or not boolean digitsShort = (format&DIGITS_SHORT)!=0; // Whether any size < 1024 bytes should be rounded to a kilobyte boolean roundToKb = (format&ROUND_TO_KB)!=0; // size < 1KB if (size < KB_1) { if (roundToKb) { // Note: ROUND_TO_KB must have precedence over DIGITS_FULL digitsString = size == 0 ? "0" : "1"; unitString = noUnit ? "" : KB; } else { digitsString = "" + size; unitString = unitLong?(size<=1?BYTE:BYTES):unitShort?B:""; } } else if ((format & DIGITS_FULL)!=0) { // DecimalFormat localizes thousands separators // Calls to DecimalFormat must be synchronized. // Quote from DecimalFormat's Javadoc: "Decimal formats are generally not synchronized. It is recommended // to create separate format instances for each thread. If multiple threads access a format concurrently, // it must be synchronized externally." synchronized(DECIMAL_FORMAT) { digitsString = DECIMAL_FORMAT.format(size); } unitString = unitLong ? BYTES : unitShort ? B :""; } else { // size < 10KB -> "9.6 KB" if (size < KB_10 && !digitsShort) { int nKB = (int)size/KB_1; digitsString = nKB + DECIMAL_SEPARATOR+(int)((size-nKB*KB_1)/(float)KB_1*10); unitString = noUnit ? "" : KB; } // size < 1MB -> "436 KB" else if (size < MB_1) { digitsString = "" + size/KB_1; unitString = noUnit ? "" : KB; } // size < 10MB -> "4.3 MB" else if(size < MB_10 && !digitsShort) { int nMB = (int)size/MB_1; digitsString = nMB + DECIMAL_SEPARATOR+(int)((size-nMB*MB_1)/(float)MB_1*10); unitString = noUnit? "" : MB; } // size < 1GB -> "548 MB" else if(size < GB_1) { digitsString = "" + size/MB_1; unitString = noUnit ? "" : MB; } // size < 10GB -> "4.8 GB" else if(size < GB_10 && !digitsShort) { long nGB = size / GB_1; digitsString = nGB + DECIMAL_SEPARATOR+(int)((size-nGB*GB_1)/(double)GB_1*10); unitString = noUnit ? "" : GB; } // size < 1TB -> "216 GB" else if(size < TB_1) { digitsString = "" + size/GB_1; unitString = noUnit ? "" : GB; } // size < 10TB -> "4.8 TB" else if(size < TB_10 && !digitsShort) { long nTB = size/TB_1; digitsString = nTB + DECIMAL_SEPARATOR+(int)((size-nTB*TB_1)/(double)TB_1*10); unitString = noUnit ? "" : TB; } else { // Will I live long enough to see files that large ?? digitsString = "" + size/TB_1; unitString = noUnit ? "" : TB; } } // Add localized '/s' to unit string if unit is speed if ((format & UNIT_SPEED) != 0) { unitString = Translator.get(SPEED_KEY, unitString); } return digitsString + ((format&INCLUDE_SPACE) !=0 ? " " : "") + unitString; } public static String getUnitString(int unit, boolean speedUnit) { // TODO use array String unitString; switch(unit) { case BYTE_UNIT: unitString = B; break; case KILOBYTE_UNIT: unitString = KB; break; case MEGABYTE_UNIT: unitString = MB; break; case GIGABYTE_UNIT: unitString = GB; break; case TERABYTE_UNIT: unitString = TB; break; default: return ""; } return speedUnit?Translator.get(SPEED_KEY, unitString):unitString; } /** * Returns the size in bytes of the given byte unit, e.g. 1024 for {@link #KILOBYTE_UNIT}. * * @param unit a unit constant, see constant fields for allowed values * @return the size in bytes of the given byte unit */ public static long getUnitBytes(int unit) { long bytes; // TODO use array switch(unit) { case BYTE_UNIT: bytes = 1; break; case KILOBYTE_UNIT: bytes = KB_1; break; case MEGABYTE_UNIT: bytes = MB_1; break; case GIGABYTE_UNIT: bytes = GB_1; break; case TERABYTE_UNIT: bytes = TB_1; break; default: return 0; } return bytes; } } ================================================ FILE: src/main/java/com/mucommander/utils/text/Translator.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2012 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.mucommander.utils.text; import java.io.*; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.util.*; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.conf.TcConfigurations; import com.mucommander.conf.TcPreference; /** * This class takes care of all text localization issues by loading all text entries from a dictionary file on startup * and translating them into the current language on demand. * *

    All public methods are static to make it easy to call them throughout the application. * *

    See dictionary file for more information about th dictionary file format. * * @author Maxence Bernard, Arik Hadas */ public class Translator { private static Logger logger; /** List of all available languages in the dictionary file */ private static final List availableLanguages = new ArrayList<>(); /** Current language */ private static Locale language; private static ResourceBundle bundle; private Translator() { } static { registerLocale(Locale.forLanguageTag("ar-SA")); registerLocale(Locale.forLanguageTag("be-BY")); registerLocale(Locale.forLanguageTag("ca-ES")); registerLocale(Locale.forLanguageTag("cs-CZ")); registerLocale(Locale.forLanguageTag("da-DA")); registerLocale(Locale.forLanguageTag("de-DE")); registerLocale(Locale.forLanguageTag("en-GB")); registerLocale(Locale.forLanguageTag("en-US")); registerLocale(Locale.forLanguageTag("es-ES")); registerLocale(Locale.forLanguageTag("fr-FR")); registerLocale(Locale.forLanguageTag("hu-HU")); registerLocale(Locale.forLanguageTag("it-IT")); registerLocale(Locale.forLanguageTag("ja-JP")); registerLocale(Locale.forLanguageTag("ko-KR")); registerLocale(Locale.forLanguageTag("no-NO")); registerLocale(Locale.forLanguageTag("nl-NL")); registerLocale(Locale.forLanguageTag("pl-PL")); registerLocale(Locale.forLanguageTag("pt-BR")); registerLocale(Locale.forLanguageTag("ro-RO")); registerLocale(Locale.forLanguageTag("ru-RU")); registerLocale(Locale.forLanguageTag("sk-SK")); registerLocale(Locale.forLanguageTag("sl-SL")); registerLocale(Locale.forLanguageTag("sv-SV")); registerLocale(Locale.forLanguageTag("uk-UA")); registerLocale(Locale.forLanguageTag("zh-CN")); registerLocale(Locale.forLanguageTag("zh-TW")); } private static void registerLocale(Locale locale) { availableLanguages.add(locale); } private static Locale loadLocale() { String localeNameFromConf = TcConfigurations.getPreferences().getVariable(TcPreference.LANGUAGE); if (localeNameFromConf == null) { // language is not set in preferences, use system's language // Try to match language with the system's language, only if the system's language // has values in dictionary, otherwise use default language (English). Locale defaultLocale = Locale.getDefault(); getLogger().info("Language not set in preferences, trying to match system's language ({})", defaultLocale); return defaultLocale; } getLogger().info("Using language set in preferences: " + localeNameFromConf); return switch (localeNameFromConf) { // for backward compatibility case "EN" -> Locale.forLanguageTag("en-US"); case "en_GB" -> Locale.forLanguageTag("en-GB"); case "FR" -> Locale.forLanguageTag("fr-FR"); case "DE" -> Locale.forLanguageTag("de-DE"); case "ES" -> Locale.forLanguageTag("es-ES"); case "CS" -> Locale.forLanguageTag("cs-CZ"); case "zh_CN" -> Locale.forLanguageTag("zh-CN"); case "zh_TW" -> Locale.forLanguageTag("zh-TW"); case "PL" -> Locale.forLanguageTag("pl-PL"); case "HU" -> Locale.forLanguageTag("hu-HU"); case "RU" -> Locale.forLanguageTag("ru-RU"); case "SL" -> Locale.forLanguageTag("sl-SL"); case "RO" -> Locale.forLanguageTag("ro-RO"); case "IT" -> Locale.forLanguageTag("it-IT"); case "KO" -> Locale.forLanguageTag("ko-KR"); case "pt_BR" -> Locale.forLanguageTag("pt-BR"); case "NL" -> Locale.forLanguageTag("nl-NL"); case "SK" -> Locale.forLanguageTag("sk-SK"); case "JA" -> Locale.forLanguageTag("ja-JP"); case "SV" -> Locale.forLanguageTag("sv-SV"); case "DA" -> Locale.forLanguageTag("da-DA"); case "UA" -> Locale.forLanguageTag("uk-UA"); case "AR" -> Locale.forLanguageTag("ar-SA"); case "BE" -> Locale.forLanguageTag("be-BY"); case "NB" -> Locale.forLanguageTag("no-NO"); case "CA" -> Locale.forLanguageTag("ca-ES"); default -> Locale.forLanguageTag(localeNameFromConf); }; } private static Locale matchLocale(Locale loadedLocale) { final String lang = loadedLocale.getLanguage(); for (Locale locale : availableLanguages) { if (lang.equals(loadedLocale.getLanguage()) && Objects.equals(locale.getCountry(), loadedLocale.getCountry())) { getLogger().info("Found exact match (language+country) for locale {}", locale); return locale; } } for (Locale locale : availableLanguages) { if (lang.equals(loadedLocale.getLanguage())) { getLogger().info("Found close match (language) for locale {}", loadedLocale); return locale; } } getLogger().info("Locale {} is not available, falling back to English", loadedLocale); return Locale.ENGLISH; } public static void init() { Locale locale = matchLocale(loadLocale()); // Determines if language is one of the languages declared as available if (availableLanguages.contains(locale)) { // Language is available bundle = ResourceBundle.getBundle("dictionary", locale, new UTF8Control()); getLogger().debug("Language {} is available.", locale); } else { // Language is not available, fall back to default language bundle = ResourceBundle.getBundle("dictionary", new UTF8Control()); getLogger().debug("Language {} is not available, falling back to English", locale); } // Set preferred language in configuration file TcConfigurations.getPreferences().setVariable(TcPreference.LANGUAGE, locale.toLanguageTag()); Translator.language = locale; getLogger().debug("Current language has been set to {}", Translator.language); } /** * Returns the current language as a language code ("EN", "FR", "pt_BR", ...). * * @return lang a language code */ public static String getLanguage() { return language.getLanguage(); } /** * Returns an array of available languages, expressed as language codes ("EN", "FR", "pt_BR"...). * The returned array is sorted by language codes in case insensitive order. * * @return an list of language codes. */ public static List getAvailableLanguages() { return availableLanguages; } /** * Returns true if the given entry's key has a value in the current language. * If the useDefaultLanguage parameter is true, entries that have no value in the * {@link #getLanguage() current language} but one in English will be considered as having * a value (true will be returned). * * @param key key of the requested dictionary entry (case-insensitive) * @param useDefaultLanguage if true, entries that have no value in the {@link #getLanguage() current * language} but one in English will be considered as having a value * @return true if the given key has a corresponding value in the current language. */ public static boolean hasValue(String key, boolean useDefaultLanguage) { return bundle.containsKey(key); } /** * Returns the localized text String for the given key expressed in the current language, or in the default language * if there is no value for the current language. Entry parameters (%1, %2, ...), if any, are replaced by the * specified values. * * @param key key of the requested dictionary entry (case-insensitive) * @param paramValues array of parameters which will be used as values for variables. * @return the localized text String for the given key expressed in the current language */ public static String get(String key, String... paramValues) { String text; try { text = bundle.getString(key); } catch (Exception e) { if (key == null) { return null; } if (key.isEmpty()) { return ""; } text = key; System.out.println("No value for " + key +" in language " + language + ", using English value"); getLogger().debug("No value for {} in language {}, using English value", key, language); } // Replace %1, %2 ... parameters by their value if (paramValues != null) { int pos = -1; for (int i = 0; i * This class is meant for use with {@link com.mucommander.utils.xml.XmlWriter}. * It's used to hold a list of XML attributes that will be passed to one of * the {@link com.mucommander.utils.xml.XmlWriter#startElement(String,XmlAttributes) element opening} methods. * * @author Nicolas Rinaudo, Arik Hadas */ public class XmlAttributes { /** Contains the XML attributes. */ private final Map attributes = new HashMap<>(); /** Contains the XML attribute names in the order they were added */ private final LinkedList names = new LinkedList<>(); /** * Returns the value associated with the specified attribute name. * @param name name of the attribute whose value should be retrieved. * @return the value associated with the specified attribute name if found, null otherwise. */ public String getValue(String name) { return attributes.get(name); } /** * Clears the list of all previously defined attributes. */ public void clear() { names.clear(); attributes.clear(); } /** * Adds the specified attribute to this container. * @param name name of the attribute to whose value should be set. * @param value value to which the attribute should be set. */ public void add(String name, String value) { names.add(name); attributes.put(name, value); } /** * Returns an iterator on the attributes contained by this instance. * @return an iterator on the attributes contained by this instance. */ public Iterator names() { return names.iterator(); } } ================================================ FILE: src/main/java/com/mucommander/utils/xml/XmlWriter.java ================================================ package com.mucommander.utils.xml; import java.io.*; import java.util.Iterator; /** * Used to write pretty-printed XML content. *

    * Application writers should keep in mind that this class does not perform any sort * of coherency check, and will not prevent them from closing elements they haven't opened yet, * or any other thing that would make the XML output invalid. * * @author Nicolas Rinaudo */ public class XmlWriter { /** Number of space characters used for one level of indentation. */ private static final int OFFSET_INCREMENT = 4; /** Identifier for publicly accessible objects. */ private static final String AVAILABILITY_PUBLIC = "PUBLIC"; /** Identifier for system resources. */ private static final String AVAILABILITY_SYSTEM = "SYSTEM"; /** Default output encoding. */ private static final String DEFAULT_ENCODING = "UTF-8"; // - XML standard entities ------------------------------------------- // ------------------------------------------------------------------- /** Forbidden XML characters. */ private final static String[] ENTITIES = new String[] {"&", "\"" , "'", "<", ">"}; /** What to replace forbidden XML characters with. */ private final static String[] ENTITY_REPLACEMENTS = new String[] {"&", """, "'", "<", ">"}; /** Where to write the XML content to. */ private PrintWriter out; /** Current indentation offset. */ private int offset; /** Whether the next element opening or closing operation should be indented. */ private boolean printIndentation; /** * Creates an XmlWriter that will write to the specified file. *

    * This is a convenience constructor and is strictly equivalent to * {@link #XmlWriter(OutputStream,String) XmlWriter}(new FileOutputStream(file), {@link #DEFAULT_ENCODING}). * * @param file where to write XML output to. * @throws FileNotFoundException if file could not be found. * @throws IOException if an I/O error occurs. */ public XmlWriter(File file) throws IOException {this(new FileOutputStream(file));} /** * Creates an XmlWriter that will write to the specified file using the specified encoding. *

    * This is a convenience constructor and is strictly equivalent to * {@link #XmlWriter(OutputStream,String) XmlWriter}(new FileOutputStream(file), encoding). * * @param file where to write XML output to. * @param encoding encoding to use when writing the XML content. * @throws FileNotFoundException if file could not be found. * @throws UnsupportedEncodingException if encoding is not supported. * @throws IOException if an I/O error occurs. */ public XmlWriter(File file, String encoding) throws IOException {this(new FileOutputStream(file), encoding);} /** * Creates an XmlWriter that will write to the specified output stream. *

    * This is a convenience constructor and is strictly equivalent to * {@link #XmlWriter(OutputStream,String) XmlWriter}(stream, {@link #DEFAULT_ENCODING}). * * @param stream where to write XML output to. * @throws IOException if an I/O error occurs. */ public XmlWriter(OutputStream stream) throws IOException {init(new OutputStreamWriter(stream, DEFAULT_ENCODING), DEFAULT_ENCODING);} /** * Creates an XmlWriter that will write to the specified stream using the specified encoding. * @param stream where to write XML output to. * @param encoding encoding to use when writing the XML content. * @throws UnsupportedEncodingException if encoding is not supported. * @throws IOException if an I/O error occurs. */ public XmlWriter(OutputStream stream, String encoding) throws IOException {init(new OutputStreamWriter(stream, encoding), encoding);} private void init(Writer writer, String encoding) throws IOException { out = new PrintWriter(writer, true); out.print(""); if (out.checkError()) { throw new IOException(); } } // - Element operations -------------------------------------------------- // ------------------------------------------------------------------- /** * Writes the document type declaration of the XML file. *

    * For the generated XML content to be valid, application writers should make sure that this * is the very first method they call. This class doesn't ensure coherency and won't * complain if the DOCTYPE statement is the last in the file. *

    * Both description and url can be set to null. * If so, they will just be ignored. * * @param topElement label of the top element in the XML file. * @param availability availability of the document (expected to be either {@link #AVAILABILITY_PUBLIC} * or {@link #AVAILABILITY_SYSTEM}, but this is not enforced). * @param description description of the file (see DOCTYPE specifications for more information). * @param url URL at which the DTD of the XML file can be downloaded. * @throws IOException if an I/O error occurs. */ public void writeDocType(String topElement, String availability, String description, String url) throws IOException { // Writes the compulsory bits. out.print("'); if (out.checkError()) { throw new IOException(); } } /** * Writes an element opening sequence. *

    * This is a convenience method and is strictly equivalent to calling * {@link #startElement(String,boolean) startElement}(name, false). * * @param name name of the element to open. * @throws IOException if an I/O error occurs. * @see #startElement(String,XmlAttributes) * @see #writeStandAloneElement(String) * @see #writeStandAloneElement(String,XmlAttributes) */ public void startElement(String name) throws IOException {startElement(name, false, null, false);} /** * Writes an element opening sequence. *

    * Elements opened using this method will not have any attribute, and will * need to be closed using an {@link #endElement(String) endElement} call. * * @param name name of the element to open. * @param lineBreak if true, a line break will be printed after the element declaration. * @throws IOException if an I/O error occurs. * @see #startElement(String,XmlAttributes) * @see #writeStandAloneElement(String) * @see #writeStandAloneElement(String,XmlAttributes) */ public void startElement(String name, boolean lineBreak) throws IOException {startElement(name, false, null, lineBreak);} /** * Writes a stand-alone element. *

    * Elements opened using this method will not have any attributes, and will be * closed immediately. *

    * A line break will always be printed after a stand-alone element. * * @param name name of the element to write. * @throws IOException if an I/O error occurs. * @see #startElement(String,XmlAttributes) * @see #startElement(String) * @see #writeStandAloneElement(String) */ public void writeStandAloneElement(String name) throws IOException {startElement(name, true, null, true);} /** * Writes a one-line comment. * * @param comment comment description. * @throws IOException if an I/O error occurs. */ public void writeCommentLine(String comment) throws IOException { out.print(""); println(); } /** * Writes an element opening sequence. *

    * This is a convenience method and is strictly equivalent to calling * {@link #startElement(String,XmlAttributes,boolean) startElement}(name, attributes, false). * * @param name name of the element to open. * @throws IOException if an I/O error occurs. * @param attributes attributes that this element will have. * @see #startElement(String) * @see #writeStandAloneElement(String) * @see #writeStandAloneElement(String,XmlAttributes) */ public void startElement(String name, XmlAttributes attributes) throws IOException {startElement(name, false, attributes, false);} /** * Writes an element opening sequence. *

    * Elements opened using this method will need to be closed using an {@link #endElement(String) endElement} call. * * @param name name of the element to open. * @param attributes attributes that this element will have. * @param lineBreak if true, a line break will be printed after the element declaration. * @throws IOException if an I/O error occurs. * @see #startElement(String) * @see #writeStandAloneElement(String) * @see #writeStandAloneElement(String,XmlAttributes) */ public void startElement(String name, XmlAttributes attributes, boolean lineBreak) throws IOException {startElement(name, false, attributes, lineBreak);} /** * Writes a stand-alone element. *

    * Elements opened using this method will not need to be closed *

    * A line break will always be printed after a stand-alone element. * * @param name name of the element to write. * @param attributes attributes that this element will be closed immediately. * @throws IOException if an I/O error occurs. * @see #startElement(String) * @see #startElement(String,XmlAttributes) * @see #writeStandAloneElement(String) */ public void writeStandAloneElement(String name, XmlAttributes attributes) throws IOException {startElement(name, true, attributes, true);} /** * Writes an element opening sequence. * @param name name of the element to open. * @param isStandAlone whether this element should be closed immediately. * @param attributes XML attributes for this element. * @throws IOException if an I/O error occurs. */ private void startElement(String name, boolean isStandAlone, XmlAttributes attributes, boolean lineBreak) throws IOException { // Prints indentation if necessary. indent(); // Opens the element. out.print('<'); out.print(name); // Writes attributes, if any. if (attributes != null) { Iterator names; String attName; names = attributes.names(); while(names.hasNext()) { attName = names.next(); out.print(' '); out.print(attName); out.print("=\""); out.print(escape(attributes.getValue(attName))); out.print("\""); } } // Closes the element if necessary. if (isStandAlone) { out.print('/'); } else { offset += OFFSET_INCREMENT; } // Finishes the element opening sequence. out.print('>'); // Stand-alone elements are followed by a line break. if (lineBreak) { println(); } if(out.checkError()) throw new IOException(); } /** * Writes an element closing sequence. * @param name name of the element to close. * @throws IOException if an I/O error occurs. */ public void endElement(String name) throws IOException { // Updates the indentation, and prints it if necessary. offset -= OFFSET_INCREMENT; indent(); // Writes the element closing sequence. out.print("'); println(); if (out.checkError()) throw new IOException(); } // - CDATA handling -------------------------------------------------- // ------------------------------------------------------------------- /** * Writes the specified CDATA to the XML stream. * @param cdata content to write to the XML stream. * @throws IOException if an I/O error occurs. */ public void writeCData(String cdata) throws IOException { indent(); out.print(escape(cdata)); if (out.checkError()) { throw new IOException(); } } /** * Escapes XML content, replacing special characters by their proper value. * @param data data to escape. * @return the escaped content. */ public String escape(String data) { for (int i = 0; i < ENTITIES.length; i++) { int position = 0; while ((position = data.indexOf(ENTITIES[i], position)) >= 0) { data = data.substring(0, position) + ENTITY_REPLACEMENTS[i] + (position == data.length() - 1 ? "" : data.substring(position + 1)); position = position + ENTITY_REPLACEMENTS[i].length(); } } return data; } // - Indentation handling -------------------------------------------- // ------------------------------------------------------------------- /** * Prints a line break. * @throws IOException if an I/O error occurs. */ public void println() throws IOException { out.println(); printIndentation = true; if (out.checkError()) { throw new IOException(); } } /** * If necessary, prints indentation. * @throws IOException if an I/O error occurs. */ private void indent() throws IOException { if (printIndentation) { for(int i = 0; i < offset; i++) { out.print(' '); } printIndentation = false; } if (out.checkError()) { throw new IOException(); } } /** * Closes the XML stream. * @throws IOException if an I/O error occurs. */ public void close() throws IOException { out.close(); } } ================================================ FILE: src/main/java/com/mucommander/utils/xml/package.html ================================================ Provides classes to write formatted XML files. ================================================ FILE: src/main/java/com/sshtools/sftp/SftpFileInputStreamEx.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.sshtools.sftp; import com.sshtools.ssh.SshException; /** * Modified SftpFileInputStream with public getPosition() and setPosition() * * @author Oleg Trifonov * Created on 26/04/16. */ public class SftpFileInputStreamEx extends SftpFileInputStream { public SftpFileInputStreamEx(SftpFile file) throws SftpStatusException, SshException { super(file); } public SftpFileInputStreamEx(SftpFile file, long position) throws SftpStatusException, SshException { super(file, position); } public long getPosition() { return position; } public void setPosition(long position) { this.position = position; } } ================================================ FILE: src/main/java/com/sshtools/sftp/SftpFileOutputStreamEx.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.sshtools.sftp; import com.sshtools.ssh.SshException; /** * Modified SftpFileOutputStream with position argument in constructor * * @author Oleg Trifonov * Created on 26/04/16. */ public class SftpFileOutputStreamEx extends SftpFileOutputStream { protected SftpFileOutputStreamEx(SftpFile file, long pos) throws SftpStatusException, SshException { super(file); this.position = pos; } } ================================================ FILE: src/main/java/com/sun/jna/platform/mac/XAttrUtils.java ================================================ /* * This file is part of muCommander, http://www.mucommander.com * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package com.sun.jna.platform.mac; import com.sun.jna.Memory; /** * Utility methods that complement the JNA library. * * @author Arik Hadas */ public class XAttrUtils { /** The key of the spotlight comment section */ public static final String COMMENT = "com.apple.metadata:kMDItemFinderComment"; /** * Reads the content of section in file's metadata. * * @param path Path to a file * @param key Key of a metadata section * @return The value associated with the given key in the file's metadata */ public static byte[] read(String path, String key) { long bufferLength = XAttr.INSTANCE.getxattr(path, key, null, 0, 0, 0); if (bufferLength <= 0) { return null; } Memory valueBuffer = new Memory(bufferLength); valueBuffer.clear(); long valueLength = XAttr.INSTANCE.getxattr(path, key, valueBuffer, bufferLength, 0, 0); if (valueLength < 0) { return null; } return valueBuffer.getByteArray(0, (int)valueLength); } /** * Writes the content to section in file's metadata. * * @param path Path to a file * @param key Key of a metadata section * @param value Content to write to the metadata section */ public static void write(String path, String key, byte[] value) { Memory valueBuffer = new Memory(value.length); valueBuffer.write(0, value, 0, value.length); XAttr.INSTANCE.setxattr(path, key, valueBuffer, valueBuffer.size(), 0, 0); } } ================================================ FILE: src/main/java/org/fife/ui/rtextarea/GutterEx.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2014-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.fife.ui.rtextarea; /** * Child of org.fife.ui.rtextarea.Gutter with some public methods * * @author Oleg Trifonov * Created on 19/04/16. */ public class GutterEx extends Gutter { public GutterEx(RTextArea textArea) { super(textArea); } public void setIconRowHeaderEnabled(boolean enabled) { super.setIconRowHeaderEnabled(enabled); } public void setLineNumbersEnabled(boolean enabled) { super.setLineNumbersEnabled(enabled); } public void setTextArea(RTextArea textArea) { super.setTextArea(textArea); } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/Calculator.kt ================================================ package ru.trolsoft.calculator import ru.trolsoft.calculator.eval.CustomOperator import ru.trolsoft.calculator.eval.ExpressionBuilder import kotlin.math.roundToInt import kotlin.math.roundToLong class Calculator { fun calculate(expression: String): Double = if (expression.isBlank()) { return 0.0 } else { ExpressionBuilder(expression).apply { withOperations(OPERATORS) withVariable("pi", Math.PI) withVariable("e", Math.E) }.build().calculate() } companion object { private fun opLL(values: DoubleArray, operation: (Long, Long) -> Long): Double = operation(values[0].roundToLong(), values[1].roundToLong()).toDouble() private fun opLI(values: DoubleArray, operation: (Long, Int) -> Long): Double = operation(values[0].roundToLong(), values[1].roundToInt()).toDouble() private val OP_SHL = object : CustomOperator("<<", true, 10, 2) { override fun applyOperation(values: DoubleArray) = opLI(values) { a: Long, b: Int -> a shl b } } private val OP_SHR = object : CustomOperator(">>", true, 11, 2) { override fun applyOperation(values: DoubleArray) = opLI(values) { a: Long, b: Int -> a shr b } } private val OP_AND = object : CustomOperator("&", true, 8, 2) { override fun applyOperation(values: DoubleArray) = opLL(values) { a: Long, b: Long -> a and b } } private val OP_OR = object : CustomOperator("|", true, 6, 2) { override fun applyOperation(values: DoubleArray) = opLL(values) { a: Long, b: Long -> a or b } } private val OP_NOT = object : CustomOperator("~", true, 15, 1) { override fun applyOperation(values: DoubleArray): Double { val arg = values[0].roundToLong() val mask = if (arg < 0xff) { 0xffL } else if (arg < 0xffff) { 0xffffL } else if (arg < 0xffffffff) { 0xffffffffL } else { (1L shl 32) - 1L } return (arg.inv() and mask).toDouble() } } private val OP_XOR = object : CustomOperator("^^", true, 7, 2) { override fun applyOperation(values: DoubleArray) = opLL(values) { a: Long, b: Long -> a xor b } } val OPERATORS = listOf( OP_SHL, OP_SHR, OP_AND, OP_OR, OP_NOT, OP_XOR ) } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/CalculatorDialog.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.calculator import com.mucommander.cache.TextHistory import com.mucommander.ui.dialog.FocusDialog import com.mucommander.ui.helper.MnemonicHelper import com.mucommander.ui.layout.XAlignedComponentPanel import com.mucommander.ui.layout.XBoxPanel import com.mucommander.ui.layout.YBoxPanel import org.slf4j.Logger import org.slf4j.LoggerFactory import ru.trolsoft.utils.StrUtils import java.awt.* import java.awt.datatransfer.StringSelection import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.awt.event.KeyEvent import java.awt.event.KeyListener import java.text.DecimalFormat import java.util.* import javax.swing.JButton import javax.swing.JLabel import javax.swing.JPanel import javax.swing.JTextField import kotlin.math.roundToLong /** * Created on 04/06/14. * @author Oleg Trifonov */ class CalculatorDialog( owner: Frame ) : FocusDialog(owner, i18n("calculator.calculator"), null), ActionListener, KeyListener { private val calculator = Calculator() private val cbExpression: HistoryComboBox private val edtDec: JTextField private val edtHex: JTextField private val edtBin: JTextField private val edtOct: JTextField private val edtExp: JTextField private val btnDec: JButton private val btnHex: JButton private val btnBin: JButton private val btnOct: JButton private val btnExp: JButton private val btnClose: JButton private val lblError: JLabel init { val contentPane = getContentPane() val yPanel = YBoxPanel(10) // Text fields panel val compPanel: XAlignedComponentPanel = object : XAlignedComponentPanel() { override fun add(comp: Component, constraints: Any) { (constraints as GridBagConstraints).fill = GridBagConstraints.HORIZONTAL super.add(comp, constraints) } } val calcHistory = TextHistory.getInstance().getList(TextHistory.Type.CALCULATOR) cbExpression = HistoryComboBox(this, calcHistory).apply { addActionListener(this@CalculatorDialog) getEditor().editorComponent.addKeyListener(this@CalculatorDialog) } compPanel.addRow(i18n("calculator.expression") + ":", cbExpression, 5) lblError = JLabel() compPanel.addRow("", lblError, 10) val buttonFont = Font.getFont(Font.MONOSPACED) btnDec = JButton("DEC").apply { addActionListener(this@CalculatorDialog) setFont(buttonFont) } edtDec = JTextField().apply { isEditable = false } compPanel.addRow(btnDec, edtDec, 0) btnHex = JButton("HEX").apply { addActionListener(this@CalculatorDialog) setFont(buttonFont) } edtHex = JTextField().apply { isEditable = false } compPanel.addRow(btnHex, edtHex, 0) btnBin = JButton("BIN").apply { addActionListener(this@CalculatorDialog) setFont(buttonFont) } edtBin = JTextField().apply { isEditable = false } compPanel.addRow(btnBin, edtBin, 0) btnOct = JButton("OCT").apply { addActionListener(this@CalculatorDialog) setFont(buttonFont) } edtOct = JTextField().apply { isEditable = false } compPanel.addRow(btnOct, edtOct, 0) btnExp = JButton("EXP").apply { addActionListener(this@CalculatorDialog) setFont(buttonFont) } edtExp = JTextField().apply { isEditable = false } compPanel.addRow(btnExp, edtExp, 0) val mnemonicHelper = MnemonicHelper() val buttonsPanel = XBoxPanel() val buttonGroupPanel = JPanel(FlowLayout(FlowLayout.RIGHT)) btnClose = JButton(i18n("close")).apply { addActionListener(this@CalculatorDialog) setMnemonic(mnemonicHelper.getMnemonic(this)) } buttonGroupPanel.add(btnClose) buttonsPanel.add(buttonGroupPanel) contentPane.add(buttonsPanel, BorderLayout.SOUTH) contentPane.add(yPanel, BorderLayout.NORTH) yPanel.add(compPanel) minimumSize = MIN_DIMENSION setModal(false) fixHeight() } private fun calculateAndShow(): Boolean { val expression = this.getExpression() ?: return false var success: Boolean try { val res = evaluate(expression) TextHistory.getInstance().add(TextHistory.Type.CALCULATOR, expression, false) cbExpression.addToHistory(expression) showResult(res) success = true } catch (e: Exception) { log.error("Calculation failed", e) clearResultFields() success = false } enableControls(success) lblError.setText(if (success) "" else i18n("calculator.error")) return success } private fun showResult(res: Double) { val valLong = res.roundToLong() val isDecimal = valLong.toDouble() == res edtDec.text = if (isDecimal) valLong.toString() else FORMAT_DEC.format(res).replace(',', '.') edtHex.text = java.lang.Long.toHexString(valLong) edtOct.text = java.lang.Long.toOctalString(valLong) edtBin.text = java.lang.Long.toBinaryString(valLong) edtExp.text = formatExp(res) } private fun getExpression(): String? { val selectedItem = cbExpression.selectedItem ?: return null val result = selectedItem.toString().trim() return StrUtils.removeUtfMarker(result).trim() } private fun enableControls(enable: Boolean) { edtDec.setEnabled(enable) edtHex.setEnabled(enable) edtOct.setEnabled(enable) edtBin.setEnabled(enable) edtOct.setEnabled(enable) edtExp.setEnabled(enable) btnDec.setEnabled(enable) btnHex.setEnabled(enable) btnOct.setEnabled(enable) btnBin.setEnabled(enable) btnOct.setEnabled(enable) btnExp.setEnabled(enable) } private fun clearResultFields() { edtDec.text = "" edtHex.text = "" edtOct.text = "" edtBin.text = "" edtExp.text = "" } @Throws(Exception::class) private fun evaluate(expression: String): Double = calculator.calculate(expression) private fun formatExp(v: Double): String { var result = FORMAT_EXP.format(v).uppercase(Locale.getDefault()) val index = result.indexOf('E') if (index > 0 && result[index + 1] != '-') { result = result.substring(0, index) + '+' + result.substring(index) } return result } override fun actionPerformed(e: ActionEvent) { val src = e.getSource() if (src === cbExpression) { calculateAndShow() } else if (src === btnClose) { cancel() } else if (src === btnDec) { toClipboard(edtDec.getText()) } else if (src === btnHex) { toClipboard(edtHex.getText()) } else if (src === btnBin) { toClipboard(edtBin.getText()) } else if (src === btnOct) { toClipboard(edtOct.getText()) } else if (src === btnExp) { toClipboard(edtExp.getText()) } } private fun toClipboard(s: String?) { val data = StringSelection(s) Toolkit.getDefaultToolkit().systemClipboard.setContents(data, data) } override fun saveState() { super.saveState() TextHistory.getInstance().save(TextHistory.Type.CALCULATOR) } override fun keyTyped(e: KeyEvent) {} override fun keyPressed(e: KeyEvent) {} override fun keyReleased(e: KeyEvent) { if (e.getKeyCode() == KeyEvent.VK_ENTER && (e.modifiersEx and (KeyEvent.CTRL_DOWN_MASK or KeyEvent.META_DOWN_MASK)) != 0) { if (calculateAndShow()) { cbExpression.setSelectedItem(edtDec.getText()) } } } companion object { private val log: Logger = LoggerFactory.getLogger(CalculatorDialog::class.java) private val MIN_DIMENSION = Dimension(520, 300) private val FORMAT_DEC = DecimalFormat("#.##################") private val FORMAT_EXP = DecimalFormat("0.00000000000000E0000") } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/HistoryComboBox.kt ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.calculator import com.mucommander.ui.combobox.TcComboBox import com.mucommander.ui.dialog.FocusDialog import java.awt.event.KeyAdapter import java.awt.event.KeyEvent /** * @author Oleg Trifonov * Created on 06/06/14. */ class HistoryComboBox( private val parent: FocusDialog, values: List ) : TcComboBox(values.toTypedArray()) { init { setEditable(true) if (!values.isEmpty()) { setSelectedItem(values.first()) getEditor().selectAll() } val keyAdapter: KeyAdapter = object : KeyAdapter() { override fun keyPressed(e: KeyEvent) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { if (isPopupVisible) { hidePopup() e.consume() } else { this@HistoryComboBox.parent.cancel() } } } } getEditor().editorComponent.addKeyListener(keyAdapter) } fun addToHistory(s: String) { for (i in 0..< itemCount) { val item = getItemAt(i) if (item.equals(s, ignoreCase = true)) { removeItem(item) break } } insertItemAt(s, 0) setSelectedIndex(0) } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/Calculable.kt ================================================ package ru.trolsoft.calculator.eval /** * This is the basic result class of the exp4j [ExpressionBuilder] * * @author frank asseg */ interface Calculable { /** * calculate the result of the expression * * @return the result of the calculation */ fun calculate(): Double = 0.0 /** * calculate the result of the expression * * @param variableValues * the values of the variable. The values must be in the same order as the declaration of variables in * the [ExpressionBuilder] used to construct this [Calculable] instance * @return the result of the calculation */ fun calculate(vararg variableValues: Double): Double /** * return the expression in reverse polish postfix notation * * @return the expression used to construct this [Calculable] */ val expression: String? /** * set a variable value for the calculation * * @param name * the variable name * @param value * the value of the variable */ fun setVariable(name: String?, value: Double) } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/CommandlineInterpreter.java ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval; import ru.trolsoft.calculator.eval.exceptions.UnknownFunctionException; import ru.trolsoft.calculator.eval.exceptions.UnparsableExpressionException; /** * Simple commandline interpreter for mathematical expressions the interpreter takes a mathematical expressions as a * {@link String} argument, evaluates it and prints out the result. * * *

     * java de.congrace.exp4j.CommandlineInterpreter "2 * log(2.2223) - ((2-3.221) * 14.232^2)"
     * > 248.91042049521056
     * 
    * * @author fas@congrace.de * */ public class CommandlineInterpreter { private static void calculateExpression(String string) { try { System.out.println(new ExpressionBuilder(string).build().calculate()); } catch (UnparsableExpressionException | UnknownFunctionException e) { e.printStackTrace(); } } public static void main(String[] args) { if (args.length != 1) { printUsage(); } else { calculateExpression(args[0]); } } private static void printUsage() { System.err.println("Commandline Expression Parser\n\n" + "Example: " + "\n" + "java -jar exp4j.jar \"2.12 * log(23) * (12 - 4)\"\n\n" + "written by fas@congrace.de"); } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/CustomFunction.java ================================================ package ru.trolsoft.calculator.eval; import ru.trolsoft.calculator.eval.exceptions.InvalidCustomFunctionException; /** * This classed is used to create custom functions for exp4j
    * * Example
    *
    {@code
     * CustomFunction fooFunc = new CustomFunction("foo") {
     * 		public double applyFunction(double value) {
     * 			return value*Math.E;
     * 		}
     * };
     * double varX=12d;
     * Calculable calc = new ExpressionBuilder("foo(x)").withCustomFunction(fooFunc).withVariable("x",varX).build();
     * assertTrue(calc.calculate() == Math.E * varX);
     * }
    * * @author frank asseg * */ public abstract class CustomFunction { public final int argc; public final String name; /** * create a new single value input CustomFunction with a set name * * @param name the name of the function (e.g. foo) */ CustomFunction(String name) throws InvalidCustomFunctionException { this.argc = 1; this.name = name; int firstChar = name.charAt(0); if ((firstChar < 'A' || firstChar > 'Z') && (firstChar < 'a' || firstChar > 'z')) { throw new InvalidCustomFunctionException("functions have to start with a lowercase or uppercase character"); } } /** * create a new single value input CustomFunction with a set name * * @param name the name of the function (e.g. foo) */ protected CustomFunction(String name, int argumentCount) { this.argc = argumentCount; this.name = name; } public int getArgumentCount(){ return argc; } public abstract double applyFunction(double... args); } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/CustomOperator.java ================================================ package ru.trolsoft.calculator.eval; /** * This class is used to create custom operators for use in expressions
    * The applyOperation(double[] values) will have to be implemented by users of this class.
    * Example
    *
    {@code
     *      CustomOperator greaterEq = new CustomOperator(">=", true, 4, 2) {
     *            double applyOperation(double[] values) {
     *            	if (values[0] >= values[1]) {
     *            		return 1d;
     *            	} else {
     *            		return 0d;
     *            	}
     *            }
     *        };
     *       Calculable calc = new ExpressionBuilder("1>=2").withOperation(greaterEq).build();
     *       assertTrue(0d == calc.calculate());
     * }
    When constructing {@link CustomOperator} special attention has to be given to the precedence of the * operation. see http://en.wikipedia.org/wiki/Order_of_operations. The precedence values for the builtin operators are * as follows:
    * Addition and Subtraction (+,-) have precedence 1
    * Division Multiplication, and Modulo (/,*,%) have precedence 3
    * Exponentiation (^) has precedence 5
    * Unary minus and plus (+1,-1) have precedence 7 * * @author frank asseg * */ public abstract class CustomOperator { public final boolean leftAssociative; public final String symbol; public final int precedence; public final int operandCount; /** * create a new {@link CustomOperator} for two operands * * @param symbol * the symbol to be used in expressions to identify this operation * @param leftAssociative * true is the operation is left associative * @param precedence * the precedence of the operation */ CustomOperator(final String symbol, final boolean leftAssociative, final int precedence) { super(); this.leftAssociative = leftAssociative; this.symbol = symbol; this.precedence = precedence; this.operandCount = 2; } /** * create a new {@link CustomOperator} * * @param symbol * the symbol to be used in expressions to identify this operation * @param leftAssociative * true is the operation is left associative * @param precedence * the precedence of the operation * @param operandCount * the number of operands of the operation. A value of 1 means the operation takes one operand. Any other * value means the operation takes 2 arguments. */ protected CustomOperator(final String symbol, final boolean leftAssociative, final int precedence, final int operandCount) { super(); this.leftAssociative = leftAssociative; this.symbol = symbol; this.precedence = precedence; this.operandCount = operandCount == 1 ? 1 : 2; } /** * create a left associative {@link CustomOperator} with precedence value of 1 that uses two operands * * @param symbol * the {@link String} to use a symbol for this operation */ CustomOperator(final String symbol) { super(); this.leftAssociative = true; this.symbol = symbol; this.precedence = 1; this.operandCount = 2; } /** * create a left associative {@link CustomOperator} for two operands * * @param symbol the {@link String} to use a symbol for this operation * @param precedence the precedence of the operation */ CustomOperator(final String symbol, final int precedence) { super(); this.leftAssociative = true; this.symbol = symbol; this.precedence = precedence; this.operandCount = 2; } /** * Apply the custom operation on the two operands and return the result as an double An example implementation for a * multiplication could look like this: * *
    	 * {@code
    	 *       double applyOperation(double[] values) {
    	 *           return values[0]*values[1];
    	 *       }
    	 * }
    * * @param values * the operands for the operation. If the {@link CustomOperator} uses only one operand such as a * factorial the operation has to be applied to the first element of the values array. If the * {@link CustomOperator} uses two operands the operation has to be applied to the first two items in the * values array, with special care given to the operator associativity. The operand to the left of the * symbol is the first element in the array while the operand to the right is the second element of the * array. * @return the result of the operation */ public abstract double applyOperation(double[] values); } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/ExpressionBuilder.java ================================================ package ru.trolsoft.calculator.eval; import ru.trolsoft.calculator.eval.exceptions.InvalidCustomFunctionException; import ru.trolsoft.calculator.eval.exceptions.UnknownFunctionException; import ru.trolsoft.calculator.eval.exceptions.UnparsableExpressionException; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static ru.trolsoft.calculator.eval.RPNConverterKt.toRPNExpression; /** * This is a Builder implementation for the exp4j API used to create a Calculable instance for the user * * @author frank asseg * */ public class ExpressionBuilder { /** * Property name for unary precedence choice. You can set System.getProperty(PROPERTY_UNARY_HIGH_PRECEDENCE,"false") * in order to change evaluation from an expression like "-3^2" from "(-3)^2" to "-(3^2)" */ public static final String PROPERTY_UNARY_HIGH_PRECEDENCE = "exp4j.unary.precedence.high"; private final Map variables = new LinkedHashMap<>(); private final Map customFunctions; private final Map builtInOperators; private final Map customOperators = new HashMap<>(); private final List validOperatorSymbols; private final boolean highUnaryPrecedence; private String expression; /** * create a new ExpressionBuilder * * @param expression * the expression to evaluate */ public ExpressionBuilder(String expression) { if (expression.trim().isEmpty()) { throw new IllegalArgumentException("Expression can not be empty!."); } this.expression = expression; highUnaryPrecedence = System.getProperty(PROPERTY_UNARY_HIGH_PRECEDENCE) == null || !System.getProperty(PROPERTY_UNARY_HIGH_PRECEDENCE).equals("false"); customFunctions = getBuiltinFunctions(); builtInOperators = getBuiltinOperators(); validOperatorSymbols = getValidOperators(); } private List getValidOperators() { return Arrays.asList('!', '#', '§', '$', '&', ';', ':', '~', '<', '>', '|', '=', '^'); } private Map getBuiltinOperators() { CustomOperator add = new CustomOperator("+") { @Override public double applyOperation(double[] values) { return values[0] + values[1]; } }; CustomOperator sub = new CustomOperator("-") { @Override public double applyOperation(double[] values) { return values[0] - values[1]; } }; CustomOperator div = new CustomOperator("/", 3) { @Override public double applyOperation(double[] values) { if (values[1] == 0d) { throw new ArithmeticException("Division by zero!"); } return values[0] / values[1]; } }; CustomOperator mul = new CustomOperator("*", 3) { @Override public double applyOperation(double[] values) { return values[0] * values[1]; } }; CustomOperator mod = new CustomOperator("%", true, 3) { @Override public double applyOperation(double[] values) { if (values[1] == 0d){ throw new ArithmeticException("Division by zero!"); } return values[0] % values[1]; } }; CustomOperator umin = new CustomOperator("'", false, this.highUnaryPrecedence ? 7 : 5, 1) { @Override public double applyOperation(double[] values) { return -values[0]; } }; CustomOperator pow = new CustomOperator("^", false, 5, 2) { @Override public double applyOperation(double[] values) { return Math.pow(values[0], values[1]); } }; Map operations = new HashMap<>(); operations.put("+", add); operations.put("-", sub); operations.put("*", mul); operations.put("/", div); operations.put("'", umin); operations.put("^", pow); operations.put("%", mod); return operations; } private Map getBuiltinFunctions() { try { CustomFunction abs = new CustomFunction("abs") { @Override public double applyFunction(double... args) { return Math.abs(args[0]); } }; CustomFunction acos = new CustomFunction("acos") { @Override public double applyFunction(double... args) { return Math.acos(args[0]); } }; CustomFunction asin = new CustomFunction("asin") { @Override public double applyFunction(double... args) { return Math.asin(args[0]); } }; CustomFunction atan = new CustomFunction("atan") { @Override public double applyFunction(double... args) { return Math.atan(args[0]); } }; CustomFunction cbrt = new CustomFunction("cbrt") { @Override public double applyFunction(double... args) { return Math.cbrt(args[0]); } }; CustomFunction ceil = new CustomFunction("ceil") { @Override public double applyFunction(double... args) { return Math.ceil(args[0]); } }; CustomFunction cos = new CustomFunction("cos") { @Override public double applyFunction(double... args) { return Math.cos(args[0]); } }; CustomFunction cosh = new CustomFunction("cosh") { @Override public double applyFunction(double... args) { return Math.cosh(args[0]); } }; CustomFunction exp = new CustomFunction("exp") { @Override public double applyFunction(double... args) { return Math.exp(args[0]); } }; CustomFunction expm1 = new CustomFunction("expm1") { @Override public double applyFunction(double... args) { return Math.expm1(args[0]); } }; CustomFunction floor = new CustomFunction("floor") { @Override public double applyFunction(double... args) { return Math.floor(args[0]); } }; CustomFunction log = new CustomFunction("log") { @Override public double applyFunction(double... args) { return Math.log(args[0]); } }; CustomFunction sine = new CustomFunction("sin") { @Override public double applyFunction(double... args) { return Math.sin(args[0]); } }; CustomFunction sinh = new CustomFunction("sinh") { @Override public double applyFunction(double... args) { return Math.sinh(args[0]); } }; CustomFunction sqrt = new CustomFunction("sqrt") { @Override public double applyFunction(double... args) { return Math.sqrt(args[0]); } }; CustomFunction tan = new CustomFunction("tan") { @Override public double applyFunction(double... args) { return Math.tan(args[0]); } }; CustomFunction tanh = new CustomFunction("tanh") { @Override public double applyFunction(double... args) { return Math.tanh(args[0]); } }; Map customFunctions = new HashMap<>(); customFunctions.put("abs", abs); customFunctions.put("acos", acos); customFunctions.put("asin", asin); customFunctions.put("atan", atan); customFunctions.put("cbrt", cbrt); customFunctions.put("ceil", ceil); customFunctions.put("cos", cos); customFunctions.put("cosh", cosh); customFunctions.put("exp", exp); customFunctions.put("expm1", expm1); customFunctions.put("floor", floor); customFunctions.put("log", log); customFunctions.put("sin", sine); customFunctions.put("sinh", sinh); customFunctions.put("sqrt", sqrt); customFunctions.put("tan", tan); customFunctions.put("tanh", tanh); return customFunctions; } catch (InvalidCustomFunctionException e) { // this should not happen... throw new RuntimeException(e); } } /** * build a new {@link Calculable} from the expression using the supplied variables * * @return the {@link Calculable} which can be used to evaluate the expression * @throws UnknownFunctionException * when an unrecognized function name is used in the expression * @throws UnparsableExpressionException * if the expression could not be parsed */ public Calculable build() throws UnknownFunctionException, UnparsableExpressionException { for (CustomOperator op : customOperators.values()) { for (int i = 0; i < op.symbol.length(); i++) { if (!validOperatorSymbols.contains(op.symbol.charAt(i))) { //TODO - change non-ascii paragraph sign to '§' throw new UnparsableExpressionException(op.symbol + " is not a valid symbol for an operator please choose from: !,#,§,$,&,;,:,~,<,>,|,="); } } } for (String varName : variables.keySet()) { checkVariableName(varName); if (customFunctions.containsKey(varName)) { throw new UnparsableExpressionException("Variable '" + varName+ "' cannot have the same name as a function"); } } builtInOperators.putAll(customOperators); return toRPNExpression(expression, variables, customFunctions, builtInOperators); } private void checkVariableName(String varName) throws UnparsableExpressionException { char[] name = varName.toCharArray(); for (int i = 0; i < name.length; i++) { if (i == 0) { if (!Character.isLetter(name[i]) && name[i] != '_') { throw new UnparsableExpressionException(varName + " is not a valid variable name: character '" + name[i] + " at " + i); } } else { if (!Character.isLetter(name[i]) && !Character.isDigit(name[i]) && name[i] != '_') { throw new UnparsableExpressionException(varName + " is not a valid variable name: character '" + name[i] + " at " + i); } } } } /** * Add a custom function instance for the evaluator to recognize * * @param function the {@link CustomFunction} to add * @return the {@link ExpressionBuilder} instance */ public ExpressionBuilder withCustomFunction(CustomFunction function) { customFunctions.put(function.name, function); return this; } public ExpressionBuilder withCustomFunctions(Collection functions) { for (CustomFunction f : functions) { withCustomFunction(f); } return this; } /** * Set the value for a variable * * @param variableName the variable name e.g. "x" * @param value the value e.g. 2.32d * @return the {@link ExpressionBuilder} instance */ public ExpressionBuilder withVariable(String variableName, double value) { variables.put(variableName, value); return this; } /** * set the variables names used in the expression without setting their values * * @param variableNames vararg {@link String} of the variable names used in the expression * @return the ExpressionBuilder instance */ public ExpressionBuilder withVariableNames(String... variableNames) { for (String variable : variableNames) { variables.put(variable, null); } return this; } /** * set the values for variables * * @param variableMap * a map of variable names to variable values * @return the {@link ExpressionBuilder} instance */ public ExpressionBuilder withVariables(Map variableMap) { variables.putAll(variableMap); return this; } /** * set a {@link CustomOperator} to be used in the expression * * @param operation the {@link CustomOperator} to be used * @return the {@link ExpressionBuilder} instance */ public ExpressionBuilder withOperation(CustomOperator operation) { customOperators.put(operation.symbol, operation); return this; } /** * set a {@link Collection} of {@link CustomOperator} to use in the expression * * @param operations the {@link Collection} of {@link CustomOperator} to use * @return the {@link ExpressionBuilder} instance */ public ExpressionBuilder withOperations(Collection operations) { for (CustomOperator op : operations) { withOperation(op); } return this; } /** * set the mathematical expression for parsing * * @param expression * a mathematical expression * @return the {@link ExpressionBuilder} instance */ public ExpressionBuilder withExpression(String expression) { this.expression = expression; return this; } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/RPNConverter.kt ================================================ package ru.trolsoft.calculator.eval import ru.trolsoft.calculator.eval.exceptions.UnknownFunctionException import ru.trolsoft.calculator.eval.exceptions.UnparsableExpressionException import ru.trolsoft.calculator.eval.tokens.* import java.util.* //internal object RPNConverter { private fun substituteUnaryOperators(expr: String, operators: MutableMap): String { val resultBuilder = StringBuilder() var whitespaceCount = 0 for (i in 0.. whitespaceCount) { if (isOperatorCharacter(resultBuilder.get(resultBuilder.length - 1 - whitespaceCount), operators)) { afterOperator = true } else if (resultBuilder.get(resultBuilder.length - 1 - whitespaceCount) == '(') { afterParantheses = true } } when (c) { '+' -> if (!resultBuilder.isEmpty() && !afterOperator && !afterParantheses && !expressionStart) { // not an unary plus so append the char resultBuilder.append(c) } '-' -> if (!resultBuilder.isEmpty() && !afterOperator && !afterParantheses && !expressionStart) { // not unary resultBuilder.append(c) } else { // unary so we substitute it resultBuilder.append('\'') } else -> resultBuilder.append(c) } whitespaceCount = 0 } return resultBuilder.toString() } @Throws(UnknownFunctionException::class, UnparsableExpressionException::class) fun toRPNExpression( infix: String, variables: MutableMap, customFunctions: MutableMap, operators: MutableMap ): RPNExpression { val tokenizer = Tokenizer(variables.keys, customFunctions, operators) val output = StringBuilder(infix.length) val operatorStack = ArrayDeque() var tokens: MutableList = tokenizer.getTokens(substituteUnaryOperators(infix, operators)) validateRPNExpression(tokens, operators) for (token in tokens) { token.mutateStackForInfixTranslation(operatorStack, output) } // all tokens read, put the rest of the operations on the output; while (!operatorStack.isEmpty()) { output.append(operatorStack.pop().value).append(" ") } val postfix = output.toString().trim { it <= ' ' } tokens = tokenizer.getTokens(postfix) return RPNExpression(tokens, postfix, variables) } @Throws(UnparsableExpressionException::class) private fun validateRPNExpression(tokens: MutableList, operators: MutableMap?) { for (i in 1..): Boolean { for (symbol in operators.keys) { if (symbol.indexOf(c) != -1) { return true } } return false } //} ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/RPNExpression.java ================================================ package ru.trolsoft.calculator.eval; import ru.trolsoft.calculator.eval.tokens.CalculationToken; import ru.trolsoft.calculator.eval.tokens.Token; import java.util.ArrayDeque; import java.util.List; import java.util.Map; import java.util.Stack; public class RPNExpression implements Calculable { final List tokens; final String expression; final Map variables; RPNExpression(List tokens, String expression, final Map variables) { super(); this.tokens = tokens; this.expression = expression; this.variables = variables; } /** * calculate the result of the expression and substitute the variables by their values beforehand * * @param values the variable values to be substituted * @return the result of the calculation * @throws IllegalArgumentException if the variables are invalid */ public double calculate(double... values) throws IllegalArgumentException { if (variables.isEmpty() && values != null) { throw new IllegalArgumentException("there are no variables to set values"); } else if (values != null && values.length != variables.size()) { throw new IllegalArgumentException("The are an unequal number of variables and arguments"); } int i = 0; if (!variables.isEmpty() && values != null) { for (Map.Entry entry : variables.entrySet()) { entry.setValue(values[i++]); } } final ArrayDeque stack = new ArrayDeque<>(); for (final Token t : tokens) { ((CalculationToken) t).mutateStackForCalculation(stack, variables); } return stack.pop(); } public String getExpression() { return expression; } public void setVariable(String name, double value) { this.variables.put(name, value); } public double calculate() { return calculate(null); } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/Tokenizer.kt ================================================ package ru.trolsoft.calculator.eval import ru.trolsoft.calculator.eval.exceptions.UnknownFunctionException import ru.trolsoft.calculator.eval.exceptions.UnparsableExpressionException import ru.trolsoft.calculator.eval.tokens.FunctionSeparatorToken import ru.trolsoft.calculator.eval.tokens.FunctionToken import ru.trolsoft.calculator.eval.tokens.NumberToken import ru.trolsoft.calculator.eval.tokens.OperatorToken import ru.trolsoft.calculator.eval.tokens.ParenthesesToken import ru.trolsoft.calculator.eval.tokens.Token import ru.trolsoft.calculator.eval.tokens.VariableToken import kotlin.math.abs internal class Tokenizer( private val variableNames: MutableSet?, private val functions: MutableMap, private val operators: MutableMap ) { private fun isVariable(name: String): Boolean = variableNames?.contains(name) ?: false private fun isFunction(name: String): Boolean = functions.containsKey(name) private fun isOperatorCharacter(c: Char): Boolean { for (symbol in operators.keys) { if (symbol.indexOf(c) != -1) { return true } } return false } @Throws(UnparsableExpressionException::class, UnknownFunctionException::class) fun getTokens(expression: String): MutableList { val tokens = mutableListOf() val chars = expression.toCharArray() var openBraces = 0 var openCurly = 0 var openSquare = 0 // iterate over the chars and fork on different types of input var lastToken: Token? var i = 0 while (i < chars.size) { val c = chars[i] if (c == ' ') { i++ continue } if (Character.isDigit(c)) { val valueBuilder = StringBuilder(1) // handle the numbers of the expression valueBuilder.append(c) var numberLen = 1 var lastCharExpNotationSeparator = false // needed to determine if a + or - following an e/E is a unary operation var expNotationSeparatorOccurred = false // to check if only one E/e notation separator has occurred var hexNotationPrefixOccurred = false // to check if only one '0x' notation prefix has occurred var binNotationPrefixOccurred = false // to check if only one '0b' notation prefix has occurred var octNotationPrefixOccurred = false // to check if only one '0' notation prefix has occurred while (i + numberLen < chars.size) { val cc = chars[i + numberLen] if (c == '0' && numberLen == 1) { // possibly binary, hex or octal notations if (cc == 'x' || cc == 'X') { // hex hexNotationPrefixOccurred = true valueBuilder.append(cc) numberLen++ continue } else if (cc == 'b' || cc == 'B') { // binary binNotationPrefixOccurred = true valueBuilder.append(cc) numberLen++ continue } else if (cc != '.') { // octal octNotationPrefixOccurred = true } } if (cc == '.') { if (hexNotationPrefixOccurred || binNotationPrefixOccurred || octNotationPrefixOccurred) { throw UnparsableExpressionException("Unexpected decimal separator") } valueBuilder.append(cc) lastCharExpNotationSeparator = false } else if (Character.isDigit(cc)) { valueBuilder.append(cc) lastCharExpNotationSeparator = false } else if ((cc >= 'a' && cc <= 'f') || (cc >= 'A' && cc <= 'F')) { if (!hexNotationPrefixOccurred && (cc == 'e' || cc == 'E')) { if (expNotationSeparatorOccurred) { throw UnparsableExpressionException("Number can have only one notation separator 'e/E'") } valueBuilder.append(cc) lastCharExpNotationSeparator = true expNotationSeparatorOccurred = true } else if (hexNotationPrefixOccurred) { valueBuilder.append(cc) } else { throw UnparsableExpressionException("Digit expected") } } else if (lastCharExpNotationSeparator && (cc == '-' || cc == '+')) { valueBuilder.append(chars[i + numberLen]) lastCharExpNotationSeparator = false } else if (cc != '_') { break // break out of the while loop here, since the number seem finished } numberLen++ } i += numberLen - 1 lastToken = NumberToken(valueBuilder.toString()) } else if (Character.isLetter(c) || c == '_') { // can be a variable or function val nameBuilder = StringBuilder() nameBuilder.append(c) var offset = 1 while (i + offset < chars.size && (Character.isLetter(chars[i + offset]) || Character.isDigit(chars[i + offset]) || chars[i + offset] == '_')) { nameBuilder.append(chars[i + offset++]) } val name = nameBuilder.toString() if (isVariable(name)) { // a variable i += offset - 1 lastToken = VariableToken(name) } else if (this.isFunction(name)) { // might be a function i += offset - 1 lastToken = FunctionToken(name, functions.get(name)!!) } else { // an unknown symbol was encountered throw UnparsableExpressionException(expression, c, i + 1) } } else if (c == ',') { // a function separator, hopefully lastToken = FunctionSeparatorToken() } else if (isOperatorCharacter(c)) { // might be an operation val symbolBuilder = StringBuilder() symbolBuilder.append(c) var offset = 1 while (chars.size > i + offset && (isOperatorCharacter(chars[i + offset])) && isOperatorStart(symbolBuilder.toString() + chars[i + offset]) ) { symbolBuilder.append(chars[i + offset]) offset++ } val symbol = symbolBuilder.toString() if (operators.containsKey(symbol)) { i += offset - 1 lastToken = OperatorToken(symbol, operators.get(symbol)!!) } else { throw UnparsableExpressionException(expression, c, i + 1) } } else if (c == '(') { openBraces++ lastToken = ParenthesesToken(c.toString()) } else if (c == '{') { openCurly++ lastToken = ParenthesesToken(c.toString()) } else if (c == '[') { openSquare++ lastToken = ParenthesesToken(c.toString()) } else if (c == ')') { openBraces-- lastToken = ParenthesesToken(c.toString()) } else if (c == '}') { openCurly-- lastToken = ParenthesesToken(c.toString()) } else if (c == ']') { openSquare-- lastToken = ParenthesesToken(c.toString()) } else { // an unknown symbol was encountered throw UnparsableExpressionException(expression, c, i + 1) } tokens.add(lastToken) i++ } if (openCurly != 0 || (openBraces != 0) or (openSquare != 0)) { val errorBuilder = StringBuilder() errorBuilder.append("There are ") var first = true if (openBraces != 0) { errorBuilder.append(abs(openBraces)).append(" unmatched parantheses ") first = false } if (openCurly != 0) { if (!first) { errorBuilder.append(" and ") } errorBuilder.append(abs(openCurly)).append(" unmatched curly brackets ") first = false } if (openSquare != 0) { if (!first) { errorBuilder.append(" and ") } errorBuilder.append(abs(openSquare)).append(" unmatched square brackets ") first = false } errorBuilder.append("in expression '").append(expression).append("'") throw UnparsableExpressionException(errorBuilder.toString()) } return tokens } private fun isOperatorStart(op: String): Boolean { operators.keys.forEach { if (it.startsWith(op)) { return true } } return false } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/exceptions/InvalidCustomFunctionException.kt ================================================ package ru.trolsoft.calculator.eval.exceptions class InvalidCustomFunctionException(message: String?) : Exception(message) ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/exceptions/UnknownFunctionException.java ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval.exceptions; import ru.trolsoft.calculator.eval.tokens.FunctionToken; /** * Exception for handling unknown Functions. * * @see FunctionToken * @author fas@congrace.de */ public class UnknownFunctionException extends Exception { /** * construct a new {@link UnknownFunctionException} * * @param functionName the function name which could not be found */ public UnknownFunctionException(String functionName) { super("Unknown function: " + functionName); } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/exceptions/UnparsableExpressionException.java ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval.exceptions; /** * Exception for invalid expressions * * @author fas@congrace.de */ public class UnparsableExpressionException extends Exception { /** * construct a new {@link UnparsableExpressionException} * * @param c * the character which could not be parsed * @param pos * the position of the character in the expression */ public UnparsableExpressionException(String expression, char c, int pos) { super("Unable to parse character '" + c + "' at position " + pos + " in expression '" + expression + "'"); } /** * construct a new {@link UnparsableExpressionException} * * @param msg * the error message */ public UnparsableExpressionException(String msg) { super(msg); } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/CalculationToken.kt ================================================ package ru.trolsoft.calculator.eval.tokens import java.util.* abstract class CalculationToken internal constructor(value: String) : Token(value) { abstract fun mutateStackForCalculation(stack: ArrayDeque, variableValues: MutableMap) } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/FunctionSeparatorToken.kt ================================================ package ru.trolsoft.calculator.eval.tokens import java.util.* class FunctionSeparatorToken : Token(",") { override fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) { var token: Token while ((operatorStack.peek().also { token = it }) !is ParenthesesToken && token.value != "(") { output.append(operatorStack.pop().value).append(" ") } } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/FunctionToken.kt ================================================ package ru.trolsoft.calculator.eval.tokens import ru.trolsoft.calculator.eval.CustomFunction import ru.trolsoft.calculator.eval.exceptions.UnknownFunctionException import ru.trolsoft.calculator.eval.reverseArray import java.util.* class FunctionToken(value: String, function: CustomFunction) : CalculationToken(value) { val name: String val function: CustomFunction init { try { this.name = function.name this.function = function } catch (_: IllegalArgumentException) { throw UnknownFunctionException(value) } } override fun equals(other: Any?): Boolean { if (other is FunctionToken) { return this.name == other.name } return false } override fun hashCode(): Int = name.hashCode() override fun mutateStackForCalculation(stack: ArrayDeque, variableValues: MutableMap) { val args = DoubleArray(function.argc) for (i in 0..< function.argc) { args[i] = stack.pop()!! } stack.push(this.function.applyFunction(*reverseArray(args))) } override fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) { operatorStack.push(this) } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/NumberToken.kt ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval.tokens import java.util.* import kotlin.math.pow /** * A [Token] for Numbers * * @author fas@congrace.de */ class NumberToken(value: String) : CalculationToken(value) { private val doubleValue: Double init { var value = value var isHex = false var isBin = false var isOct = false if (value.length > 1 && value[0] == '0') { val ch2 = value[1] if (ch2 == 'x' || ch2 == 'X') { isHex = true value = value.substring(2) } else if (ch2 == 'b' || ch2 == 'B') { isBin = true value = value.substring(2) } else if (ch2 != '.') { isOct = true value = value.substring(1) } } if (isHex) { if (value.indexOf('_') >= 0) { value = value.replace("_", "") } this.doubleValue = value.toLong(16).toDouble() } else if (isBin) { if (value.indexOf('_') >= 0) { value = value.replace("_", "") } this.doubleValue = value.toLong(2).toDouble() } else if (isOct) { if (value.indexOf('_') >= 0) { value = value.replace("_", "") } this.doubleValue = value.toLong(8).toDouble() } else if (value.indexOf('E') > 0 || value.indexOf('e') > 0) { //scientific notation as requested in EXP-17 value = value.lowercase(Locale.getDefault()) val pos = value.indexOf('e') val mantissa = value.substring(0, pos).toDouble() val exponent = value.substring(pos + 1).toDouble() this.doubleValue = mantissa * 10.0.pow(exponent) } else { if (value.indexOf('_') >= 0) { value = value.replace("_", "") } this.doubleValue = value.toDouble() } } override fun equals(other: Any?): Boolean { if (other is NumberToken) { return other.value == this.value } return false } override fun hashCode(): Int = value.hashCode() override fun mutateStackForCalculation(stack: ArrayDeque, variableValues: MutableMap) { stack.push(this.doubleValue) } override fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) { output.append(this.value).append(' ') } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/OperatorToken.kt ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval.tokens import ru.trolsoft.calculator.eval.CustomOperator import java.util.* /** * [Token] for Operations like +,-,*,/,% and ^ * * @author fas@congrace.de */ class OperatorToken( value: String, var operation: CustomOperator ) : CalculationToken(value) { fun applyOperation(vararg values: Double): Double = operation.applyOperation(values) override fun equals(other: Any?): Boolean { if (other is OperatorToken) { return other.value == this.value } return false } override fun hashCode(): Int = value.hashCode() override fun mutateStackForCalculation(stack: ArrayDeque, variableValues: MutableMap) { val operands = DoubleArray(operation.operandCount) for (i in 0..< operation.operandCount) { operands[operation.operandCount - i - 1] = stack.pop()!! } stack.push(operation.applyOperation(operands)) } override fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) { while (!operatorStack.isEmpty()) { val before = operatorStack.peek() if (before is FunctionToken) { operatorStack.pop() output.append(before.value).append(" ") } else if (before is OperatorToken) { if (isLeftAssociative() && precedence() <= before.precedence()) { output.append(operatorStack.pop().value).append(" ") } else if (!isLeftAssociative() && precedence() < before.precedence()) { output.append(operatorStack.pop().value).append(" ") } else { break } } else { break } } operatorStack.push(this) } private fun isLeftAssociative(): Boolean = operation.leftAssociative private fun precedence(): Int = operation.precedence } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/ParenthesesToken.kt ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval.tokens import java.util.* /** * Token for parenthesis * * @author fas@congrace.de */ class ParenthesesToken(value: String) : Token(value) { override fun equals(other: Any?): Boolean { if (other is ParenthesesToken) { return other.value == this.value } return false } override fun hashCode(): Int = value.hashCode() fun isOpen(): Boolean = value == "(" || value == "[" || value == "{" override fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) { if (isOpen()) { operatorStack.push(this) } else { var next: Token? while ((operatorStack.peek().also { next = it }) is OperatorToken || next is FunctionToken || (next is ParenthesesToken && !next.isOpen())) { output.append(operatorStack.pop().value).append(" ") } operatorStack.pop() } } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/Token.kt ================================================ package ru.trolsoft.calculator.eval.tokens import java.util.* abstract class Token internal constructor( @JvmField val value: String ) { abstract fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/tokens/VariableToken.kt ================================================ /* Copyright 2011 frank asseg 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 ru.trolsoft.calculator.eval.tokens import java.util.* class VariableToken(value: String) : CalculationToken(value) { override fun equals(other: Any?): Boolean { if (other is VariableToken) { return super.value == other.value } return false } override fun hashCode(): Int = super.value.hashCode() override fun mutateStackForCalculation(stack: ArrayDeque, variableValues: MutableMap) { val value = variableValues[value]!! stack.push(value) } override fun mutateStackForInfixTranslation(operatorStack: ArrayDeque, output: StringBuilder) { output.append(this.value).append(" ") } } ================================================ FILE: src/main/java/ru/trolsoft/calculator/eval/utils.kt ================================================ package ru.trolsoft.calculator.eval import java.util.Locale fun reverseArray(data: DoubleArray): DoubleArray { var left = 0 var right = data.size - 1 while (left < right) { // swap the values at the left and right indices val temp = data[left] data[left] = data[right] data[right] = temp // move the left and right index pointers in toward the center left++ right-- } return data } /** * normalize a number to an acceptable format for exp4j e.g. normalizing "314e-2" yields "3.14" */ fun normalizeNumber(number: String, loc: Locale? = Locale.getDefault()): String = number.replace("e|E".toRegex(), "*10^") ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/data/AbstractByteBuffer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.hexeditor.data; import lombok.Getter; import lombok.Setter; import java.io.IOException; /** * Buffered file reader */ public abstract class AbstractByteBuffer { /** * Стратегия кеширования при чтении */ public enum CacheStrategy { FORWARD, BACKWARD, CENTER } /** * */ static final int DEFAULT_CAPACITY = 1024*256; /** * Size of buffer */ @Getter protected final int capacity; /** * Number of bytes in buffer */ protected int bufferSize; @Getter protected long offset; protected byte[] buffer; /** * Size of file */ protected long streamSize; @Getter @Setter private CacheStrategy cacheStrategy = CacheStrategy.CENTER; public AbstractByteBuffer(int capacity) { this.capacity = capacity; buffer = new byte[capacity]; this.offset = 0; this.bufferSize = 0; this.streamSize = -1; } public byte getByte(long fileOffset) throws IOException { long index = fileOffset - offset; if (index < 0 || index >= bufferSize) { if (fileOffset < 0 || fileOffset >= getFileSize()) { throw new IndexOutOfBoundsException("Position: " + fileOffset + ", file size = " + getFileSize()); } offset = calcOffset(fileOffset, supportRandomAccess()); // FIXME if offset > size if (offset < 0) { offset = 0; } loadBuffer(); index = fileOffset - offset; } try { return buffer[(int)index]; } catch (ArrayIndexOutOfBoundsException e) { System.err.println("\nERROR\nfile offset: " + fileOffset + ", file size: " + getFileSize()); System.err.println("offset: " + offset + ", buffer size: " + buffer.length); e.printStackTrace(); return 0; // TODO !!!! } } private long calcOffset(long fileOffset, boolean randomAccessStream) { if (randomAccessStream) { return switch (cacheStrategy) { case FORWARD -> fileOffset; case BACKWARD -> fileOffset - buffer.length + 1; case CENTER -> fileOffset - buffer.length / 2; }; } else { // TODO что-то странное ! switch (cacheStrategy) { case FORWARD: return fileOffset; case BACKWARD: return fileOffset - buffer.length + 1; case CENTER: return fileOffset; } } return fileOffset; } public long getFileSize() throws IOException { if (streamSize < 0) { streamSize = getStreamSize(); } return streamSize; } public void close() throws IOException { bufferSize = 0; buffer = null; closeStream(); } abstract protected void closeStream() throws IOException; abstract protected long getStreamSize() throws IOException; /** * Load file data from #offset and fills #buffer */ abstract protected void loadBuffer() throws IOException; abstract protected boolean supportRandomAccess(); } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/data/FileByteBuffer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.hexeditor.data; import java.io.*; public class FileByteBuffer extends AbstractByteBuffer { private final String filePath; private final String fileMode; private RandomAccessFile file; public FileByteBuffer(String filePath, String fileMode, int capacity) { super(capacity); this.filePath = filePath; this.fileMode = fileMode; } public FileByteBuffer(String filePath, String fileMode) { this(filePath, fileMode, DEFAULT_CAPACITY); } private RandomAccessFile getFile() throws FileNotFoundException { if (file == null) { file = new RandomAccessFile(filePath, fileMode); } return file; } @Override protected void closeStream() throws IOException { if (file != null) { file.close(); } } @Override protected long getStreamSize() throws IOException { return getFile().length(); } @Override protected void loadBuffer() throws IOException { getFile().seek(offset); bufferSize = getFile().read(buffer); } @Override protected boolean supportRandomAccess() { return true; } } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/data/MemoryByteBuffer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.hexeditor.data; /** * @author Oleg Trifonov * Created on 08/02/17. */ public class MemoryByteBuffer extends AbstractByteBuffer { public MemoryByteBuffer(int capacity) { super(capacity); bufferSize = capacity; streamSize = capacity; } @Override protected void closeStream() { } @Override protected long getStreamSize() { return capacity; } @Override protected void loadBuffer() { } @Override protected boolean supportRandomAccess() { return true; } @Override public long getFileSize() { return capacity; } @Override public byte getByte(long offset) { return buffer[(int)offset]; } public void setByte(long offset, int val) { buffer[(int)offset] = (byte)val; } } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/data/TrolCommanderByteBuffer.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.hexeditor.data; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.io.RandomAccessInputStream; import java.io.IOException; import java.io.InputStream; public class TrolCommanderByteBuffer extends AbstractByteBuffer { private final AbstractFile file; private InputStream is; private long lastOffset; public TrolCommanderByteBuffer(AbstractFile file) { super(DEFAULT_CAPACITY); this.file = file; } @Override protected void closeStream() throws IOException { if (is != null) { is.close(); } } @Override protected long getStreamSize() { return file.getSize(); } @Override protected void loadBuffer() throws IOException { getInputStream(); if (is instanceof RandomAccessInputStream rndIs) { // Seek and reuse the stream rndIs.seek(offset); //System.out.println("RANDOM ACCESS " + offset); } else { // TODO: it would be more efficient to use some sort of PushBackInputStream, though we can't use PushBackInputStream because we don't want to keep pushing back for the whole InputStream lifetime // Close the InputStream and open a new one // Note: we could use mark/reset if the InputStream supports it, but it is almost never implemented by // InputStream subclasses and a broken by design anyway. if (lastOffset > offset) { //System.out.println("GENERAL ACCESS WITH RECREATE " + offset + " " + lastOffset + " " + (lastOffset - offset)); is.close(); is = file.getInputStream(); is.skip(offset); } else if (lastOffset != offset) { //System.out.println("GENERAL ACCESS WITH SKIP " + offset + " " + (offset - lastOffset)); is.skip(offset - lastOffset); } } int bufPos = 0; bufferSize = 0; while (bufferSize < capacity) { int read = is.read(buffer, bufPos, capacity- bufferSize); if (read < 0) { break; } bufPos += read; bufferSize += read; } lastOffset = offset + bufferSize; } @Override protected boolean supportRandomAccess() { return file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE); } protected InputStream getInputStream() throws IOException { if (is == null) { if (file.isFileOperationSupported(FileOperation.RANDOM_READ_FILE)) { try { is = file.getRandomAccessInputStream(); } catch(IOException e) { // In that case we simply get an InputStream } } if (is == null) { is = file.getPushBackInputStream(1024); } lastOffset = 0; } return is; } } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/events/OffsetChangeListener.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.hexeditor.events; /** * @author Oleg Trifonov * Created on 02/04/14. */ public interface OffsetChangeListener { void onChange(long offset); } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/events/SelectionChangeListener.java ================================================ package ru.trolsoft.hexeditor.events; public interface SelectionChangeListener { void onSelectionChanged(long fromAddress, long toAddress); } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/search/ByteBufferSearchUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.hexeditor.search; import ru.trolsoft.hexeditor.data.AbstractByteBuffer; import java.io.IOException; /** * Search in AbstractByteBuffer */ public class ByteBufferSearchUtils { /** * Returns the offset within the ByteBuffer of the first occurrence of the specified data, starting at the specified offset. * * @param data buffer for search * @param pattern the data to search for * @param fromOffset the offset from which to start the search * @return the offset of the first occurrence of the specified data, at the specified offset, or -1 if there is no such occurrence */ public static long indexOf(AbstractByteBuffer data, byte[] pattern, long fromOffset) throws IOException { if (data == null || pattern == null || fromOffset < 0) { return -1; } long fileSize = data.getFileSize(); if (fileSize <= 0 || pattern.length == 0 || pattern.length > fileSize) { return -1; } fromOffset = Math.min(fromOffset, fileSize - 1); int[] failure = computeFailure(pattern); AbstractByteBuffer.CacheStrategy cacheStrategy = data.getCacheStrategy(); data.setCacheStrategy(AbstractByteBuffer.CacheStrategy.FORWARD); try { int j = 0; for (long i = fromOffset; i <= fileSize - 1; i++) { byte currentByte = data.getByte(i); while (j > 0 && pattern[j] != currentByte) { j = failure[j - 1]; } if (pattern[j] == currentByte) { j++; } if (j == pattern.length) { return i - pattern.length + 1; } } return -1; } finally { data.setCacheStrategy(cacheStrategy); } } /** * Searches for the first occurrence of the specified patterns in the buffer, * starting at the specified offset. * * @param data buffer for search * @param patterns array of patterns to search for * @param fromOffset the offset from which to start the search * @return the offset of the first occurrence of any pattern, or -1 if none found */ public static long indexOf(AbstractByteBuffer data, byte[][] patterns, long fromOffset) throws IOException { if (data == null || patterns == null || fromOffset < 0) { return -1; } long fileSize = data.getFileSize(); if (fileSize <= 0) { return -1; } // fromOffset = Math.min(fromOffset, fileSize - 1); long earliestMatch = -1; AbstractByteBuffer.CacheStrategy originalStrategy = data.getCacheStrategy(); data.setCacheStrategy(AbstractByteBuffer.CacheStrategy.FORWARD); try { for (byte[] pattern : patterns) { if (pattern == null || pattern.length == 0 || pattern.length > fileSize) { continue; } long match = indexOf(data, pattern, fromOffset); if (match != -1) { if (earliestMatch == -1 || match < earliestMatch) { earliestMatch = match; // Оптимизация: если нашли в starting offset, раньше быть не может if (earliestMatch == fromOffset) { return earliestMatch; } } } } } finally { data.setCacheStrategy(originalStrategy); } return earliestMatch; } public static long indexOfBackward(AbstractByteBuffer data, byte[] pattern, long fromOffset) throws IOException { if (data == null || pattern == null || fromOffset < 0) { return -1; } long fileSize = data.getFileSize(); if (fileSize <= 0 || pattern.length == 0 || pattern.length > fileSize) { return -1; } fromOffset = Math.min(fromOffset, fileSize - 1); byte[] patternInvert = new byte[pattern.length]; for (int i = 0; i < pattern.length; i++) { patternInvert[i] = pattern[pattern.length-i-1]; } int[] failure = computeFailure(patternInvert); AbstractByteBuffer.CacheStrategy cacheStrategy = data.getCacheStrategy(); data.setCacheStrategy(AbstractByteBuffer.CacheStrategy.BACKWARD); try { int j = 0; for (long i = fromOffset; i >= 0; i--) { while (j > 0 && patternInvert[j] != data.getByte(i)) { j = failure[j - 1]; } if (patternInvert[j] == data.getByte(i)) { j++; } if (j == pattern.length) { return i; } } return -1; } finally { data.setCacheStrategy(cacheStrategy); } } /** * Knuth-Morris-Pratt Algorithm for Pattern Matching * Finds the first occurrence of the pattern in the text. */ public static int indexOf(byte[] data, byte[] pattern) { if (data == null || pattern == null || pattern.length == 0) { return -1; } if (pattern.length > data.length) { return -1; } int[] failure = computeFailure(pattern); int j = 0; for (int i = 0; i < data.length; i++) { while (j > 0 && pattern[j] != data[i]) { j = failure[j - 1]; } if (pattern[j] == data[i]) { j++; } if (j == pattern.length) { return i - pattern.length + 1; } } return -1; } /** * Computes the failure function using a bootstrapping process, * where the pattern is matched against itself. */ private static int[] computeFailure(byte[] pattern) { int[] failure = new int[pattern.length]; int j = 0; for (int i = 1; i < pattern.length; i++) { while (j > 0 && pattern[j] != pattern[i]) { j = failure[j - 1]; } if (pattern[j] == pattern[i]) { j++; } failure[i] = j; } return failure; } } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/ui/HexTable.java ================================================ package ru.trolsoft.hexeditor.ui; import lombok.Getter; import lombok.Setter; import ru.trolsoft.hexeditor.events.OffsetChangeListener; import ru.trolsoft.hexeditor.events.SelectionChangeListener; import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; /** * The table displaying the hex data */ public class HexTable extends JTable { private static final Dimension ZERO_DIMENSION = new Dimension(0, 0); @Setter private SelectionChangeListener selectionChangeListener; /** * ASCII characters. Used to prevent strings creation on repainting cell */ private static final String[] CHARACTERS = new String[256]; static { for (char i = 0; i < CHARACTERS.length; i++) { CHARACTERS[i] = Character.toString(i); } } private ViewerHexTableModel model; private final CellRenderer cellRenderer = new CellRenderer(); private final HeaderRenderer headerRenderer = new HeaderRenderer(); private final Rectangle repaintRect = new Rectangle(); private int charWidth; private int fontHeight; private int fontAscent; private Color alternateCellColor; private Color offsetColor; private Color asciiDumpColor; private Color selectionAsciiBackgroundColor; private boolean alternateRowBackground; private boolean alternateColumnBackground; private long leadSelectionIndex; private long anchorSelectionIndex; @Setter @Getter private OffsetChangeListener offsetChangeListener; private JPopupMenu popupMenu; private JMenuItem copyBinaryItem; private JMenuItem copyHexItem; private JMenuItem saveSelectedItem; public HexTable(ViewerHexTableModel model) { super(model); this.model = model; enableEvents(AWTEvent.KEY_EVENT_MASK); setAutoResizeMode(JTable.AUTO_RESIZE_OFF); setShowGrid(false); setIntercellSpacing(ZERO_DIMENSION); alternateCellColor = getBackground(); offsetColor = getForeground(); asciiDumpColor = getForeground(); setCellSelectionEnabled(true); setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION); setDefaultEditor(Object.class, cellEditor); setDefaultRenderer(Object.class, cellRenderer); for (int i = getColumnModel().getColumnCount()-1; i >= 0; i--) { TableColumn col = getColumnModel().getColumn(i); col.setHeaderRenderer(headerRenderer); } getTableHeader().setReorderingAllowed(false); setShowGrid(false); setFont(getFont()); initContextMenu(); } private void initContextMenu() { popupMenu = new JPopupMenu(); copyBinaryItem = new JMenuItem("Copy selected as binary"); copyBinaryItem.addActionListener(e -> copySelectedAsBinary()); popupMenu.add(copyBinaryItem); copyHexItem = new JMenuItem("Copy selected as hex"); copyHexItem.addActionListener(e -> copySelectedAsHex()); popupMenu.add(copyHexItem); saveSelectedItem = new JMenuItem("Save selected"); saveSelectedItem.addActionListener(e -> saveSelected()); popupMenu.add(saveSelectedItem); addMouseListener(new java.awt.event.MouseAdapter() { @Override public void mousePressed(java.awt.event.MouseEvent e) { if (e.isPopupTrigger()) { showContextMenu(e); } } @Override public void mouseReleased(java.awt.event.MouseEvent e) { if (e.isPopupTrigger()) { showContextMenu(e); } } }); } private void showContextMenu(java.awt.event.MouseEvent e) { long from = getSmallestSelectionIndex(); long to = getLargestSelectionIndex(); boolean hasSelection = from >= 0 && to >= 0 && from <= to; copyBinaryItem.setEnabled(hasSelection); copyHexItem.setEnabled(hasSelection); saveSelectedItem.setEnabled(hasSelection); popupMenu.show(e.getComponent(), e.getX(), e.getY()); } /** * Changes the selected byte range. * * @param row row * @param col column * @param toggle see description for {@link JTable#changeSelection(int, int, boolean, boolean)} * @param extend if true, extend the current selection * @see #changeSelectionByOffset(long, boolean) * @see #setSelectedRows(int, int) * @see #setSelectionByOffsets(long, long) */ @Override public void changeSelection(int row, int col, boolean toggle, boolean extend) { // remind previous selection range //long prevSmallest = getSmallestSelectionIndex(); //long prevLargest = getLargestSelectionIndex(); // Don't allow the user to select the "ascii dump" or any // empty cells in the last row of the table. col = adjustColumn(row, col); if (row < 0) { row = 0; } final long prevSelectionIndexFrom = anchorSelectionIndex; final long prevSelectionIndexTo = leadSelectionIndex; if (extend) { leadSelectionIndex = cellToOffset(row, col); } else { anchorSelectionIndex = leadSelectionIndex = cellToOffset(row, col); } if (offsetChangeListener != null) { offsetChangeListener.onChange(anchorSelectionIndex); } // Scroll after changing the selection as blit scrolling is immediate, so that if we cause the repaint after the // scroll we end up painting everything! if (getAutoscrolls()) { ensureCellIsVisible(row, col); } // Draw the new selection. repaintSelection(prevSelectionIndexFrom, prevSelectionIndexTo); if (selectionChangeListener != null) { selectionChangeListener.onSelectionChanged(anchorSelectionIndex, leadSelectionIndex); } } private static int min(int x1, int x2, int x3, int x4) { int min = Math.min(x1, x2); if (x3 < min) { min = x3; } if (x4 < min) { min = x4; } return min; } private static int max(int x1, int x2, int x3, int x4) { int max = Math.max(x1, x2); if (x3 > max) { max = x3; } if (x4 > max) { max = x4; } return max; } @Override public boolean isCellEditable(int row, int col) { return false;//cellToOffset(row, col) > -1; } @Override public boolean isCellSelected(int row, int col) { // Offset and ASCII dump if (col == 0 || col == model.getColumnCount()-1) { return false; } long offset = cellToOffset(row, col); final long start = getSmallestSelectionIndex(); final long end = getLargestSelectionIndex(); return offset >= start && offset <= end; } @Override public void setFont(Font font) { super.setFont(font); calculateSizes(font); if (cellRenderer != null) { // check for NPE prevent, because constructor of JTable calls setFont() cellRenderer.setFont(font); } } public void setModel(ViewerHexTableModel model) { super.setModel(model); this.model = model; calculateSizes(getFont()); } /** * Calculates table column sizes after change of font */ private void calculateSizes(Font font) { if (model == null) { return; } FontMetrics fm = getFontMetrics(font); FontMetrics fmHeader = getFontMetrics(getTableHeader().getFont()); charWidth = max(fm.charWidth('W'), fm.charWidth('_'), fm.charWidth('8'), fm.charWidth('@')); fontHeight = fm.getHeight(); fontAscent = fm.getAscent(); int width = 0; final int hexColumns = model.getNumberOfHexColumns(); int w = Math.max(charWidth * 3, fmHeader.stringWidth("+9D9")+2); // +9W9 for (int i = 1; i <= hexColumns; i++) { setColumnWidth(i, w); width += w; } // Offset setColumnWidth(0, charWidth * 10); width += charWidth * 10; // Hex dump w = charWidth * (hexColumns + 1); width += w; // ASCII dump setColumnWidth(hexColumns + 1, w); model.setAsciiCharVisible((char) 0, false); for (char ch = 1; ch <= 0xff; ch++) { model.setAsciiCharVisible(ch, fm.charWidth(ch) <= charWidth); } setRowHeight(fontHeight); setPreferredScrollableViewportSize(new Dimension(width, 25 * getRowHeight())); } /** * Set preferred, minimum and maximum width of column * @param index index of column * @param width column width */ private void setColumnWidth(int index, int width) { TableColumn column = getColumnModel().getColumn(index); column.setPreferredWidth(width); column.setMinWidth(width); column.setMaxWidth(width); } /** * Returns the column for the cell containing data that is the closest * to the specified cell. This is used when, for example, the user clicks * on an "empty" cell in the last row of the table. * * @param row The row of the cell clicked on. * @param col The column of the cell clicked on. * @return The column of the closest cell containing data. */ private int adjustColumn(int row, int col) { if (col < 0) { return 0; } // last row if (row == getRowCount() - 1) { final long fileSize = model.getSize(); final int hexColumns = model.getNumberOfHexColumns(); int lastRowCount = (int)(fileSize % hexColumns); if (lastRowCount > 0) { return Math.min(col, lastRowCount); } } return Math.min(col, getColumnCount()-2); } /** * Returns the cell representing the specified offset into the hex document. * * @param offset The offset into the document. * @return The cell, in the form (row, col). If the specified offset is invalid, (-1, -1) is returned. * @see #cellToOffset(int, int) */ public Point offsetToCell(long offset) { if (offset < 0 || offset >= model.getSize()) { return new Point(-1, -1); } final int hexColumns = model.getNumberOfHexColumns(); int row = (int)(offset / hexColumns); int col = (int)(offset % hexColumns); return new Point(row, col); } /** * Returns the offset into the bytes being edited represented at the * specified cell in the table, if any. * * @param row The row in the table. * @param col The column in the table. * @return The offset into the byte array, or -1 if the cell does not represent part of the byte array * (such as the tailing "ASCII dump" column's cells). * @see #offsetToCell(long) */ public long cellToOffset(int row, int col) { // Check row and column individually to prevent them being invalid values but still pointing to a valid offset in the buffer. final int hexColumns = model.getNumberOfHexColumns(); // Don't include last column (ASCII dump) if (row < 0 || row >= getRowCount() || col < 1 || col > hexColumns) { return -1; } long offs = (long)row * hexColumns + col - 1; return (offs >= 0 && offs < model.getSize()) ? offs : -1; } /** * Returns the largest selection index. * * @return The largest selection index. * @see #getSmallestSelectionIndex() */ public long getLargestSelectionIndex() { long index = Math.max(leadSelectionIndex, anchorSelectionIndex); return index < 0 ? 0 : index; // Don't return -1 if table is empty } /** * Returns the smallest selection index. * * @return The smallest selection index. * @see #getLargestSelectionIndex() */ public long getSmallestSelectionIndex() { long index = Math.min(leadSelectionIndex, anchorSelectionIndex); return index < 0 ? 0 : index; // Don't return -1 if table is empty } /** * Changes the selection by an offset into the bytes being edited. * * @param offset offset * @param extend if true, extend the current selection * @see #changeSelection(int, int, boolean, boolean) * @see #setSelectedRows(int, int) * @see #setSelectionByOffsets(long, long) */ public void changeSelectionByOffset(long offset, boolean extend) { final long fileSize = model.getSize(); if (offset < 0) { offset = 0; } else if (offset >= fileSize) { offset = fileSize - 1; } final int hexColumns = model.getNumberOfHexColumns(); int row = (int)(offset / hexColumns); int col = (int)(offset % hexColumns) + 1; changeSelection(row, col, false, extend); } /** * Clears the selection. The "lead" of the selection is set back to the position of the "anchor." */ @Override public void clearSelection() { final long prevSelectionIndexFrom = anchorSelectionIndex; final long prevSelectionIndexTo = leadSelectionIndex; if (anchorSelectionIndex >= 0) { // Always true unless an error leadSelectionIndex = anchorSelectionIndex; } else { anchorSelectionIndex = leadSelectionIndex = 0; } repaintSelection(prevSelectionIndexFrom, prevSelectionIndexTo); } public void setSelectedRows(int min, int max) { if (min < 0 || min >= getRowCount() || max < 0 || max >= getRowCount()) { throw new IllegalArgumentException(); } final int hexColumns = model.getNumberOfHexColumns(); int startOffs = min * hexColumns; int endOffs = max*hexColumns + hexColumns-1; // TODO: Have a single call to change selection by a range. changeSelectionByOffset(startOffs, false); changeSelectionByOffset(endOffs, true); } /** * Selects the specified range of bytes in the table. * * @param startOffs The "anchor" byte of the selection. * @param endOffs The "lead" byte of the selection. * @see #changeSelection(int, int, boolean, boolean) * @see #changeSelectionByOffset(long, boolean) */ public void setSelectionByOffsets(long startOffs, long endOffs) { final long prevSelectionIndexFrom = anchorSelectionIndex; final long prevSelectionIndexTo = leadSelectionIndex; if (startOffs < 0) { startOffs = 0; } else if (startOffs >= model.getSize()) { startOffs = model.getSize()-1; } // Clear the old selection (may not be necessary). //repaintSelection(); anchorSelectionIndex = startOffs; leadSelectionIndex = endOffs; // Scroll after changing the selection as blit scrolling is // immediate, so that if we cause the repaint after the scroll we // end up painting everything! if (getAutoscrolls()) { final int hexColumns = model.getNumberOfHexColumns(); int endRow = (int)(endOffs / hexColumns); int endCol = (int)(endOffs % hexColumns); // Don't allow the user to select the "ascii dump" or any empty cells in the last row of the table. endCol = adjustColumn(endRow, endCol); if (endRow < 0) { endRow = 0; } ensureCellIsVisible(endRow, endCol); } // Draw the new selection. repaintSelection(prevSelectionIndexFrom, prevSelectionIndexTo); if (offsetChangeListener != null) { offsetChangeListener.onChange(anchorSelectionIndex); } } public void gotoOffset(long offset) { setSelectionByOffsets(offset, offset); } /** * Ensures the specified cell is visible. * * @param row The row of the cell. * @param col The column of the cell. */ private void ensureCellIsVisible(int row, int col) { Rectangle cellRect = getCellRect(row, col, false); scrollRectToVisible(cellRect); } private void repaintSelection(long indexFromBefore, long indexFromAfter) { if (model == null) { return; } // calculate area for repainting final int hexColumns = model.getNumberOfHexColumns(); final int row1 = (int)(indexFromBefore / hexColumns); final int row2 = (int)(indexFromAfter / hexColumns); final int row3 = (int)(anchorSelectionIndex / hexColumns); final int row4 = (int)(leadSelectionIndex / hexColumns); final int rowFrom = min(row1, row2, row3, row4); final int rowTo = max(row1, row2, row3, row4); int colFrom, colTo; if (rowFrom == rowTo) { final int col1 = (int)(indexFromBefore % hexColumns); final int col2 = (int)(indexFromAfter % hexColumns); final int col3 = (int)(anchorSelectionIndex % hexColumns); final int col4 = (int)(leadSelectionIndex % hexColumns); colFrom = min(col1, col2, col3, col4); colTo = max(col1, col2, col3, col4); } else { colFrom = 0; colTo = hexColumns-1; } //System.out.println("> " + colFrom + ", " + rowFrom + " -> " + colTo + ", " + rowTo); // repaint hex columns final int rowHeight = getRowHeight(); final TableColumnModel cm = getColumnModel(); final int offsetColumnWidth = cm.getColumn(0).getWidth(); final int hexColumnWidth = cm.getColumn(1).getWidth(); final int dumpColumnWidth = cm.getColumn(cm.getColumnCount()-1).getWidth(); //repaintRect.setBounds(offsetColumnWidth, rowHeight*rowFrom, getWidth()-offsetColumnWidth, (rowTo-rowFrom+1)*rowHeight); //repaint(repaintRect); repaintRect.setBounds(offsetColumnWidth + colFrom*hexColumnWidth, rowHeight*rowFrom, (colTo-colFrom+1)*hexColumnWidth, (rowTo-rowFrom+1)*rowHeight); repaint(repaintRect); repaintRect.setBounds(getWidth() - dumpColumnWidth, rowHeight*rowFrom, dumpColumnWidth, (rowTo-rowFrom+1)*rowHeight); repaint(repaintRect); } /** * Current offset in file * @return current offset in file */ public long getCurrentAddress() { return anchorSelectionIndex; } /** * Set alternate background color * @param color alternate background color */ public void setAlternateBackground(Color color) { this.alternateCellColor = color; } /** * Get alternate background color * @return alternate background color */ public Color getAlternateBackground() { return alternateCellColor; } /** * Set color to render the first offset column * @param color color to render the first offset column */ public void setOffsetColumnColor(Color color) { this.offsetColor = color; } /** * Get color to render the first offset column * @return color to render the first offset column */ public Color getOffsetColumnColor() { return offsetColor; } /** * Set color to render the last ASCII dump column * @param color color for ASCII dump text */ public void setAsciiColumnColor(Color color) { this.asciiDumpColor = color; } /** * Get color to render the last ASCII dump column * @return color to render the ASCII dump column */ public Color getAsciiColumnColor() { return asciiDumpColor; } /** * * @param color background color of highlighted section in ascii dump */ public void setAsciiSelectionBackgroundColor(Color color) { this.selectionAsciiBackgroundColor = color; } /** * * @return background color of highlighted section in ascii dump */ public Color getSelectionAsciiBackgroundColor() { return selectionAsciiBackgroundColor; } /** * * @param enable true if used alternate background color for odd rows */ public void setAlternateRowBackground(boolean enable) { this.alternateRowBackground = enable; } /** * * @return true if used alternate background color for odd rows */ public boolean isAlternateRowBackground() { return alternateRowBackground; } /** * * @param enable true if used alternate background color for odd columns */ public void setAlternateColumnBackground(boolean enable) { this.alternateColumnBackground = enable; } /** * * @return true if used alternate background color for odd columns */ public boolean isAlternateColumnBackground() { return alternateColumnBackground; } /** * Returns the rendering hints for text that will most accurately reflect * those of the native windowing system. * * @return The rendering hints, or null if they cannot be * determined. */ private Map getDesktopAntiAliasHints() { return (Map)getToolkit().getDesktopProperty("awt.font.desktophints"); } @Override protected void processKeyEvent (KeyEvent e) { // TODO: Convert into Actions and put into InputMap/ActionMap? final int hexColumns = model.getNumberOfHexColumns(); final long lastOffset = model.getSize() - 1; final boolean extend = e.isShiftDown(); if (e.getID() == KeyEvent.KEY_PRESSED) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: long offs = leadSelectionIndex > 0 ? leadSelectionIndex-1 : 0; changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_RIGHT: offs = Math.min(leadSelectionIndex+1, lastOffset); changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_UP: offs = Math.max(leadSelectionIndex - hexColumns, 0); changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_DOWN: offs = Math.min(leadSelectionIndex + hexColumns, lastOffset); changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_PAGE_DOWN: int visibleRowCount = getVisibleRect().height/getRowHeight(); offs = Math.min(leadSelectionIndex + visibleRowCount * hexColumns, lastOffset); changeSelectionByOffset(offs, extend); e.consume(); break; case KeyEvent.VK_PAGE_UP: visibleRowCount = getVisibleRect().height / getRowHeight(); offs = Math.max(leadSelectionIndex - visibleRowCount*hexColumns, 0); changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_HOME: offs = (leadSelectionIndex/hexColumns)*hexColumns; changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_END: offs = (leadSelectionIndex/hexColumns)*hexColumns + hexColumns-1; offs = Math.min(offs, lastOffset); changeSelectionByOffset(offs, extend); e.consume(); return; case KeyEvent.VK_BACK_SPACE: //System.out.println(Profiler.getTime()); return; } } super.processKeyEvent(e); } private void copySelectedAsBinary() { long from = getSmallestSelectionIndex(); long to = getLargestSelectionIndex(); if (from < 0 || to < 0 || from > to) { return; } try { int length = (int)(to - from + 1); byte[] data = new byte[length]; for (int i = 0; i < length; i++) { data[i] = model.getByteAt(from + i); } Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); Transferable transferable = new Transferable() { @Override public DataFlavor[] getTransferDataFlavors() { return new DataFlavor[]{DataFlavor.stringFlavor}; } @Override public boolean isDataFlavorSupported(DataFlavor flavor) { return true; } @Override public Object getTransferData(DataFlavor flavor) { return data; } }; clipboard.setContents(transferable, null); } catch (IOException e) { e.printStackTrace(); } } private void copySelectedAsHex() { long from = getSmallestSelectionIndex(); long to = getLargestSelectionIndex(); if (from < 0 || to < 0 || from > to) { return; } try { StringBuilder sb = new StringBuilder(); for (long i = from; i <= to; i++) { byte b = model.getByteAt(i); if (!sb.isEmpty()) { sb.append(' '); } sb.append(String.format("%02X", b & 0xFF)); } StringSelection stringSelection = new StringSelection(sb.toString()); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); } catch (IOException e) { e.printStackTrace(); } } private void saveSelected() { long from = getSmallestSelectionIndex(); long to = getLargestSelectionIndex(); if (from < 0 || to < 0 || from > to) { return; } JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); fileChooser.setDialogTitle("Save selected bytes"); if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { File file = fileChooser.getSelectedFile(); try (FileOutputStream fos = new FileOutputStream(file)) { for (long i = from; i <= to; i++) { fos.write(model.getByteAt(i)); } } catch (IOException e) { e.printStackTrace(); JOptionPane.showMessageDialog(this, "Error saving file: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } } private class CellRenderer extends DefaultTableCellRenderer { private final Point highlight; private final Map desktopAAHints; private boolean hasSeparatorLine; CellRenderer() { highlight = new Point(); desktopAAHints = getDesktopAntiAliasHints(); setFont(HexTable.this.getFont()); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { // Component result = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); setValue(value); highlight.setLocation(-1, -1); hasSeparatorLine = column > 1 && column % 4 == 1; // ASCII dump if (column == table.getColumnCount() - 1) { long selStart = getSmallestSelectionIndex(); long selEnd = getLargestSelectionIndex(); final int hexColumns = model.getNumberOfHexColumns(); int b1 = row * hexColumns; int b2 = b1 + hexColumns - 1; if (selStart <= b2 && selEnd >= b1) { long start = Math.max(selStart, b1) - b1; long end = Math.min(selEnd, b2) - b1; highlight.setLocation(start, end); } boolean alternateColor = alternateRowBackground && (row & 1) > 0; setBackground(alternateColor ? alternateCellColor : table.getBackground()); setForeground(asciiDumpColor); hasSeparatorLine = false; } else { if (!isSelected) { if (shouldUseAlternateBackground(row, column)) { setBackground(alternateCellColor); } else { setBackground(table.getBackground()); } } else { setBackground(table.getSelectionBackground()); } // Offset column if (column == 0) { setForeground(offsetColor); } else { setForeground(table.getForeground()); } } return this; } private boolean shouldUseAlternateBackground(int row, int column) { return (alternateRowBackground && (row & 1) > 0) ^ (alternateColumnBackground && (column & 1) > 0); } @Override protected void paintComponent(Graphics g) { g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); final String text = getText(); final int len = text.length(); int x = (getWidth() - charWidth *len)/2; int y = (getHeight() - fontHeight)/2 + fontAscent; if (highlight.x >= 0) { g.setColor(selectionAsciiBackgroundColor); g.fillRect(x + highlight.x * charWidth, 0, (highlight.y - highlight.x + 1) * charWidth, getRowHeight()); } Graphics2D g2d = (Graphics2D)g; Object oldHints = null; if (desktopAAHints != null) { oldHints = g2d.getRenderingHints(); g2d.addRenderingHints(desktopAAHints); } g.setColor(getForeground()); // not padding low bytes, and this one is in range 00-0f. if (len == 1) { x += charWidth; } for (int i = 0; i < len; i++) { char ch = text.charAt(i); if (ch != ' ') { g.drawString(CHARACTERS[ch], x, y); } x += charWidth; } //g.drawString(text, x, y); // Restore rendering hints appropriately. if (desktopAAHints != null) { g2d.addRenderingHints((Map)oldHints); } if (hasSeparatorLine) { g.setColor(Color.GRAY); g.drawLine(0, 0, 0, getHeight()); } } } private class HeaderRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = 1L; private final Map desktopAAHints; private boolean centerText; HeaderRenderer() { desktopAAHints = getDesktopAntiAliasHints(); setFont(HexTable.this.getFont()); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { setValue(value); centerText = column != 0 && column != getColumnCount()-1; return this; } @Override protected void paintComponent(Graphics g) { g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); final String text = getText(); final int len = text.length(); int x; if (centerText) { x = (getWidth() - charWidth *len)/2 + 1; } else { x = 5; } int y = (getHeight() - fontHeight)/2 + fontAscent; Graphics2D g2d = (Graphics2D)g; Object oldHints = null; if (desktopAAHints != null) { oldHints = g2d.getRenderingHints(); g2d.addRenderingHints(desktopAAHints); } g.setColor(getForeground()); g2d.drawString(text, x, y); // Restore rendering hints appropriately. if (desktopAAHints != null) { g2d.addRenderingHints((Map)oldHints); } // separator g.setColor(Color.GRAY); g.drawLine(0, 0, 0, getHeight()); } } } ================================================ FILE: src/main/java/ru/trolsoft/hexeditor/ui/ViewerHexTableModel.java ================================================ package ru.trolsoft.hexeditor.ui; import ru.trolsoft.hexeditor.data.AbstractByteBuffer; import ru.trolsoft.utils.StrUtils; import javax.swing.table.AbstractTableModel; import java.io.IOException; /** * The table model used by the JTable in the hex viewer/editor. */ public class ViewerHexTableModel extends AbstractTableModel { protected final boolean[] VISIBLE_SYMBOLS = new boolean[256]; protected long fileSize; private final int hexDataColumns; protected final AbstractByteBuffer buffer; public ViewerHexTableModel(AbstractByteBuffer byteBuffer, int columns) { this.buffer = byteBuffer; for (int i = 0; i < VISIBLE_SYMBOLS.length; i++) { VISIBLE_SYMBOLS[i] = i > 0; } this.hexDataColumns = columns; } public ViewerHexTableModel(AbstractByteBuffer byteBuffer) { this(byteBuffer, 32); } public void load() throws IOException { this.fileSize = buffer.getFileSize(); if (fileSize > 0) { buffer.getByte(0); } } public long getSize() { return fileSize; } @Override public int getRowCount() { long fs = getSize(); int rows = (int)(fs / hexDataColumns); if (fs % hexDataColumns > 0) { rows++; } return rows; } @Override public int getColumnCount() { return hexDataColumns + 2; // offset, data columns, ASCII dump } @Override public Object getValueAt(int rowIndex, int columnIndex) { if (columnIndex == 0) { // offset return StrUtils.dwordToHexStr(getRowOffset(rowIndex)); } else if (columnIndex == hexDataColumns + 1) { // dump try { return getAsciiDump(rowIndex * hexDataColumns); } catch (IOException e) { e.printStackTrace(); return ""; } } else { long fileOffset = rowIndex * hexDataColumns + columnIndex - 1; if (fileOffset >= fileSize) { return ""; } try { return StrUtils.byteToHexStr(buffer.getByte(fileOffset)); } catch (ArrayIndexOutOfBoundsException | IOException e) { e.printStackTrace(); return "xx"; } } } protected long getRowOffset(int row) { return row * hexDataColumns; } private String getAsciiDump(long fileOffset) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < hexDataColumns; i++) { long offset = fileOffset + i; if (offset >= fileSize) { sb.append(' '); } else { int b = buffer.getByte(offset) & 0xff;// getData()[i + bufferOffset] & 0xff; char ch = (char)b; if (!VISIBLE_SYMBOLS[b]) { ch = ' '; } sb.append(ch); } } return sb.toString(); } public int getNumberOfHexColumns() { return hexDataColumns; } @Override public String getColumnName(int column) { if (column == 0) { return "Offset"; } else if (column == hexDataColumns + 1) { return "ASCII dump"; } return "+" + Integer.toHexString(column-1).toUpperCase(); } void setAsciiCharVisible(char ch, boolean visible) { if (ch < VISIBLE_SYMBOLS.length) { VISIBLE_SYMBOLS[ch] = visible; } } public byte getByteAt(long offset) throws IOException { return buffer.getByte(offset); } } ================================================ FILE: src/main/java/ru/trolsoft/jni/NativeFileUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.jni; import ru.trolsoft.utils.FileUtils; import java.io.File; import java.io.IOException; public class NativeFileUtils { private static final int VERSION = 2; public static final int FA_MASK_EXISTS = 1; public static final int FA_MASK_DIRECTORY = 2; public static final int FA_MASK_HIDDEN = 4; private static boolean init = false; private static boolean installed = false; private static String getJnilibFileName() { return "libtrolsoft-" + System.getProperty("os.arch") + ".jnilib"; } private static void prepareLibrary(boolean overwrite) { String jarPath = FileUtils.getJarPath(); try { FileUtils.copyFromJarFile(getJnilibFileName(), jarPath, overwrite); } catch (IOException e) { e.printStackTrace(); } } public static boolean init() { if (!init) { init = true; prepareLibrary(false); try { String path = FileUtils.getJarPath() + File.separator; System.load(path + getJnilibFileName()); installed = true; if (getLibraryVersion() != VERSION) { prepareLibrary(true); } } catch (Throwable t) { t.printStackTrace(); System.out.println("java.library.path=" + System.getenv("java.library.path")); installed = false; } } return installed; } native private static int getLibraryVersion(); native public static int getLocalFileAttributes(String path); native public static boolean isLocalFileHidden(String path); native public static boolean isLocalDirectory(String path); native public static boolean isLocalFileExecutable(String path); } ================================================ FILE: src/main/java/ru/trolsoft/macosx/FileLabelCache.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.macosx; import ch.randelshofer.quaqua.osx.OSXFile; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.impl.CachedFile; import com.mucommander.commons.file.impl.local.LocalFile; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.utils.FileIconsCache; import java.awt.*; import java.io.File; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; /** * Created on 27/12/16. * @author Oleg Trifonov */ public class FileLabelCache { private static final int CACHE_SIZE = 1000; private static final Color TRANSPARENT_COLOR = new Color(0, 0, 0, 0); private static volatile FileLabelCache instance; private final Map colors = new HashMap<>(); private final LinkedList files = new LinkedList<>(); public static FileLabelCache getInstance() { if (instance == null) { synchronized (FileIconsCache.class) { if (instance == null) { if (OsFamily.MAC_OS_X.isCurrent()) { instance = new FileLabelCache(); } else { instance = new FileLabelCache() { @Override public Color getLabelColor(AbstractFile file) { return null; } }; } } } } return instance; } public Color getLabelColor(AbstractFile file) { String path = file.getAbsolutePath(); Color result = colors.get(path); if (result != null) { // move record to top files.remove(path); files.addFirst(path); return result == TRANSPARENT_COLOR ? null : result; } return addColor(file); } private Color addColor(AbstractFile file) { Color color = getFileLabel(file); if (color == null) { color = TRANSPARENT_COLOR; } String path = file.getAbsolutePath(); colors.put(path, color); files.addFirst(path); // remove oldest record if the cache is full if (files.size() > CACHE_SIZE) { colors.remove(files.removeLast()); } return color == TRANSPARENT_COLOR ? null : color; } private Color getFileLabel(AbstractFile file) { if (file instanceof CachedFile) { file = ((CachedFile) file).getProxiedFile(); } if (file instanceof LocalFile) { File f = (File) file.getUnderlyingFileObject(); return OSXFile.getLabelColor(OSXFile.getLabel(f), 0); } return null; } } ================================================ FILE: src/main/java/ru/trolsoft/macosx/RetinaImageIcon.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.macosx; import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.lang.reflect.Field; import java.net.URL; /** * Created on 23/12/16. * @author Oleg Trifonov */ public class RetinaImageIcon extends ImageIcon { public static final boolean IS_RETINA = checkRetina(); /** * Creates an RetinaImageIcon from the specified URL. * * @param location the URL for the image */ public RetinaImageIcon(URL location) { super(location); } public RetinaImageIcon(Image img) { super(img); } /** * * @return true if Retina display found */ private static boolean checkRetina() { try { GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); Field field = graphicsDevice.getClass().getDeclaredField("scale"); field.setAccessible(true); Object scale = field.get(graphicsDevice); if (scale instanceof Integer && (Integer) scale == 2) { return true; } } catch (Exception ignore) { } return false; } @Override public int getIconWidth() { return IS_RETINA ? super.getIconWidth() / 2 : super.getIconWidth(); } @Override public int getIconHeight() { return IS_RETINA ? super.getIconHeight() / 2 : super.getIconHeight(); } @Override public synchronized void paintIcon(Component c, Graphics g, int x, int y) { if (IS_RETINA) { ImageObserver observer = getImageObserver(); if (observer == null) { observer = c; } Image image = getImage(); int width = image.getWidth(observer); int height = image.getHeight(observer); final Graphics2D g2d = (Graphics2D)g.create(x, y, width, height); g2d.scale(0.5, 0.5); g2d.drawImage(image, 0, 0, observer); g2d.scale(1, 1); g2d.dispose(); } else { super.paintIcon(c, g, x, y); } } public ImageIcon buildDisabledIcon() { return new RetinaImageIcon(GrayFilter.createDisabledImage(getImage())); } public static ImageIcon buildPaddedIcon(ImageIcon icon, Insets insets) { int scale = IS_RETINA ? 2 : 1; BufferedImage bi = new BufferedImage( scale*icon.getIconWidth() + scale*insets.left + scale*insets.right, scale*icon.getIconHeight() + scale*insets.top + scale*insets.bottom, BufferedImage.TYPE_INT_ARGB); Graphics g = bi.getGraphics(); g.drawImage(icon.getImage(), scale*insets.left, scale*insets.top, null); return new RetinaImageIcon(bi); } } ================================================ FILE: src/main/java/ru/trolsoft/ui/InputField.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2014 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.ui; import ru.trolsoft.utils.StrUtils; import javax.swing.JTextField; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.EventQueue; import java.awt.Toolkit; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; /** * Input filed for text and hex values */ public class InputField extends JTextField implements DocumentListener { public enum FilterType { ANY_TEXT, HEX_DUMP, HEX_LONG, DEC_LONG } private static final String HEX_SYMBOLS = "0123456789ABCDEF"; private static final String DEFAULT_ENCODING = "windows-1252"; private FilterType filterType = FilterType.HEX_DUMP; private int maxLength = 0xff; private boolean filtering = false; private String textEncoding; private List bindedFields; public InputField() { super(); init(); } public InputField(int columns) { super(columns); init(); } public InputField(int columns, FilterType filterType) { super(columns); this.filterType = filterType; init(); } public InputField(FilterType filterType) { super(); this.filterType = filterType; init(); } private void init() { getDocument().addDocumentListener(this); this.textEncoding = DEFAULT_ENCODING; } @Override public void insertUpdate(DocumentEvent e) { filterText(); } @Override public void removeUpdate(DocumentEvent e) { filterText(); } @Override public void changedUpdate(DocumentEvent e) { filterText(); } public FilterType getFilterType() { return filterType; } public void setFilterType(FilterType filterType) { this.filterType = filterType; } public int getMaxLength() { return maxLength; } public void setMaxLength(int maxLength) { this.maxLength = maxLength; } private void filterText() { if (filtering) { return; } // don't filter text value if (filterType == null || filterType == FilterType.ANY_TEXT) { onChange(); updateBindFields(); return; } filtering = true; EventQueue.invokeLater(() -> { String input = getText().toUpperCase(); String filtered = filterInput(input, maxLength); setText(filtered); updateBindFields(); onChange(); filtering = false; }); } private String filterInput(String input, int maxLength) { switch (filterType) { case HEX_DUMP: return filterHexString(input, maxLength); case DEC_LONG: return filterDecLong(input); case HEX_LONG: return filterHexLong(input); } return input; } private static String filterHexString(String input, int maxBytes) { StringBuilder filtered = new StringBuilder(); int index = 0; // filter for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); if (HEX_SYMBOLS.indexOf(c) >= 0) { filtered.append(c); if (index++ % 2 == 1 && i != input.length() - 1) filtered.append(' '); // whitespace after each byte } } // limit size if (maxBytes > 0) { if (filtered.length() > 3 * maxBytes) { filtered.setLength(3 * maxBytes); beep(); } } return filtered.toString(); } private String filterDecLong(String input) { if (input == null) { return null; } StringBuilder filtered = new StringBuilder(); for (int i = 0; i < input.length(); i++) { char ch = input.charAt(i); if (Character.isDigit(ch)) { filtered.append(ch); } } if (filtered.length() != input.length()) { beep(); } return filtered.toString(); } private String filterHexLong(String input) { if (input == null) { return null; } StringBuilder filtered = new StringBuilder(); for (int i = 0; i < input.length(); i++) { char ch = input.charAt(i); if (HEX_SYMBOLS.indexOf(ch) >= 0) { filtered.append(ch); } } if (filtered.length() != input.length()) { beep(); } return filtered.toString(); } private static void beep() { Toolkit.getDefaultToolkit().beep(); } public byte[] getBytes() { return StrUtils.hexStringToBytes(getText()); } public void setBytes(byte[] bytes) { if (bytes == null) { setText(""); } else { setText(StrUtils.bytesToHexStr(bytes, 0, bytes.length)); } } public long getValue() { switch (filterType) { case DEC_LONG: return Long.parseLong(getText()); case HEX_LONG: return Long.parseLong(getText(), 16); } return 0; } public void setValue(long val) { setText(Long.toString(val)); } public void bindField(InputField field) { if (bindedFields == null) { bindedFields = new ArrayList<>(); } bindedFields.add(field); } private void setTextWithoutFilter(String text) { filtering = true; setText(text); filtering = false; } private void updateBindFields() { if (bindedFields == null) { return; } final String src = getText(); for (InputField field : bindedFields) { String text; try { text = convert(src, filterType, field.getFilterType()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); text = ""; } if (!text.equals(field.getText())) { field.setTextWithoutFilter(text); } } } private String convert(String text, FilterType from, FilterType to) throws UnsupportedEncodingException { if (from == to) { return text; } if (from == FilterType.ANY_TEXT && to == FilterType.HEX_DUMP) { return StrUtils.bytesToHexString(text.getBytes(textEncoding)); } else if (from == FilterType.HEX_DUMP && to == FilterType.ANY_TEXT) { return new String(StrUtils.hexStringToBytes(text), textEncoding); } return text; } public String getTextEncoding() { return textEncoding; } public void setTextEncoding(String textEncoding) { this.textEncoding = textEncoding; } public boolean isEmpty() { return getText().isEmpty(); } public void onChange() { } } ================================================ FILE: src/main/java/ru/trolsoft/ui/TCheckBoxMenuItem.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.ui; import com.mucommander.commons.runtime.OsFamily; import javax.swing.*; import java.awt.*; /** * JCheckBoxMenuItem wrapper with fixed color (fix issues on linux with Cinnamon) * * @author Oleg Trifonov * Created on 21.09.16. */ public class TCheckBoxMenuItem extends JCheckBoxMenuItem { public TCheckBoxMenuItem() { super(); init(); } public TCheckBoxMenuItem(Icon icon) { super(icon); init(); } public TCheckBoxMenuItem(String text) { super(text); init(); } public TCheckBoxMenuItem(Action a) { super(a); init(); } public TCheckBoxMenuItem(String text, Icon icon) { super(text, icon); init(); } public TCheckBoxMenuItem(String text, boolean b) { super(text, b); init(); } public TCheckBoxMenuItem(String text, Icon icon, boolean b) { super(text, icon, b); init(); } private void init() { if (OsFamily.LINUX.isCurrent()) { setOpaque(true); setForeground(Color.BLACK); } } } ================================================ FILE: src/main/java/ru/trolsoft/ui/TMenuSeparator.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.ui; import com.mucommander.commons.runtime.OsFamily; import javax.swing.*; import java.awt.*; /** * JSeparator that fixed visibility issue on linux with Cinnamon * * @author Oleg Trifonov * Created on 21.09.16. */ public class TMenuSeparator extends JSeparator { public TMenuSeparator() { super(); if (OsFamily.LINUX.isCurrent() && getPreferredSize().height <= 0) { Dimension d = getPreferredSize(); d.height = 1; setPreferredSize(d); } } } ================================================ FILE: src/main/java/ru/trolsoft/ui/TProgressBar.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.ui; import javax.swing.JProgressBar; import java.awt.*; /** * @author Oleg Trifonov * Created on 06/07/16. */ public class TProgressBar extends JProgressBar { private static final Color GRADIENT_ENDING_COLOR = new Color(0xc0c0c0); private static final Color BORDER_COLOR = new Color(0x736a60); private static final Color DISABLED_BORDER_COLOR = new Color(0xbebebe); public static final Color PREFERRED_PROGRESS_COLOR = new Color(0x1869A6);//new Color(0x3889F6); private static final Composite TRANSPARENT = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.45f); private static final Composite VERY_TRANSPARENT = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f); private static final Composite NOT_TRANSPARENT = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f); private static GradientPaint gradient; public TProgressBar() { setFont(getFont().deriveFont(Font.BOLD)); } @Override protected void paintComponent(Graphics g) { int w = getWidth() - 1; int h = getHeight() - 1; if (gradient == null) { gradient = new GradientPaint(0.0f, 0.0f, Color.WHITE, 0.0f, h, GRADIENT_ENDING_COLOR); } Graphics2D g2d = (Graphics2D) g; // Clean background if (isOpaque()) { g2d.setColor(getBackground()); g2d.fillRect(0, 0, getWidth(), getHeight()); } // Control Border g2d.setColor(isEnabled() ? BORDER_COLOR : DISABLED_BORDER_COLOR); g2d.drawLine(1, 0, w - 1, 0); g2d.drawLine(1, h, w - 1, h); g2d.drawLine(0, 1, 0, h - 1); g2d.drawLine(w, 1, w, h - 1); // Fill in the progress int min = getMinimum(); int max = getMaximum(); int total = max - min; float dx = (float) (w - 2) / (float) total; int value = getValue(); int progress = value >= max ? w - 1 : (int) (dx * value); g2d.setColor(PREFERRED_PROGRESS_COLOR); g2d.fillRect(1, 1, progress, h - 1); // A gradient over the progress fill g2d.setPaint(gradient); g2d.setComposite(TRANSPARENT); g2d.fillRect(1, 1, w - 1, (h >> 1)); final float FACTOR = 0.20f; g2d.fillRect(1, h - (int) (h * FACTOR), w - 1, (int) (h * FACTOR)); g2d.setComposite(VERY_TRANSPARENT); final int n = 10; int delta = w/n; int i = 0; if (isEnabled()) { for (int xx = delta; xx < w; xx += delta) { i++; if (value > i*n) { //g2d.setColor(getBackground()); //g2d.drawLine(xx-1, 1, xx-1, h - 1); g2d.setColor(Color.GRAY); g2d.drawLine(xx, 1, xx, h - 1); } g2d.setColor(Color.WHITE); g2d.drawLine(xx + 1, 1, xx + 1, h - 1); } } else { for (int xx = 0; xx < w; xx += delta) { i++; if (value > i*n) { g2d.setColor(Color.RED); g2d.drawLine(xx, h - 1, xx + h, 1); } g2d.setColor(Color.WHITE); g2d.drawLine(xx + 1, h - 1, xx + 1 + h, 1); } } FontMetrics fm = g.getFontMetrics(); int stringW = fm.stringWidth(getString()); int stringH = ((h - fm.getHeight()) / 2) + fm.getAscent(); g2d.setComposite(NOT_TRANSPARENT); g2d.setColor(getForeground()); g2d.setFont(getFont()); g2d.drawString(getString(), (w - stringW)/2, stringH); } } ================================================ FILE: src/main/java/ru/trolsoft/ui/TRadioButtonMenuItem.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.ui; import com.mucommander.commons.runtime.OsFamily; import javax.swing.Action; import javax.swing.Icon; import javax.swing.JRadioButtonMenuItem; import java.awt.Color; public class TRadioButtonMenuItem extends JRadioButtonMenuItem { public TRadioButtonMenuItem() { init(); } public TRadioButtonMenuItem(Icon icon) { super(icon); init(); } public TRadioButtonMenuItem(String text) { super(text); init(); } public TRadioButtonMenuItem(Action a) { super(a); init(); } public TRadioButtonMenuItem(String text, Icon icon) { super(text, icon); init(); } public TRadioButtonMenuItem(String text, boolean selected) { super(text, selected); init(); } public TRadioButtonMenuItem(Icon icon, boolean selected) { super(icon, selected); init(); } public TRadioButtonMenuItem(String text, Icon icon, boolean selected) { super(text, icon, selected); init(); } private void init() { if (OsFamily.LINUX.isCurrent()) { setOpaque(true); setForeground(Color.BLACK); } } } ================================================ FILE: src/main/java/ru/trolsoft/ui/ZxSpectrumLoadPane.java ================================================ package ru.trolsoft.ui; import javax.swing.*; import java.awt.*; import java.awt.event.ActionListener; import java.util.Random; public class ZxSpectrumLoadPane extends JPanel { private static final Color COLOR_BLUE = new Color(0x0001c8); private static final Color COLOR_YELLOW = new Color(0xbbbf0a); // private final Color COLOR_RED = new Color(0xaa0000); // private final Color COLOR_CYAN = new Color(0x00aaaa); private static final Random r = new Random(); private int randomPrev; private final Timer timer; public ZxSpectrumLoadPane() { this(null); } public ZxSpectrumLoadPane(LayoutManager layout) { super(layout); timer = initTimer(); } private Timer initTimer() { ActionListener updater = evt -> repaint(); return new Timer(30, updater); } public void stop() { timer.stop(); SwingUtilities.invokeLater(this::repaint); } public void start() { timer.start(); } @Override public void paint(Graphics g) { if (timer.isRunning()) { final int dx = 100; final int dy = 50; final int stripeH = 6; final int w = getWidth(); final int h = getHeight(); g.setClip(4, 4, w - 8, h - 8); int y = 0; boolean firstColor = true; while (y < h) { g.setColor(firstColor ? COLOR_BLUE : COLOR_YELLOW); int rectH = nextBit() ? 2 * stripeH : stripeH; g.fillRect(0, y, w, rectH); y += rectH; firstColor = !firstColor; } g.setClip(dx, dy, w - 2 * dx, h - 2 * dy); } super.paint(g); } private boolean nextBit() { if (randomPrev == 0) { randomPrev = r.nextBoolean() ? 2 : 1; return randomPrev == 2; } boolean res = randomPrev == 2; randomPrev = 0; return res; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/FileUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils; import com.mucommander.commons.io.StreamUtils; import java.io.*; import java.net.URISyntaxException; /** * Created on 08.01.15. * @author Oleg Trifonov */ public class FileUtils { public static void copyFileFromJar(String src, String dest, boolean overwrite) throws IOException { File fileDest = new File(dest); if (!overwrite && fileDest.exists() && fileDest.length() > 0) { return; } fileDest.getParentFile().mkdirs(); InputStream is = FileUtils.class.getResourceAsStream(src); OutputStream os = new FileOutputStream(dest); StreamUtils.copyStream(is, os); is.close(); os.close(); } public static void copyFromJarFile(String name, String jarPath, boolean overwrite) throws IOException { final String outFile = jarPath + File.separatorChar + name; if (overwrite || !new File(outFile).exists()) { copyFileFromJar('/' + name, outFile, overwrite); } } public static void copyFromJarFile(String name, String jarPath) throws IOException { copyFromJarFile(name, jarPath, false); } public static String getJarPath() { try { return new File(FileUtils.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getParent(); } catch (URISyntaxException e) { e.printStackTrace(); return null; } } } ================================================ FILE: src/main/java/ru/trolsoft/utils/ImageSizeDetector.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2017 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils; import java.io.IOException; import java.io.InputStream; /** * @author Oleg Trifonov * Created on 11/01/17. */ public class ImageSizeDetector { public enum ImageType { GIF, PNG, JPEG, BMP, TIFF } private int height; private int width; private ImageType type; public ImageSizeDetector(InputStream is) throws IOException { process(is); } private void process(InputStream is) throws IOException { int c1 = is.read(); int c2 = is.read(); int c3 = is.read(); width = height = -1; if (c1 == 'G' && c2 == 'I' && c3 == 'F') { // GIF is.skip(3); width = readWordLH(is); height = readWordLH(is); type = ImageType.GIF; } else if (c1 == 0xFF && c2 == 0xD8) { // JPG while (c3 == 255) { int marker = is.read(); int len = readWordHL(is); if (marker == 192 || marker == 193 || marker == 194) { is.skip(1); height = readWordHL(is); width = readWordHL(is); type = ImageType.JPEG; break; } is.skip(len - 2); c3 = is.read(); } } else if (c1 == 137 && c2 == 80 && c3 == 78) { // PNG is.skip(15); width = readWordHL(is); is.skip(2); height = readWordHL(is); type = ImageType.PNG; } else if (c1 == 66 && c2 == 77) { // BMP is.skip(15); width = readWordLH(is); is.skip(2); height = readWordLH(is); type = ImageType.BMP; } else { int c4 = is.read(); if ((c1 == 'M' && c2 == 'M' && c3 == 0 && c4 == 42) || (c1 == 'I' && c2 == 'I' && c3 == 42 && c4 == 0)) { //TIFF boolean bigEndian = c1 == 'M'; int entries; int ifd = readDword(is, bigEndian); is.skip(ifd - 8); entries = readWord(is, bigEndian); for (int i = 1; i <= entries; i++) { int tag = readWord(is, bigEndian); int fieldType = readWord(is, bigEndian); //long count = readDword(is, bigEndian); int valOffset; if ((fieldType == 3 || fieldType == 8)) { valOffset = readWord(is, bigEndian); is.skip(2); } else { valOffset = readDword(is, bigEndian); } if (tag == 256) { width = valOffset; } else if (tag == 257) { height = valOffset; } if (width != -1 && height != -1) { type = ImageType.TIFF; break; } } } } } private static int readWordLH(InputStream is) throws IOException { int b1 = is.read(); int b2 = is.read(); return b2 >= 0 ? b1 + (b2 << 8) : -1; } private static int readWordHL(InputStream is) throws IOException { int b1 = is.read(); int b2 = is.read(); return b2 >= 0 ? b2 + (b1 << 8) : -1; } private static int readWord(InputStream is, boolean hiLo) throws IOException { return hiLo ? readWordHL(is) : readWordLH(is); } private static int readDwordLH(InputStream is) throws IOException { int b1 = is.read(); int b2 = is.read(); int b3 = is.read(); int b4 = is.read(); return b4 >= 0 ? b1 + (b2 << 8) + (b3 << 16) + (b4 << 24): -1; } private static int readDwordHL(InputStream is) throws IOException { int b1 = is.read(); int b2 = is.read(); int b3 = is.read(); int b4 = is.read(); return b4 >= 0 ? b4 + (b3 << 8) + (b2 << 16) + (b1 << 24): -1; } private static int readDword(InputStream is, boolean hiLo) throws IOException { return hiLo ? readDwordHL(is) : readDwordLH(is); } public int getWidth() { return width; } public int getHeight() { return height; } public ImageType getType() { return type; } @Override public String toString() { return type + "\t Width : " + width + "\t Height : " + height; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/JavaClassVersionDetector.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2026 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils; import java.io.IOException; import java.io.InputStream; /** * @author Oleg Trifonov * Created on 19/01/17. */ public class JavaClassVersionDetector { private int major; private int minor; private final Version version; public enum Version { WRONG_FORMAT(-1, -1, "wrong"), VER_1_0(45, 0, "1.0"), VER_1_1(45, 3, "1.1"), VER_1_2(46, 0, "1.2"), VER_1_3(47, 0, "1.3"), VER_1_4(48, 0, "1.4"), VER_1_5(49, 0, "1.5"), VER_1_6(50, 0, "1.6"), VER_1_7(51, 0, "1.7"), VER_1_8(52, 0, "1.8"), VER_9(53, 0, "9"), VER_10(54, 0, "10"), VER_11(55, 0, "11 (LTS)"), VER_12(56, 0, "12"), VER_13(57, 0, "13"), VER_14(58, 0, "14"), VER_15(59, 0, "15"), VER_16(60, 0, "16"), VER_17(61, 0, "17 (LTS)"), VER_18(62, 0, "18"), VER_19(63, 0, "19"), VER_20(64, 0, "20"), VER_21(65, 0, "21 (LTS)"), VER_22(66, 0, "22"), VER_23(67, 0, "23"), VER_24(68, 0, "24"), VER_25(69, 0, "25 (LTS)"), VER_26(70, 0, "26"), UNKNOWN(-1, -1, "unknown"); private final int major; private final int minor; public final String name; Version(int major, int minor, String name) { this.major = major; this.minor = minor; this.name = name; } } public JavaClassVersionDetector(InputStream is) throws IOException { version = process(is); } private Version process(InputStream is) throws IOException { int b1 = is.read(); int b2 = is.read(); int b3 = is.read(); int b4 = is.read(); if (b1 != 0xCA && b2 != 0xFE && b3 != 0xBA && b4 != 0xBE) { return Version.WRONG_FORMAT; } minor = (is.read() << 8) + is.read(); major = (is.read() << 8) + is.read(); for (Version ver : Version.values()) { if (ver.minor == minor && ver.major == major) { return ver; } } return Version.UNKNOWN; } public int getMajor() { return major; } public int getMinor() { return minor; } public Version getVersion() { return version; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/StrUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2020 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils; /** * @author Oleg Trifonov * Created on 03/04/14. */ public class StrUtils { private static final char[] HEX_CHAR_ARRAY = "0123456789ABCDEF".toCharArray(); private static final String[] STRING_OF_ZERO = {"", "0", "00", "000", "0000", "00000", "000000", "0000000", "00000000", "000000000", "0000000000"}; private static final String[] HEX_BYTE_STRINGS = new String[256]; private static final String[] BINARY_BYTE_STRINGS = new String[256]; private static final String[] OCTAL_BYTE_STRINGS = new String[256]; private static final char UTF_16_BE_MARKER = 0xFEFF; private static final char UTF_16_LE_MARKER = 0xFFFE; public static String dwordToHexStr(long val) { String result = Long.toHexString(val); int len = result.length(); if (len > 8) { return result; } return STRING_OF_ZERO[8-len] + result; } public static String byteToHexStr(byte b) { int v = b & 0xFF; String result = HEX_BYTE_STRINGS[v]; if (result == null) { result = Character.toString(HEX_CHAR_ARRAY[v >>> 4]) + HEX_CHAR_ARRAY[v & 0x0f]; HEX_BYTE_STRINGS[v] = result; } return result; } public static String bytesToHexStr(byte[] bytes, int offset, int size) { char[] hexChars = new char[size * 2]; for (int i = offset; i < offset + size; i++) { int v = bytes[i] & 0xFF; hexChars[i * 2] = HEX_CHAR_ARRAY[v >>> 4]; hexChars[i * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F]; } return new String(hexChars); } public static String byteToBinaryStr(byte b) { int v = b & 0xFF; return byteToBinaryStr(v); } public static String byteToBinaryStr(int v) { String result = BINARY_BYTE_STRINGS[v]; if (result == null) { result = Integer.toBinaryString(v); result = STRING_OF_ZERO[8 - result.length()] + result; BINARY_BYTE_STRINGS[v] = result; } return result; } public static String byteToOctalStr(byte b) { int v = b & 0xFF; return byteToOctalStr(v); } public static String byteToOctalStr(int v) { String result = OCTAL_BYTE_STRINGS[v]; if (result == null) { result = Integer.toOctalString(v); result = STRING_OF_ZERO[3 - result.length()] + result; OCTAL_BYTE_STRINGS[v] = result; } return result; } public static String bytesToHexString(byte[] bytes) { StringBuilder s = new StringBuilder(); for (byte aByte : bytes) { s.append(StrUtils.byteToHexStr(aByte)); s.append(' '); } return s.toString(); } public static byte[] hexStringToBytes(String text) { int len = text.length(); int i = 0; char c1 = 0; char c2 = 0; int outPos = 0; byte[] array = new byte[(len+1)/2]; while (i <= len) { if (c2 != 0) { int b1 = parseNibble(c1); int b2 = parseNibble(c2); if (b1 < 0 || b2 < 0) { return new byte[0]; } array[outPos++] = (byte)((b1 << 4) + b2); c1 = 0; c2 = 0; } if (i == len) break; char nextNibble = text.charAt(i++); if (nextNibble == ' ' || nextNibble == '\t') { continue; } if (c1 == 0) { c1 = nextNibble; } else { c2 = nextNibble; } } if (c1 != 0) { int b1 = parseNibble(c1); if (b1 < 0) { return new byte[0]; } array[outPos++] = (byte)b1; } if (array.length == outPos) { return array; } else { byte[] result = new byte[outPos]; System.arraycopy(array, 0, result, 0, outPos); return result; } } private static int parseNibble(char c) { if (c >= '0' && c <= '9') { return c - '0'; } else if (c >= 'A' && c <= 'F') { return c - 'A' + 10; } else if (c >= 'a' && c <= 'f') { return c - 'a' + 10; } else { return -1; } } public static boolean hasUtfMarker(String s) { if (s == null || s.isEmpty()) { return false; } char firstChar = s.charAt(0); return firstChar == UTF_16_BE_MARKER || firstChar == UTF_16_LE_MARKER; } public static String removeUtfMarker(String s) { if (s == null) { return null; } return hasUtfMarker(s) ? s.substring(1) : s; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/StringStream.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2018 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils; import java.util.LinkedList; public class StringStream { private final LinkedList lines = new LinkedList<>(); private StringBuffer lastIncomplete; public void add(String s) { String[] array = s.split("(?<=\n)"); for (int i = 0; i < array.length; i++) { String line = array[i]; if (i == array.length-1 && !line.endsWith("\n")) { getLastIncomplete().append(line); } else if (i == 0 && hasRemains()) { lines.add(getRemains() + line); } else { lines.add(line); } } } public boolean hasCompleted() { return !lines.isEmpty(); } public String getNext() { return lines.pollFirst(); } public String getRemains() { if (lastIncomplete != null) { String result = lastIncomplete.toString(); lastIncomplete.delete(0, lastIncomplete.length()); return result; } else { return ""; } } public boolean hasRemains() { return lastIncomplete != null && !lastIncomplete.isEmpty(); } private StringBuffer getLastIncomplete() { if (lastIncomplete == null) { lastIncomplete = new StringBuffer(); } return lastIncomplete; } public void clear() { lines.clear(); lastIncomplete = null; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/BytesSearchPattern.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; /** * @author Oleg Trifonov * Created on 08/12/14. */ public class BytesSearchPattern implements SearchPattern { private final byte[] bytes; public BytesSearchPattern(byte[] bytes) { this.bytes = bytes; } @Override public int length() { return bytes.length; } @Override public boolean checkByte(int index, int val) { return (bytes[index] & 0xff) == val; } @Override public boolean checkSelf(int index1, int index2) { return bytes[index1] == bytes[index2]; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/InputStreamSource.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; /** * @author Oleg Trifonov * Created on 18/11/14. */ public class InputStreamSource implements SearchSourceStream { private final InputStream is; private int next; private boolean closed; public InputStreamSource(InputStream is) { this.is = new BufferedInputStream(is); } @Override public boolean hasNext() throws SearchException { try { next = is.read(); } catch (IOException e) { throw new SearchException(e); } return next >= 0; } @Override public void close() { if (closed) { return; } try { is.close(); closed = true; } catch (IOException e) { e.printStackTrace(); } } @Override public int next() throws SearchException { if (next < 0) { throw new SearchException("next < 0"); } return next; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/SearchException.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; /** * @author Oleg Trifonov * Created on 18/11/14. */ public class SearchException extends Exception { /** * Constructs a new exception with {@code null} as its detail message. * The cause is not initialized, and may subsequently be initialized by a * call to {@link #initCause}. */ public SearchException() { super(); } /** * Constructs a new exception with the specified detail message. The * cause is not initialized, and may subsequently be initialized by * a call to {@link #initCause}. * * @param message the detail message. The detail message is saved for * later retrieval by the {@link #getMessage()} method. */ public SearchException(String message) { super(message); } /** * Constructs a new exception with the specified detail message and * cause.

    Note that the detail message associated with * {@code cause} is not automatically incorporated in * this exception's detail message. * * @param message the detail message (which is saved for later retrieval * by the {@link #getMessage()} method). * @param cause the cause (which is saved for later retrieval by the * {@link #getCause()} method). (A null value is * permitted, and indicates that the cause is nonexistent or * unknown.) */ public SearchException(String message, Throwable cause) { super(message, cause); } /** * Constructs a new exception with the specified cause and a detail * message of (cause==null ? null : cause.toString()) (which * typically contains the class and detail message of cause). * This constructor is useful for exceptions that are little more than * wrappers for other throwables (for example, {@link * java.security.PrivilegedActionException}). * * @param cause the cause (which is saved for later retrieval by the * {@link #getCause()} method). (A null value is * permitted, and indicates that the cause is nonexistent or * unknown.) */ public SearchException(Throwable cause) { super(cause); } } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/SearchPattern.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; /** * @author Oleg Trifonov * Created on 16/11/14. */ public interface SearchPattern { /** * * @return length of the search pattern */ int length(); /** * * @param index offset in pattern * @param val compared value * @return true if pattern[index] == val */ boolean checkByte(int index, int val); boolean checkSelf(int index1, int index2); } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/SearchSourceStream.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; /** * @author Oleg Trifonov * Created on 16/11/14. */ public interface SearchSourceStream extends AutoCloseable { int next() throws SearchException; boolean hasNext() throws SearchException; void close(); } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/SearchUtils.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; /** * @author Oleg Trifonov * Created on 16/11/14. */ public class SearchUtils { public static long indexOf(SearchSourceStream source, SearchPattern pattern) throws SearchException { if (!source.hasNext() || pattern.length() == 0) { return -1; } int[] failure = computeFailure(pattern); int j = 0; long i = 0; while (source.hasNext()) { int b = source.next(); i++; while (j > 0 && !pattern.checkByte(j, b)) { j = failure[j - 1]; } if (pattern.checkByte(j, b)) { j++; } if (j == pattern.length()) { return i - pattern.length() + 1; } } source.close(); return -1; } /** * Computes the failure function using a boot-strapping process, * where the pattern is matched against itself. */ private static int[] computeFailure(SearchPattern pattern) { int[] failure = new int[pattern.length()]; int j = 0; for (int i = 1; i < pattern.length(); i++) { while (j > 0 && !pattern.checkSelf(i, j)) { j = failure[j - 1]; } if (pattern.checkSelf(i, j)) { j++; } failure[i] = j; } return failure; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/StringCaseInsensitiveSearchPattern.java ================================================ /* * This file is part of trolCommander, http://www.trolsoft.ru/en/soft/trolcommander * Copyright (C) 2013-2016 Oleg Trifonov * * trolCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * trolCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package ru.trolsoft.utils.search; import java.io.UnsupportedEncodingException; /** * @author Oleg Trifonov * Created on 19/11/14. */ public class StringCaseInsensitiveSearchPattern implements SearchPattern { private final byte[] data; private final byte[] dataAlt; public StringCaseInsensitiveSearchPattern(String s, String charset) throws UnsupportedEncodingException { this.data = s.toLowerCase().getBytes(charset); this.dataAlt = s.toUpperCase().getBytes(charset); } @Override public int length() { return data.length; } @Override public boolean checkByte(int index, int val) { return (data[index] & 0xff) == val || (dataAlt[index] & 0xff) == val; } @Override public boolean checkSelf(int index1, int index2) { return data[index1] == data[index2] || dataAlt[index1] == dataAlt[index2] || data[index1] == dataAlt[index2] || dataAlt[index1] == data[index2]; } } ================================================ FILE: src/main/java/ru/trolsoft/utils/search/StringCaseSensitiveSearchPattern.java ================================================ package ru.trolsoft.utils.search; import java.io.UnsupportedEncodingException; /** * @author Oleg Trifonov * Created on 19/11/14. */ public class StringCaseSensitiveSearchPattern implements SearchPattern { private final byte[] data; public StringCaseSensitiveSearchPattern(String s, String charset) throws UnsupportedEncodingException { this.data = s.getBytes(charset); } @Override public int length() { return data.length; } @Override public boolean checkByte(int index, int val) { return (data[index] & 0xff) == val; } @Override public boolean checkSelf(int index1, int index2) { return data[index1] == data[index2]; } } ================================================ FILE: src/main/java/se/vidstige/jadb/AdbFilterInputStream.java ================================================ package se.vidstige.jadb; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; public class AdbFilterInputStream extends FilterInputStream { public AdbFilterInputStream(InputStream inputStream) { super(inputStream); } @Override public int read() throws IOException { int b1 = in.read(); if (b1 == 0x0d) { in.mark(1); int b2 = in.read(); if (b2 == 0x0a) { return b2; } in.reset(); } return b1; } @Override public int read(byte[] buffer, int offset, int length) throws IOException { int n = 0; for (int i = 0; i < length; i++) { int b = read(); if (b == -1) return n == 0 ? -1 : n; buffer[offset + n] = (byte) b; n++; // Return as soon as no more data is available (and at least one byte was read) if (in.available() <= 0) { return n; } } return n; } @Override public int read(byte[] buffer) throws IOException { return read(buffer, 0, buffer.length); } } ================================================ FILE: src/main/java/se/vidstige/jadb/AdbFilterOutputStream.java ================================================ package se.vidstige.jadb; import java.io.IOException; import java.io.OutputStream; public class AdbFilterOutputStream extends LookBackFilteringOutputStream { public AdbFilterOutputStream(OutputStream inner) { super(inner, 1); } @Override public void write(int c) throws IOException { if (!lookback().isEmpty()) { Byte last = lookback().getFirst(); if (last != null && last == 0x0d && c == 0x0a) { unwrite(); } } super.write(c); } } ================================================ FILE: src/main/java/se/vidstige/jadb/AdbServerLauncher.java ================================================ package se.vidstige.jadb; import java.io.IOException; import java.util.Map; /** * Launches the ADB server */ public class AdbServerLauncher { private final String executable; private final Subprocess subprocess; /** * Creates a new launcher loading ADB from the environment. * * @param subprocess the sub-process. * @param environment the environment to use to locate the ADB executable. */ public AdbServerLauncher(Subprocess subprocess, Map environment) { this.subprocess = subprocess; this.executable = findAdbExecutable(environment); } /** * Creates a new launcher with the specified ADB. * * @param subprocess the sub-process. * @param executable the location of the ADB executable. */ public AdbServerLauncher(Subprocess subprocess, String executable) { this.subprocess = subprocess; this.executable = executable; } private static String findAdbExecutable(Map environment) { String androidHome = environment.get("ANDROID_HOME"); if (androidHome == null || androidHome.isEmpty()) { return "adb"; } return androidHome + "/platform-tools/adb"; } public void launch() throws IOException, InterruptedException { Process p = subprocess.execute(new String[]{executable, "start-server"}); p.waitFor(); int exitValue = p.exitValue(); if (exitValue != 0) { throw new IOException("adb exited with exit code: " + exitValue); } } } ================================================ FILE: src/main/java/se/vidstige/jadb/ConnectionToRemoteDeviceException.java ================================================ package se.vidstige.jadb; public class ConnectionToRemoteDeviceException extends Exception { public ConnectionToRemoteDeviceException(String message) { super(message); } } ================================================ FILE: src/main/java/se/vidstige/jadb/DeviceDetectionListener.java ================================================ package se.vidstige.jadb; import java.util.List; public interface DeviceDetectionListener { void onDetect(List devices); void onException(Exception e); } ================================================ FILE: src/main/java/se/vidstige/jadb/DeviceWatcher.java ================================================ package se.vidstige.jadb; import java.io.IOException; public class DeviceWatcher implements Runnable { private Transport transport; private final DeviceDetectionListener listener; private final JadbConnection connection; public DeviceWatcher(Transport transport, DeviceDetectionListener listener, JadbConnection connection) { this.transport = transport; this.listener = listener; this.connection = connection; } @Override public void run() { watch(); } public void watch() { try { while (true) { listener.onDetect(connection.parseDevices(transport.readString())); } } catch (IOException ioe) { synchronized(this) { if (transport != null) { listener.onException(ioe); } } } catch (Exception e) { listener.onException(e); } } public void stop() throws IOException { synchronized(this) { transport.close(); transport = null; } } } ================================================ FILE: src/main/java/se/vidstige/jadb/HostConnectToRemoteTcpDevice.java ================================================ package se.vidstige.jadb; import java.io.IOException; import java.net.InetSocketAddress; class HostConnectToRemoteTcpDevice { private final Transport transport; private final ResponseValidator responseValidator; HostConnectToRemoteTcpDevice(Transport transport) { this.transport = transport; this.responseValidator = new ResponseValidatorImp(); } //Visible for testing HostConnectToRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) { this.transport = transport; this.responseValidator = responseValidator; } InetSocketAddress connect(InetSocketAddress inetSocketAddress) throws IOException, JadbException, ConnectionToRemoteDeviceException { transport.send(String.format("host:connect:%s:%d", inetSocketAddress.getHostString(), inetSocketAddress.getPort())); verifyTransportLevel(); verifyProtocolLevel(); return inetSocketAddress; } private void verifyTransportLevel() throws IOException, JadbException { transport.verifyResponse(); } private void verifyProtocolLevel() throws IOException, ConnectionToRemoteDeviceException { String status = transport.readString(); responseValidator.validate(status); } //@VisibleForTesting interface ResponseValidator { void validate(String response) throws ConnectionToRemoteDeviceException; } final static class ResponseValidatorImp implements ResponseValidator { private final static String SUCCESSFULLY_CONNECTED = "connected to"; private final static String ALREADY_CONNECTED = "already connected to"; ResponseValidatorImp() { } public void validate(String response) throws ConnectionToRemoteDeviceException { if (!checkIfConnectedSuccessfully(response) && !checkIfAlreadyConnected(response)) { throw new ConnectionToRemoteDeviceException(extractError(response)); } } private boolean checkIfConnectedSuccessfully(String response) { return response.startsWith(SUCCESSFULLY_CONNECTED); } private boolean checkIfAlreadyConnected(String response) { return response.startsWith(ALREADY_CONNECTED); } private String extractError(String response) { int lastColon = response.lastIndexOf(":"); if (lastColon != -1) { return response.substring(lastColon); } else { return response; } } } } ================================================ FILE: src/main/java/se/vidstige/jadb/HostDisconnectFromRemoteTcpDevice.java ================================================ package se.vidstige.jadb; import java.io.IOException; import java.net.InetSocketAddress; public class HostDisconnectFromRemoteTcpDevice { private final Transport transport; private final ResponseValidator responseValidator; HostDisconnectFromRemoteTcpDevice(Transport transport) { this.transport = transport; this.responseValidator = new ResponseValidatorImp(); } //Visible for testing HostDisconnectFromRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) { this.transport = transport; this.responseValidator = responseValidator; } InetSocketAddress disconnect(InetSocketAddress inetSocketAddress) throws IOException, JadbException, ConnectionToRemoteDeviceException { transport.send(String.format("host:disconnect:%s:%d", inetSocketAddress.getHostString(), inetSocketAddress.getPort())); verifyTransportLevel(); verifyProtocolLevel(); return inetSocketAddress; } private void verifyTransportLevel() throws IOException, JadbException { transport.verifyResponse(); } private void verifyProtocolLevel() throws IOException, ConnectionToRemoteDeviceException { String status = transport.readString(); responseValidator.validate(status); } //@VisibleForTesting interface ResponseValidator { void validate(String response) throws ConnectionToRemoteDeviceException; } final static class ResponseValidatorImp implements ResponseValidator { private final static String SUCCESSFULLY_DISCONNECTED = "disconnected"; private final static String ALREADY_DISCONNECTED = "error: no such device"; ResponseValidatorImp() { } public void validate(String response) throws ConnectionToRemoteDeviceException { if (!checkIfConnectedSuccessfully(response) && !checkIfAlreadyConnected(response)) { throw new ConnectionToRemoteDeviceException(extractError(response)); } } private boolean checkIfConnectedSuccessfully(String response) { return response.startsWith(SUCCESSFULLY_DISCONNECTED); } private boolean checkIfAlreadyConnected(String response) { return response.startsWith(ALREADY_DISCONNECTED); } private String extractError(String response) { int lastColon = response.lastIndexOf(":"); if (lastColon != -1) { return response.substring(lastColon); } else { return response; } } } } ================================================ FILE: src/main/java/se/vidstige/jadb/ITransportFactory.java ================================================ package se.vidstige.jadb; import java.io.IOException; /** * Created by Törcsi on 2016. 03. 01.. */ public interface ITransportFactory { Transport createTransport() throws IOException; } ================================================ FILE: src/main/java/se/vidstige/jadb/JadbConnection.java ================================================ package se.vidstige.jadb; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.util.ArrayList; import java.util.List; public class JadbConnection implements ITransportFactory { private final String host; private final int port; private static final int DEFAULTPORT = 5037; public JadbConnection() { this("localhost", DEFAULTPORT); } private JadbConnection(String host, int port) { this.host = host; this.port = port; } public Transport createTransport() throws IOException { return new Transport(new Socket(host, port)); } public String getHostVersion() throws IOException, JadbException { Transport main = createTransport(); main.send("host:version"); main.verifyResponse(); String version = main.readString(); main.close(); return version; } public InetSocketAddress connectToTcpDevice(InetSocketAddress inetSocketAddress) throws IOException, JadbException, ConnectionToRemoteDeviceException { Transport transport = createTransport(); try { return new HostConnectToRemoteTcpDevice(transport).connect(inetSocketAddress); } finally { transport.close(); } } public InetSocketAddress disconnectFromTcpDevice(InetSocketAddress tcpAddressEntity) throws IOException, JadbException, ConnectionToRemoteDeviceException { Transport transport = createTransport(); try { return new HostDisconnectFromRemoteTcpDevice(transport).disconnect(tcpAddressEntity); } finally { transport.close(); } } public List getDevices() throws IOException, JadbException { Transport devices = createTransport(); devices.send("host:devices"); devices.verifyResponse(); String body = devices.readString(); devices.close(); return parseDevices(body); } public DeviceWatcher createDeviceWatcher(DeviceDetectionListener listener) throws IOException, JadbException { Transport transport = createTransport(); transport.send("host:track-devices"); transport.verifyResponse(); return new DeviceWatcher(transport, listener, this); } List parseDevices(String body) { String[] lines = body.split("\n"); ArrayList devices = new ArrayList<>(lines.length); for (String line : lines) { String[] parts = line.split("\t"); if (parts.length > 1) { devices.add(new JadbDevice(parts[0], parts[1], this)); } } return devices; } public JadbDevice getAnyDevice() { return JadbDevice.createAny(this); } } ================================================ FILE: src/main/java/se/vidstige/jadb/JadbDevice.java ================================================ package se.vidstige.jadb; import se.vidstige.jadb.managers.Bash; import java.io.*; import java.util.ArrayList; import java.util.List; public class JadbDevice { public enum State { UNKNOWN, OFFLINE, DEVICE, BOOTLOADER } private final String serial; private final ITransportFactory transportFactory; JadbDevice(String serial, String type, ITransportFactory tFactory) { this.serial = serial; this.transportFactory = tFactory; } static JadbDevice createAny(JadbConnection connection) { return new JadbDevice(connection); } private JadbDevice(ITransportFactory tFactory) { serial = null; this.transportFactory = tFactory; } private State convertState(String type) { switch (type) { case "device": return State.DEVICE; case "offline": return State.OFFLINE; case "bootloader": return State.BOOTLOADER; default: return State.UNKNOWN; } } private Transport getTransport() throws IOException, JadbException { Transport transport = transportFactory.createTransport(); if (serial == null) { transport.send("host:transport-any"); transport.verifyResponse(); } else { transport.send("host:transport:" + serial); transport.verifyResponse(); } return transport; } public String getSerial() { return serial; } public State getState() throws IOException, JadbException { Transport transport = transportFactory.createTransport(); if (serial == null) { transport.send("host:get-state"); transport.verifyResponse(); } else { transport.send("host-serial:" + serial + ":get-state"); transport.verifyResponse(); } State state = convertState(transport.readString()); transport.close(); return state; } /**

    Execute a shell command. * *

    For Lollipop and later see: {@link #execute(String, String...)} * * @param command main command to run. E.g. "ls" * @param args arguments to the command. * @return combined stdout/stderr stream. * @throws IOException * @throws JadbException */ public InputStream executeShell(String command, String... args) throws IOException, JadbException { Transport transport = getTransport(); StringBuilder shellLine = buildCmdLine(command, args); send(transport, "shell:" + shellLine.toString()); return new AdbFilterInputStream(new BufferedInputStream(transport.getInputStream())); } /** * * @deprecated Use InputStream executeShell(String command, String... args) method instead. Together with * Stream.copy(in, out), it is possible to achieve the same effect. */ @Deprecated public void executeShell(OutputStream output, String command, String... args) throws IOException, JadbException { Transport transport = getTransport(); StringBuilder shellLine = buildCmdLine(command, args); send(transport, "shell:" + shellLine.toString()); if (output != null) { try (AdbFilterOutputStream out = new AdbFilterOutputStream(output)) { transport.readResponseTo(out); } } } /**

    Execute a command with raw binary output. * *

    Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary * data with that version or later. For earlier versions of Android, use * {@link #executeShell(String, String...)}. * * @param command main command to run, e.g. "screencap" * @param args arguments to the command, e.g. "-p". * @return combined stdout/stderr stream. * @throws IOException * @throws JadbException */ public InputStream execute(String command, String... args) throws IOException, JadbException { Transport transport = getTransport(); StringBuilder shellLine = buildCmdLine(command, args); send(transport, "exec:" + shellLine.toString()); return new BufferedInputStream(transport.getInputStream()); } /** * Builds a command line string from the command and its arguments. * * @param command the command. * @param args the list of arguments. * @return the command line. */ private StringBuilder buildCmdLine(String command, String... args) { StringBuilder shellLine = new StringBuilder(command); for (String arg : args) { shellLine.append(" "); shellLine.append(Bash.quote(arg)); } return shellLine; } public List list(String remotePath) throws IOException, JadbException { Transport transport = getTransport(); SyncTransport sync = transport.startSync(); sync.send("LIST", remotePath); List result = new ArrayList<>(); for (RemoteFileRecord dent = sync.readDirectoryEntry(); dent != RemoteFileRecord.DONE; dent = sync.readDirectoryEntry()) { result.add(dent); } return result; } private int getMode(File file) { //noinspection OctalInteger return 0664; } public void push(InputStream source, long lastModified, int mode, RemoteFile remote) throws IOException, JadbException { Transport transport = getTransport(); SyncTransport sync = transport.startSync(); sync.send("SEND", remote.getPath() + "," + Integer.toString(mode)); sync.sendStream(source); sync.sendStatus("DONE", (int) lastModified); sync.verifyStatus(); } public void push(File local, RemoteFile remote) throws IOException, JadbException { FileInputStream fileStream = new FileInputStream(local); push(fileStream, local.lastModified(), getMode(local), remote); fileStream.close(); } public void pull(RemoteFile remote, OutputStream destination) throws IOException, JadbException { Transport transport = getTransport(); SyncTransport sync = transport.startSync(); sync.send("RECV", remote.getPath()); sync.readChunksTo(destination); } public void pull(RemoteFile remote, File local) throws IOException, JadbException { FileOutputStream fileStream = new FileOutputStream(local); pull(remote, fileStream); fileStream.close(); } private void send(Transport transport, String command) throws IOException, JadbException { transport.send(command); transport.verifyResponse(); } @Override public String toString() { return "Android Device with serial " + serial; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((serial == null) ? 0 : serial.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; JadbDevice other = (JadbDevice) obj; if (serial == null) { return other.serial == null; } else return serial.equals(other.serial); } } ================================================ FILE: src/main/java/se/vidstige/jadb/JadbException.java ================================================ package se.vidstige.jadb; public class JadbException extends Exception { public JadbException(String message) { super(message); } private static final long serialVersionUID = -3879283786835654165L; } ================================================ FILE: src/main/java/se/vidstige/jadb/LookBackFilteringOutputStream.java ================================================ package se.vidstige.jadb; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayDeque; public class LookBackFilteringOutputStream extends FilterOutputStream { private final ArrayDeque buffer; private final int lookBackBufferSize; LookBackFilteringOutputStream(OutputStream inner, int lookBackBufferSize) { super(inner); this.lookBackBufferSize = lookBackBufferSize; this.buffer = new ArrayDeque<>(lookBackBufferSize); } void unwrite() { buffer.removeFirst(); } ArrayDeque lookback() { return buffer; } @Override public void write(int c) throws IOException { buffer.addLast((byte) c); flushBuffer(lookBackBufferSize); } @Override public void flush() throws IOException { flushBuffer(0); out.flush(); } private void flushBuffer(int size) throws IOException { while (buffer.size() > size) { Byte b = buffer.removeFirst(); out.write(b); } } } ================================================ FILE: src/main/java/se/vidstige/jadb/RemoteFile.java ================================================ package se.vidstige.jadb; /** * Created by vidstige on 2014-03-20 */ public class RemoteFile { private final String path; public RemoteFile(String path) { this.path = path; } public String getName() { throw new UnsupportedOperationException(); } public int getSize() { throw new UnsupportedOperationException(); } public long getLastModified() { throw new UnsupportedOperationException(); } public boolean isDirectory() { throw new UnsupportedOperationException(); } public String getPath() { return path; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) return false; RemoteFile that = (RemoteFile) o; return path.equals(that.path); } @Override public int hashCode() { return path.hashCode(); } } ================================================ FILE: src/main/java/se/vidstige/jadb/RemoteFileRecord.java ================================================ package se.vidstige.jadb; /** * Created by vidstige on 2014-03-19. */ class RemoteFileRecord extends RemoteFile { public static final RemoteFileRecord DONE = new RemoteFileRecord(null, 0, 0, 0); private final int mode; private final int size; private final long lastModified; public RemoteFileRecord(String name, int mode, int size, long lastModified) { super(name); this.mode = mode; this.size = size; this.lastModified = lastModified; } @Override public int getSize() { return size; } @Override public long getLastModified() { return lastModified; } @Override public boolean isDirectory() { return (mode & (1 << 14)) == (1 << 14); } } ================================================ FILE: src/main/java/se/vidstige/jadb/Stream.java ================================================ package se.vidstige.jadb; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; public class Stream { public static void copy(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024 * 10]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } public static String readAll(InputStream input, Charset charset) throws IOException { ByteArrayOutputStream tmp = new ByteArrayOutputStream(); Stream.copy(input, tmp); return new String(tmp.toByteArray(), charset); } } ================================================ FILE: src/main/java/se/vidstige/jadb/Subprocess.java ================================================ package se.vidstige.jadb; import java.io.IOException; public class Subprocess { public Process execute(String[] command) throws IOException { return Runtime.getRuntime().exec(command); } } ================================================ FILE: src/main/java/se/vidstige/jadb/SyncTransport.java ================================================ package se.vidstige.jadb; import java.io.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; /** * Created by vidstige on 2014-03-19. */ public class SyncTransport { private final DataOutput output; private final DataInput input; SyncTransport(OutputStream outputStream, InputStream inputStream) { output = new DataOutputStream(outputStream); input = new DataInputStream(inputStream); } public SyncTransport(DataOutput outputStream, DataInput inputStream) { output = outputStream; input = inputStream; } public void send(String syncCommand, String name) throws IOException { if (syncCommand.length() != 4) { throw new IllegalArgumentException("sync commands must have length 4"); } output.writeBytes(syncCommand); // output.writeInt(Integer.reverseBytes(name.length())); // output.writeBytes(name); byte[] data = name.getBytes(StandardCharsets.UTF_8); output.writeInt(Integer.reverseBytes(data.length)); output.write(data); } public void sendStatus(String statusCode, int length) throws IOException { output.writeBytes(statusCode); output.writeInt(Integer.reverseBytes(length)); } void verifyStatus() throws IOException, JadbException { String status = readString(4); int length = readInt(); if ("FAIL".equals(status)) { String error = readString(length); throw new JadbException(error); } if (!"OKAY".equals(status)) { throw new JadbException("Unknown error: " + status); } } private int readInt() throws IOException { return Integer.reverseBytes(input.readInt()); } private String readString(int length) throws IOException { byte[] buffer = new byte[length]; input.readFully(buffer); return new String(buffer, Charset.forName("utf-8")); } RemoteFileRecord readDirectoryEntry() throws IOException { String id = readString(4); int mode = readInt(); int size = readInt(); int time = readInt(); int nameLength = readInt(); String name = readString(nameLength); if (!"DENT".equals(id)) { return RemoteFileRecord.DONE; } return new RemoteFileRecord(name, mode, size, time); } private void sendChunk(byte[] buffer, int offset, int length) throws IOException { output.writeBytes("DATA"); output.writeInt(Integer.reverseBytes(length)); output.write(buffer, offset, length); } private int readChunk(byte[] buffer) throws IOException, JadbException { String id = readString(4); int n = readInt(); if ("FAIL".equals(id)) { throw new JadbException(readString(n)); } if (!"DATA".equals(id)) return -1; input.readFully(buffer, 0, n); return n; } public void sendStream(InputStream in) throws IOException { byte[] buffer = new byte[1024 * 64]; int n = in.read(buffer); while (n != -1) { sendChunk(buffer, 0, n); n = in.read(buffer); } } public void readChunksTo(OutputStream stream) throws IOException, JadbException { byte[] buffer = new byte[1024 * 64]; int n = readChunk(buffer); while (n != -1) { stream.write(buffer, 0, n); n = readChunk(buffer); } } } ================================================ FILE: src/main/java/se/vidstige/jadb/Transport.java ================================================ package se.vidstige.jadb; import java.io.*; import java.net.Socket; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; class Transport { private final OutputStream outputStream; private final InputStream inputStream; private Transport(OutputStream outputStream, InputStream inputStream) { this.outputStream = outputStream; this.inputStream = inputStream; } Transport(Socket socket) throws IOException { this(socket.getOutputStream(), socket.getInputStream()); } String readString() throws IOException { String encodedLength = readString(4); int length = Integer.parseInt(encodedLength, 16); return readString(length); } void readResponseTo(OutputStream output) throws IOException { Stream.copy(inputStream, output); } public InputStream getInputStream() { return inputStream; } void verifyResponse() throws IOException, JadbException { String response = readString(4); if (!"OKAY".equals(response)) { String error = readString(); throw new JadbException("command failed: " + error); } } private String readString(int length) throws IOException { DataInput reader = new DataInputStream(inputStream); byte[] responseBuffer = new byte[length]; reader.readFully(responseBuffer); return new String(responseBuffer, Charset.forName("utf-8")); } // private String getCommandLength(String command) { // return String.format("%04x", command.length()); // } public void send(String command) throws IOException { OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); byte[] data = command.getBytes(StandardCharsets.UTF_8); writer.write(String.format("%04x", data.length)); //writer.write(getCommandLength(command)); writer.write(command); writer.flush(); } SyncTransport startSync() throws IOException, JadbException { send("sync:"); verifyResponse(); return new SyncTransport(outputStream, inputStream); } public void close() throws IOException { inputStream.close(); outputStream.close(); } } ================================================ FILE: src/main/java/se/vidstige/jadb/managers/Bash.java ================================================ package se.vidstige.jadb.managers; public class Bash { public static String quote(String s) { // TODO: Should also check other whitespace if (!s.contains(" ")) { return s; } return "'" + s.replace("'", "'\\''") + "'"; } } ================================================ FILE: src/main/java/se/vidstige/jadb/managers/Package.java ================================================ package se.vidstige.jadb.managers; /** * Android package */ public class Package { private final String name; public Package(String name) { this.name = name; } @Override public String toString() { return name; } @Override public boolean equals(Object o) { return name.equals(o); } @Override public int hashCode() { return name.hashCode(); } } ================================================ FILE: src/main/java/se/vidstige/jadb/managers/PackageManager.java ================================================ package se.vidstige.jadb.managers; import se.vidstige.jadb.JadbDevice; import se.vidstige.jadb.JadbException; import se.vidstige.jadb.RemoteFile; import se.vidstige.jadb.Stream; import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Java interface to package manager. Launches package manager through jadb */ public class PackageManager { private final JadbDevice device; public PackageManager(JadbDevice device) { this.device = device; } public List getPackages() throws IOException, JadbException { ArrayList result = new ArrayList<>(); try (BufferedReader input = new BufferedReader(new InputStreamReader(device.executeShell("pm", "list", "packages"), StandardCharsets.UTF_8))) { String line; while ((line = input.readLine()) != null) { final String prefix = "package:"; if (line.startsWith(prefix)) { result.add(new Package(line.substring(prefix.length()))); } } } return result; } private String getErrorMessage(String operation, String target, String errorMessage) { return "Could not " + operation + " " + target + ": " + errorMessage; } private void verifyOperation(String operation, String target, String result) throws JadbException { if (!result.contains("Success")) throw new JadbException(getErrorMessage(operation, target, result)); } public void remove(RemoteFile file) throws IOException, JadbException { InputStream s = device.executeShell("rm", "-f", Bash.quote(file.getPath())); Stream.readAll(s, StandardCharsets.UTF_8); } private void install(File apkFile, List extraArguments) throws IOException, JadbException { RemoteFile remote = new RemoteFile("/sdcard/tmp/" + apkFile.getName()); device.push(apkFile, remote); ArrayList arguments = new ArrayList<>(); arguments.add("install"); arguments.addAll(extraArguments); arguments.add(remote.getPath()); InputStream s = device.executeShell("pm", arguments.toArray(new String[0])); String result = Stream.readAll(s, StandardCharsets.UTF_8); remove(remote); verifyOperation("install", apkFile.getName(), result); } public void install(File apkFile) throws IOException, JadbException { install(apkFile, new ArrayList<>(0)); } private void installWithOptions(File apkFile, List options) throws IOException, JadbException { List optionsAsStr = new ArrayList<>(options.size()); for(InstallOption installOption : options) { optionsAsStr.add(installOption.getStringRepresentation()); } install(apkFile, optionsAsStr); } public void forceInstall(File apkFile) throws IOException, JadbException { installWithOptions(apkFile, Collections.singletonList(REINSTALL_KEEPING_DATA)); } public void uninstall(Package name) throws IOException, JadbException { InputStream s = device.executeShell("pm", "uninstall", name.toString()); String result = Stream.readAll(s, StandardCharsets.UTF_8); verifyOperation("uninstall", name.toString(), result); } public void launch(Package name) throws IOException, JadbException { InputStream s = device.executeShell("monkey", "-p", name.toString(), "-c", "android.intent.category.LAUNCHER", "1"); s.close(); } // public static class InstallOption { InstallOption(String ... varargs) { for(String str: varargs) { stringBuilder.append(str).append(" "); } } private final StringBuilder stringBuilder = new StringBuilder(); private String getStringRepresentation() { return stringBuilder.toString(); } } public static final InstallOption WITH_FORWARD_LOCK = new InstallOption("-l"); private static final InstallOption REINSTALL_KEEPING_DATA = new InstallOption("-r"); public static final InstallOption ALLOW_TEST_APK = new InstallOption("-t"); public static InstallOption WITH_INSTALLER_PACKAGE_NAME(String name) { return new InstallOption("-t", name); } public static InstallOption ON_SHARED_MASS_STORAGE(String name) { return new InstallOption("-s", name); } public static InstallOption ON_INTERNAL_SYSTEM_MEMORY(String name) { return new InstallOption("-f", name); } public static final InstallOption ALLOW_VERSION_DOWNGRADE = new InstallOption("-d"); /** * This option is supported only from Android 6.X+ */ public static final InstallOption GRANT_ALL_PERMISSIONS = new InstallOption("-g"); // } ================================================ FILE: src/main/java/se/vidstige/jadb/managers/PropertyManager.java ================================================ package se.vidstige.jadb.managers; import se.vidstige.jadb.JadbDevice; import se.vidstige.jadb.JadbException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A class which works with properties, uses getprop and setprop methods of android shell */ public class PropertyManager { private final Pattern pattern = Pattern.compile("^\\[([a-zA-Z0-9_.-]*)\\]:.\\[([^\\[\\]]*)\\]"); private final JadbDevice device; public PropertyManager(JadbDevice device) { this.device = device; } public Map getprop() throws IOException, JadbException { try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(device.executeShell("getprop")))) { return parseProp(bufferedReader); } } private Map parseProp(BufferedReader bufferedReader) throws IOException { //final Pattern pattern = Pattern.compile("^\\[([a-zA-Z0-9_.-]*)\\]:.\\[([a-zA-Z0-9_.-]*)\\]"); Map result = new HashMap<>(); String line; Matcher matcher = pattern.matcher(""); while ((line = bufferedReader.readLine()) != null) { matcher.reset(line); if (matcher.find()) { if (matcher.groupCount() < 2) { System.err.println("Property line: " + line + " does not match patter. Ignoring"); continue; } String key = matcher.group(1); String value = matcher.group(2); result.put(key, value); } } return result; } } ================================================ FILE: src/main/java/se/vidstige/jadb/server/AdbDeviceResponder.java ================================================ package se.vidstige.jadb.server; import se.vidstige.jadb.JadbException; import se.vidstige.jadb.RemoteFile; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataOutputStream; import java.io.IOException; /** * Created by vidstige on 20/03/14. */ public interface AdbDeviceResponder { String getSerial(); String getType(); void filePushed(RemoteFile path, int mode, ByteArrayOutputStream buffer) throws JadbException; void filePulled(RemoteFile path, ByteArrayOutputStream buffer) throws JadbException, IOException; void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException; } ================================================ FILE: src/main/java/se/vidstige/jadb/server/AdbProtocolHandler.java ================================================ package se.vidstige.jadb.server; import se.vidstige.jadb.JadbException; import se.vidstige.jadb.RemoteFile; import se.vidstige.jadb.SyncTransport; import java.io.*; import java.net.ProtocolException; import java.net.Socket; import java.nio.charset.StandardCharsets; class AdbProtocolHandler implements Runnable { private final Socket socket; private final AdbResponder responder; private AdbDeviceResponder selected; public AdbProtocolHandler(Socket socket, AdbResponder responder) { this.socket = socket; this.responder = responder; } private AdbDeviceResponder findDevice(String serial) throws ProtocolException { for (AdbDeviceResponder d : responder.getDevices()) { if (d.getSerial().equals(serial)) return d; } throw new ProtocolException("'" + serial + "' not connected"); } @Override public void run() { try { runServer(); } catch (IOException e) { if (e.getMessage() != null) // thrown when exiting for some reason System.out.println("IO Error: " + e.getMessage()); } } private void runServer() throws IOException { DataInput input = new DataInputStream(socket.getInputStream()); DataOutputStream output = new DataOutputStream(socket.getOutputStream()); while (true) { byte[] buffer = new byte[4]; input.readFully(buffer); String encodedLength = new String(buffer, StandardCharsets.UTF_8); int length = Integer.parseInt(encodedLength, 16); buffer = new byte[length]; input.readFully(buffer); String command = new String(buffer, StandardCharsets.UTF_8); responder.onCommand(command); try { if ("host:version".equals(command)) { output.writeBytes("OKAY"); send(output, String.format("%04x", responder.getVersion())); } else if ("host:transport-any".equals(command)) { // TODO: Check so that exactly one device is selected. selected = responder.getDevices().getFirst(); output.writeBytes("OKAY"); } else if ("host:devices".equals(command)) { ByteArrayOutputStream tmp = new ByteArrayOutputStream(); DataOutputStream writer = new DataOutputStream(tmp); for (AdbDeviceResponder d : responder.getDevices()) { writer.writeBytes(d.getSerial() + "\t" + d.getType() + "\n"); } output.writeBytes("OKAY"); send(output, tmp.toString(StandardCharsets.UTF_8)); } else if (command.startsWith("host:transport:")) { String serial = command.substring("host:transport:".length()); selected = findDevice(serial); output.writeBytes("OKAY"); } else if ("sync:".equals(command)) { output.writeBytes("OKAY"); try { sync(output, input); } catch (JadbException e) { // sync response with a different type of fail message SyncTransport sync = new SyncTransport(output, input); sync.send("FAIL", e.getMessage()); } } else if (command.startsWith("shell:")) { String shellCommand = command.substring("shell:".length()); output.writeBytes("OKAY"); shell(shellCommand, output, input); output.close(); return; } else if ("host:get-state".equals(command)) { // TODO: Check so that exactly one device is selected. AdbDeviceResponder device = responder.getDevices().getFirst(); output.writeBytes("OKAY"); send(output, device.getType()); } else if (command.startsWith("host-serial:")) { String[] strs = command.split(":",0); if (strs.length != 3) { throw new ProtocolException("Invalid command: " + command); } String serial = strs[1]; boolean found = false; output.writeBytes("OKAY"); for (AdbDeviceResponder d : responder.getDevices()) { if (d.getSerial().equals(serial)) { send(output, d.getType()); found = true; break; } } if (!found) { send(output, "unknown"); } } else { throw new ProtocolException("Unknown command: " + command); } } catch (ProtocolException e) { output.writeBytes("FAIL"); send(output, e.getMessage()); } output.flush(); } } private void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException { selected.shell(command, stdout, stdin); } private int readInt(DataInput input) throws IOException { return Integer.reverseBytes(input.readInt()); } private String readString(DataInput input, int length) throws IOException { byte[] responseBuffer = new byte[length]; input.readFully(responseBuffer); return new String(responseBuffer, StandardCharsets.UTF_8); } private void sync(DataOutput output, DataInput input) throws IOException, JadbException { String id = readString(input, 4); int length = readInt(input); if ("SEND".equals(id)) { String remotePath = readString(input, length); int idx = remotePath.lastIndexOf(','); String path = remotePath; int mode = 0666; if (idx > 0) { path = remotePath.substring(0, idx); mode = Integer.parseInt(remotePath.substring(idx + 1)); } SyncTransport transport = new SyncTransport(output, input); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); transport.readChunksTo(buffer); selected.filePushed(new RemoteFile(path), mode, buffer); transport.sendStatus("OKAY", 0); // 0 = ignored } else if ("RECV".equals(id)) { String remotePath = readString(input, length); SyncTransport transport = new SyncTransport(output, input); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); selected.filePulled(new RemoteFile(remotePath), buffer); transport.sendStream(new ByteArrayInputStream(buffer.toByteArray())); transport.sendStatus("DONE", 0); // ignored } else throw new JadbException("Unknown sync id " + id); } private String getCommandLength(String command) { return String.format("%04x", command.length()); } public void send(DataOutput writer, String response) throws IOException { writer.writeBytes(getCommandLength(response)); writer.writeBytes(response); } } ================================================ FILE: src/main/java/se/vidstige/jadb/server/AdbResponder.java ================================================ package se.vidstige.jadb.server; import java.util.List; /** * Created by vidstige on 20/03/14. */ public interface AdbResponder { void onCommand(String command); int getVersion(); List getDevices(); } ================================================ FILE: src/main/java/se/vidstige/jadb/server/AdbServer.java ================================================ package se.vidstige.jadb.server; import java.net.Socket; /** * Created by vidstige on 2014-03-20 */ public class AdbServer extends SocketServer { public static final int DEFAULT_PORT = 15037; private final AdbResponder responder; public AdbServer(AdbResponder responder) { this(responder, DEFAULT_PORT); } public AdbServer(AdbResponder responder, int port) { super(port); this.responder = responder; } @Override protected Runnable createResponder(Socket socket) { return new AdbProtocolHandler(socket, responder); } } ================================================ FILE: src/main/java/se/vidstige/jadb/server/SocketServer.java ================================================ package se.vidstige.jadb.server; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; // >set ANDROID_ADB_SERVER_PORT=15037 public abstract class SocketServer implements Runnable { private final int port; private ServerSocket socket; private Thread thread; private boolean isStarted = false; private final Object lockObject = new Object(); protected SocketServer(int port) { this.port = port; } public void start() throws InterruptedException { thread = new Thread(this, "Fake Adb Server"); thread.setDaemon(true); thread.start(); waitForServer(); } public int getPort() { return port; } @Override public void run() { try { socket = new ServerSocket(port); socket.setReuseAddress(true); serverReady(); while (true) { Socket c = socket.accept(); Thread clientThread = new Thread(createResponder(c), "AdbClientWorker"); clientThread.setDaemon(true); clientThread.start(); } } catch (IOException ignored) { } } private void serverReady() { synchronized (lockObject) { isStarted = true; lockObject.notify(); } } private void waitForServer() throws InterruptedException { synchronized (lockObject) { if (!isStarted) { lockObject.wait(); } } } protected abstract Runnable createResponder(Socket socket); public void stop() throws IOException, InterruptedException { socket.close(); thread.join(); } } ================================================ FILE: src/main/resources/META-INF/LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: src/main/resources/avr/avr_commands.properties ================================================ ADC = Add with Carry ADD = Add without Carry ADIW = Add Immediate to Word AND = Logical AND ANDI = Logical AND with Immediate ASR = Arithmetic Shift Right BCLR = Bit Clear in SREG BLD = Bit Load from the T Flag in SREG to a Bit in Register BRBC = Branch if Bit in SREG is Cleared BRBS = Branch if Bit in SREG is Set BRCC = Branch if Carry Cleared BRCS = Branch if Carry Set BREAK = Break BREQ = Branch if Equal BRGE = Branch if Greater or Equal (Signed) BRHC = Branch if Half Carry Flag is Cleared BRHS = Branch if Half Carry Flag is Set BRID = Branch if Global Interrupt is Disabled BRIE = Branch if Global Interrupt is Enabled BRLO = Branch if Lower (Unsigned) BRLT = Branch if Less Than (Signed) BRMI = Branch if Minus BRNE = Branch if Not Equal BRPL = Branch if Plus BRSH = Branch if Same or Higher (Unsigned) BRTC = Branch if the T Flag is Cleared BRTS = Branch if the T Flag is Set BRVC = Branch if Overflow Cleared BRVS = Branch if Overflow Set BSET = Bit Set in SREG BST = Bit Store from Bit in Register to T Flag in SREG CALL = Long Call to a Subroutine CBI = Clear Bit in I/O Register CBR = Clear Bits in Register CLC = Clear Carry Flag CLH = Clear Half Carry Flag CLI = Clear Global Interrupt Flag CLN = Clear Negative Flag CLR = Clear Register CLS = Clear Signed Flag CLT = Clear T Flag CLV = Clear Overflow Flag CLZ = Clear Zero Flag COM = One's Complement CP = Compare CPC = Compare with Carry CPI = Compare with Immediate CPSE = Compare Skip if Equal DEC = Decrement DES = Data Encryption Standard EICALL = Extended Indirect Call to Subroutine EIJMP = Extended Indirect Jump ELPM = Extended Load Program Memory EOR = Exclusive OR FMUL = Fractional Multiply Unsigned FMULS = Fractional Multiply Signed FMULSU = Fractional Multiply Signed with Unsigned ICALL = Indirect Call to Subroutine IJMP = Indirect Jump IN = Load an I/O Location to Register INC = Increment JMP = Jump LAC = Load and Clear LAS = Load and Set LAT = Load and Toggle LD = Load Indirect from Data Space to Register using Index X LDD = Load Indirect from Data Space to Register using Index X LDI = Loads an 8 bit constant directly to register 16 to 31. LDS = Load Direct from Data Space (Register File, I/O memory, SRAM) LDS = Load Direct from Data Space (Register File, I/O memory, SRAM) LPM = Load Program Memory, R0 <- (Z=R31:R30) LSL = Logical Shift Left LSR = Logical Shift Right MOV = Copy Register MOVW = Copy Register Word, R(d+1):R(d) <- R(r+1):R(r) MUL = Multiply Unsigned MULS = Multiply Signed MULSU = Multiply Signed with Unsigned NEG = Two's Complement (Rd <- 0x00-Rd) NOP = No Operation OR = Logical OR ORI = Logical OR with Immediate OUT = Store Register to I/O Location POP = Pop Register from Stack PUSH = Push Register on Stack RCALL = Relative Call to Subroutine RET = Return from Subroutine RETI = Return from Interrupt RJMP = Relative Jump ROL = Rotate Left trough Carry ROR = Rotate Right through Carry SBC = Subtract with Carry SBCI = Subtract Immediate with Carry SBI = Set Bit in I/O Register SBIC = Skip if Bit in I/O Registeris Cleared SBIS = Skip if Bit in I/O Register is Set SBIW = Subtract Immediate from Word SBR = Set Bits in Register SBRC = Skip if Bit in Register is Cleared SBRS = Skip if Bit in Register is Set SEC = Set Carry Flag SEH = Set Half Carry Flag SEI = Set Global Interrupt Flag SEN = Set Negative Flag SER = Set all Bits in Register SES = Set Signed Flag SET = Set T Flag SEV = Set Overflow Flag SEZ = Set Zero Flag SLEEP = Enter sleep mode SPM = Store Program Memory ST = Store Indirect From Register to Data Space using Index X, Y or Z STD = Store Indirect From Register to Data Space using Index X, Y or Z STS = Store Direct to Data Space (Register File, I/O memory, SRAM) SUB = Subtract without Carry SUBI = Subtract Immediate SWAP = Swap Nibbles TST = Test for Zero or Minus WDR = Watchdog Reset XCH = Exchange ================================================ FILE: src/main/resources/avr/avrdude.csv ================================================ t11:ATtiny11:0x1e9004:calibration[1]:lock[1]:flash[1024]:fuse[1]:signature[3]:eeprom[64] t12:ATtiny12:0x1e9005:calibration[1]:lock[1]:flash[1024]:fuse[1]:signature[3]:eeprom[64] t13:ATtiny13:0x1e9007:calibration[2]:lock[1]:flash[1024]:eeprom[64]:signature[3]:hfuse[1]:lfuse[1] t15:ATtiny15:0x1e9006:calibration[1]:lock[1]:flash[1024]:fuse[1]:signature[3]:eeprom[64] 1200:AT90S1200:0x1e9001:fuse[1]:lock[1]:flash[1024]:eeprom[64]:signature[3] 4414:AT90S4414:0x1e9201:fuse[1]:lock[1]:flash[4096]:eeprom[256]:signature[3] 2313:AT90S2313:0x1e9101:fuse[1]:lock[1]:flash[2048]:eeprom[128]:signature[3] 2333:AT90S2333:0x1e9105:fuse[1]:lock[1]:flash[2048]:eeprom[128]:signature[3] 2343:AT90S2343:0x1e9103:fuse[1]:lock[1]:flash[2048]:eeprom[128]:signature[3] 4433:AT90S4433:0x1e9203:fuse[1]:lock[1]:flash[4096]:eeprom[256]:signature[3] 4434:AT90S4434:0x1e9202:fuse[1]:lock[1]:flash[4096]:eeprom[256]:signature[3] 8515:AT90S8515:0x1e9301:fuse[1]:lock[1]:flash[8192]:eeprom[512]:signature[3] 8535:AT90S8535:0x1e9303:fuse[1]:lock[1]:flash[8192]:eeprom[512]:signature[3] m103:ATmega103:0x1e9701:fuse[1]:lock[1]:flash[131072]:eeprom[4096]:signature[3] m64:ATmega64:0x1e9602:flash[65536]:calibration[4]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] m128:ATmega128:0x1e9702:flash[131072]:calibration[4]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] c128:AT90CAN128:0x1e9781:flash[131072]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] c64:AT90CAN64:0x1e9681:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] c32:AT90CAN32:0x1e9581:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m16:ATmega16:0x1e9403:calibration[4]:lock[1]:flash[16384]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m164p:ATmega164P:0x1e940a:calibration[4]:lock[1]:flash[16384]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m324p:ATmega324P:0x1e9508:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m324pa:ATmega324PA:0x1e9511:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m644:ATmega644:0x1e9609:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] m644p:ATmega644P:0x1e960a:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] m1284p:ATmega1284P:0x1e9705:flash[131072]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m162:ATmega162:0x1e9404:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m163:ATmega163:0x1e9402:calibration[1]:lock[1]:flash[16384]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m169:ATmega169:0x1e9405:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m329:ATmega329:0x1e9503:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m329p:ATmega329P:0x1e950b:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m3290:ATmega3290:0x1e9504:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m3290p:ATmega3290P:0x1e950c:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m649:ATmega649:0x1e9603:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] m6490:ATmega6490:0x1e9604:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] m32:ATmega32:0x1e9502:calibration[4]:lock[1]:flash[32768]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m161:ATmega161:0x1e9401:fuse[1]:lock[1]:flash[16384]:eeprom[512]:signature[3] m8:ATmega8:0x1e9307:calibration[4]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m8515:ATmega8515:0x1e9306:calibration[4]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m8535:ATmega8535:0x1e9308:calibration[4]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] t26:ATtiny26:0x1e9109:calibration[4]:lock[1]:flash[2048]:eeprom[128]:signature[3]:hfuse[1]:lfuse[1] t261:ATtiny261:0x1e910c:efuse[1]:calibration[1]:lock[1]:flash[2048]:eeprom[128]:signature[3]:hfuse[1]:lfuse[1] t461:ATtiny461:0x1e9208:efuse[1]:calibration[1]:lock[1]:flash[4096]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] t861:ATtiny861:0x1e930d:efuse[1]:calibration[1]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m48:ATmega48:0x1e9205:flash[4096]:calibration[1]:lock[1]:efuse[1]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] m48p:ATmega48P:0x1e920a:flash[4096]:calibration[1]:lock[1]:efuse[1]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] m88:ATmega88:0x1e930a:flash[8192]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m88p:ATmega88P:0x1e930f:flash[8192]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m168:ATmega168:0x1e9406:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m168p:ATmega168P:0x1e940b:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] t88:ATtiny88:0x1e9311:flash[8192]:calibration[1]:lock[1]:efuse[1]:eeprom[64]:signature[3]:hfuse[1]:lfuse[1] m328:ATmega328:0x1e9514:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m328p:ATmega328P:0x1e950F:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] t2313:ATtiny2313:0x1e910a:efuse[1]:calibration[2]:lock[1]:flash[2048]:eeprom[128]:signature[3]:hfuse[1]:lfuse[1] t4313:ATtiny4313:0x1e920d:efuse[1]:calibration[2]:lock[1]:flash[4096]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] pwm2:AT90PWM2:0x1e9381:efuse[1]:calibration[1]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] pwm2b:AT90PWM2B:0x1e9383:efuse[1]:calibration[1]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] pwm316:AT90PWM316:0x1e9483:flash[16384]:efuse[1]:calibration[1]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] t25:ATtiny25:0x1e9108:efuse[1]:calibration[2]:lock[1]:flash[2048]:eeprom[128]:signature[3]:hfuse[1]:lfuse[1] t45:ATtiny45:0x1e9206:efuse[1]:calibration[2]:lock[1]:flash[4096]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] t85:ATtiny85:0x1e930b:efuse[1]:calibration[2]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m640:ATmega640:0x1e9608:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m1280:ATmega1280:0x1e9703:flash[131072]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m1281:ATmega1281:0x1e9704:flash[131072]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m2560:ATmega2560:0x1e9801:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m2561:ATmega2561:0x1e9802:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m128rfa1:ATmega128RFA1:0x1ea701:flash[131072]:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m256rfr2:ATmega256RFR2:0x1ea802:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m128rfr2:ATmega128RFR2:0x1ea702:flash[131072]:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m64rfr2:ATmega64RFR2:0x1ea602:flash[65536]:flash[131072]:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m2564rfr2:ATmega2564RFR2:0x1ea803:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m1284rfr2:ATmega1284RFR2:0x1ea703:flash[131072]:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] m644rfr2:ATmega644RFR2:0x1ea603:flash[65536]:flash[131072]:flash[262144]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] t24:ATtiny24:0x1e910b:efuse[1]:calibration[1]:lock[1]:flash[2048]:eeprom[128]:signature[3]:hfuse[1]:lfuse[1] t44:ATtiny44:0x1e9207:efuse[1]:calibration[1]:lock[1]:flash[4096]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] t84:ATtiny84:0x1e930c:efuse[1]:calibration[1]:lock[1]:flash[8192]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] t43u:ATtiny43u:0x1e920C:efuse[1]:calibration[2]:lock[1]:flash[4096]:eeprom[64]:signature[3]:hfuse[1]:lfuse[1] m32u4:ATmega32U4:0x1e9587:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] usb646:AT90USB646:0x1e9682:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] usb647:AT90USB647:0x1e9682:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] usb1286:AT90USB1286:0x1e9782:flash[131072]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] usb1287:AT90USB1287:0x1e9782:flash[131072]:calibration[1]:lock[1]:efuse[1]:eeprom[4096]:signature[3]:hfuse[1]:lfuse[1] usb162:AT90USB162:0x1e9482:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] usb82:AT90USB82:0x1e9382:flash[8192]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m32u2:ATmega32U2:0x1e958a:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m16u2:ATmega16U2:0x1e9489:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m8u2:ATmega8U2:0x1e9389:flash[8192]:calibration[1]:lock[1]:efuse[1]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] m325:ATmega325:0x1e9505:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m645:ATmega645:0x1E9605:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] m3250:ATmega3250:0x1E9506:flash[32768]:calibration[1]:lock[1]:efuse[1]:eeprom[1024]:signature[3]:hfuse[1]:lfuse[1] m6450:ATmega6450:0x1E9606:flash[65536]:calibration[1]:lock[1]:efuse[1]:eeprom[2048]:signature[3]:hfuse[1]:lfuse[1] x16a4u:ATxmega16A4U:0x1e9441:flash[1280]:boot[256]:application[1024]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x16c4:ATxmega16C4:0x1e9544:flash[1280]:boot[256]:application[1024]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x16d4:ATxmega16D4:0x1e9442:flash[1280]:boot[256]:application[1024]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x16a4:ATxmega16A4:0x1e9441:fuse0[1]:flash[1280]:boot[256]:application[1024]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x32a4u:ATxmega32A4U:0x1e9541:flash[2304]:boot[256]:application[2048]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x32c4:ATxmega32C4:0x1e9443:flash[2304]:boot[256]:application[2048]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x32d4:ATxmega32D4:0x1e9542:flash[2304]:boot[256]:application[2048]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x32a4:ATxmega32A4:0x1e9541:fuse0[1]:flash[2304]:boot[256]:application[2048]:apptable[256]:usersig[16]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64a4u:ATxmega64A4U:0x1e9646:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64c3:ATxmega64C3:0x1e9649:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64d3:ATxmega64D3:0x1e964a:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64d4:ATxmega64D4:0x1e9647:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64a1:ATxmega64A1:0x1e964e:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64a1u:ATxmega64A1U:0x1e964e:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64a3:ATxmega64A3:0x1e9642:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64a3u:ATxmega64A3U:0x1e9642:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64a4:ATxmega64A4:0x1e9646:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64b1:ATxmega64B1:0x1e9652:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x64b3:ATxmega64B3:0x1e9651:fuse0[1]:flash[4352]:boot[256]:application[4096]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128c3:ATxmega128C3:0x1e9752:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128d3:ATxmega128D3:0x1e9748:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128d4:ATxmega128D4:0x1e9747:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a1:ATxmega128A1:0x1e974c:fuse0[1]:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a1d:ATxmega128A1revD:0x1e9741:fuse0[1]:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a1u:ATxmega128A1U:0x1e974c:fuse0[1]:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a3:ATxmega128A3:0x1e9742:fuse0[1]:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a3u:ATxmega128A3U:0x1e9742:fuse0[1]:flash[8704]:boot[512]:application[8192]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a4:ATxmega128A4:0x1e9746:flash[8704]:boot[512]:fuse0[1]:application[8192]:apptable[256]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128a4u:ATxmega128A4U:0x1e9746:flash[8704]:boot[512]:application[8192]:apptable[256]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128b1:ATxmega128B1:0x1e974d:flash[8704]:boot[512]:fuse0[1]:application[8192]:apptable[512]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x128b3:ATxmega128B3:0x1e974b:flash[8704]:boot[512]:fuse0[1]:application[8192]:apptable[512]:usersig[16]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x192c3:ATxmega192C3:0x1e9751:flash[12800]:boot[512]:application[12288]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x192d3:ATxmega192D3:0x1e9749:flash[12800]:boot[512]:application[12288]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x192a1:ATxmega192A1:0x1e974e:fuse0[1]:flash[12800]:boot[512]:application[12288]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x192a3:ATxmega192A3:0x1e9744:fuse0[1]:flash[12800]:boot[512]:application[12288]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x192a3u:ATxmega192A3U:0x1e9744:fuse0[1]:flash[12800]:boot[512]:application[12288]:apptable[512]:usersig[32]:eeprom[128]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256c3:ATxmega256C3:0x1e9846:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256d3:ATxmega256D3:0x1e9844:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256a1:ATxmega256A1:0x1e9846:fuse0[1]:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256a3:ATxmega256A3:0x1e9842:fuse0[1]:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256a3u:ATxmega256A3U:0x1e9842:fuse0[1]:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256a3b:ATxmega256A3B:0x1e9843:fuse0[1]:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x256a3bu:ATxmega256A3BU:0x1e9843:fuse0[1]:flash[16896]:boot[512]:application[16384]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x384c3:ATxmega384C3:0x1e9845:flash[25088]:boot[512]:application[24576]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x384d3:ATxmega384D3:0x1e9847:flash[25088]:boot[512]:application[24576]:apptable[512]:usersig[32]:eeprom[256]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x8e5:ATxmega8E5:0x1e9341:flash[640]:boot[128]:application[512]:apptable[128]:usersig[8]:eeprom[32]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x16e5:ATxmega16E5:0x1e9445:flash[1280]:boot[256]:application[1024]:apptable[256]:usersig[8]:eeprom[32]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] x32e5:ATxmega32E5:0x1e954c:flash[2304]:boot[256]:application[2048]:apptable[256]:usersig[8]:eeprom[64]:lock[1]:prodsig[3]:fuse1[1]:fuse2[1]:fuse4[1]:fuse5[1]:signature[3] uc3a0512:AT32UC3A0512:0xEDC03F:flash[524288] t1634:ATtiny1634:0x1e9412:flash[16384]:calibration[1]:lock[1]:efuse[1]:eeprom[256]:signature[3]:hfuse[1]:lfuse[1] t4:ATtiny4:0x1e8f0a:flash[512]:lockbits[1]:fuse[1]:calibration[1]:signature[3] t5:ATtiny5:0x1e8f09:flash[512]:lockbits[1]:fuse[1]:calibration[1]:signature[3] t9:ATtiny9:0x1e9008:flash[1024]:lockbits[1]:fuse[1]:calibration[1]:signature[3] t10:ATtiny10:0x1e9003:flash[1024]:lockbits[1]:fuse[1]:calibration[1]:signature[3] t20:ATtiny20:0x1e910F:flash[2048]:lockbits[1]:fuse[1]:calibration[1]:signature[3] t40:ATtiny40:0x1e920E:flash[4096]:lockbits[1]:fuse[1]:calibration[1]:signature[3] m406:ATMEGA406:0x1e9507:lockbits[1]:flash[40960]:eeprom[512]:signature[3]:hfuse[1]:lfuse[1] ================================================ FILE: src/main/resources/com/mucommander/commons/file/mime.types ================================================ application/andrew-inset ez application/mac-binhex40 hqx application/mac-compactpro cpt application/msword doc application/octet-stream bin dms lha lzh exe class so dll dmg application/oda oda application/pdf pdf application/postscript ai eps ps application/smil smi smil application/vnd.mif mif application/vnd.ms-excel xls application/vnd.ms-powerpoint ppt application/vnd.wap.wbxml wbxml application/vnd.wap.wmlc wmlc application/vnd.wap.wmlscriptc wmlsc application/x-bcpio bcpio application/x-cdlink vcd application/x-chess-pgn pgn application/x-cpio cpio application/x-csh csh application/x-director dcr dir dxr application/x-dvi dvi application/x-futuresplash spl application/x-gtar gtar application/x-hdf hdf application/x-javascript js application/x-java-jnlp-file jnlp application/x-koan skp skd skt skm application/x-latex latex application/x-netcdf nc cdf application/x-sh sh application/x-shar shar application/x-shockwave-flash swf application/x-stuffit sit application/x-sv4cpio sv4cpio application/x-sv4crc sv4crc application/x-tar tar application/x-tcl tcl application/x-tex tex application/x-texinfo texinfo texi application/x-troff t tr roff application/x-troff-man man application/x-troff-me me application/x-troff-ms ms application/x-ustar ustar application/x-wais-source src application/xhtml+xml xhtml xht application/zip zip audio/basic au snd audio/midi mid midi kar audio/mpeg mpga mp2 mp3 audio/x-aiff aif aiff aifc audio/x-mpegurl m3u audio/x-pn-realaudio ram rm audio/x-pn-realaudio-plugin rpm audio/x-realaudio ra audio/x-wav wav chemical/x-pdb pdb chemical/x-xyz xyz image/bmp bmp image/gif gif image/ief ief image/jpeg jpeg jpg jpe image/pict pict pic pct image/png png image/tiff tiff tif image/vnd.djvu djvu djv image/vnd.wap.wbmp wbmp image/x-cmu-raster ras image/x-macpaint pntg pnt mac image/x-portable-anymap pnm image/x-portable-bitmap pbm image/x-portable-graymap pgm image/x-portable-pixmap ppm image/x-quicktime qtif qti image/x-rgb rgb image/x-xbitmap xbm image/x-xpixmap xpm image/x-xwindowdump xwd model/iges igs iges model/mesh msh mesh silo model/vrml wrl vrml text/css css text/html html htm text/plain asc txt text/richtext rtx text/rtf rtf text/sgml sgml sgm text/tab-separated-values tsv text/vnd.wap.wml wml text/vnd.wap.wmlscript wmls text/x-setext etx text/xml xml xsl video/mp4 mp4 video/mpeg mpeg mpg mpe video/quicktime qt mov video/vnd.mpegurl mxu video/x-dv dv dif video/x-msvideo avi video/x-sgi-movie movie x-conference/x-cooltalk ice ================================================ FILE: src/main/resources/dictionary.properties ================================================ language.en-US = English language.en-GB = British English language.fr-FR = Français language.es-ES = Español language.de-DE = Deutsch language.cs-CZ = Čeština language.zh-CN = 简体中文 language.zh-TW = 繁體中文 language.pl-PL = Polski language.hu-HU = Magyar language.ru-RU = Русский language.sl-SL = Slovenščina language.ro-RO = Română language.it-IT = Italiano language.ko-KR = 한국어 language.pt-BR = Português brasileiro language.nl-NL = Nederlands language.sk-SK = Slovenčina language.ja-JP = 日本語 language.sv-SV = Svenska language.da-DA = Dansk language.uk-UA = Українська language.ar-SA = العربية language.be-BY = Беларуская language.no-NO = Norsk bokmål language.tr-TR = Türkçe language.ca-ES = Català ok = OK yes = Yes no = No cancel = Cancel edit = Edit close = Close reset = Reset rename = Rename apply = Apply change = Change save = Save dont_save = Don't save replace = Replace dont_replace = Don't replace delete = Delete skip = Skip skip_all = Skip all overwrite_all = Overwrite all retry = Retry resume = Resume overwrite = Overwrite overwrite_if_older = Overwrite if older duplicate = Duplicate apply_to_all = Apply to all copy = Copy move = Move pack = Pack unpack = Unpack download = Download split = Split combine = Combine browse = Browse ask = Ask stop = Stop pause = Pause quick_search = Quick search file_manager = File manager create = Create creating_file = Creating %1 choose = Choose customize = Customize clean = Clean search = Search choose_folder = Choose a folder login = Login password = Password user = User encoding = Encoding preferred_encodings = Preferred encodings license = License name = Name size = Size date = Date extension = Extension permissions = Permissions owner = Owner group = Group location = Location untitled = Untitled source = Source destination = Destination recurse_directories = Process selected directories recursively go_to = Go to example = Example preview = Preview comment = Comment sample_text = Sample text nb_files = %1 file(s) nb_folders = %1 folder(s) loading = Loading... this_operation_cannot_be_undone = This operation cannot be undone. remove = Remove details = Details title = Title warning = Warning error = Error files = files parent = Parent generic_error = An error has occurred while performing the requested operation. folder_does_not_exist = This folder doesn't exist or is not available. this_folder_does_not_exist = This folder doesn't exist or is not available: %1 this_file_does_not_exist = This file doesn't exist or is not available: %1 failed_to_read_folder = Failed to read this folder. invalid_path = Invalid path: %1 directory_already_exists = Directory %1 already exists. file_exists_in_destination = File already exists in destination source_parent_of_destination = Attempt to transfer a folder to one of its subfolders cannot_read_file = Cannot read file %1 cannot_write_file = Cannot write file %1 overwrite_readonly_file = File is read-only: %1 cannot_create_folder = Unable to create directory %1 cannot_read_folder = Unable to read contents of folder %1 cannot_delete_file = Unable to delete file %1 cannot_delete_folder = Unable to delete folder %1 error_while_transferring = Error while transferring file %1 error_unsupported_operation = Operation is not supported same_source_destination = Source and destination folders are the same file_already_exists = %1 already exists, do you want to replace it ? write_error = Write error read_error = Read error integrity_check_error = Integrity check failed: source and destination don't match startup_error = An error prevented trolCommander from starting. image_size = Image size cannot_write_symlink=Cannot write symlink %1 cannot_write_symlink_already_exists=Symlink already exists: %1 cannot_write_symlink_access_denied=Cannot write symlink %1, access denied permissions.read = Read permissions.write = Write permissions.executable = Executable permissions.user = User permissions.group = Group permissions.other = Other permissions.octal_notation = Octal notation action_categories.all = All action_categories.navigation = Navigation action_categories.selection = Selection action_categories.view = View action_categories.file_operations = File operations action_categories.windows = Windows action_categories.tabs = Tabs action_categories.commands = Custom commands action_categories.misc = Misc Terminal.label = Terminal Terminal.tooltip = Run Terminal window in current directory TerminalPanel.label = Terminal TerminalPanel.tooltip = Show/hide Terminal panel UserMenu.label = User menu UserMenu.tooltip = Show global or local user menu UserMenu.press_f4_to_edit_menu = Press F4 to open menu editor UserMenu.command_not_defined = Command doesn't defined FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Add bookmark AddBookmark.tooltip = Add the current folder to the list of bookmarks BatchRename.label = Batch rename EditBookmarks.label = Edit bookmarks ExploreBookmarks.label = Explore bookmarks EditCredentials.label = Edit credentials ChangeLocation.label = Change current location ChangeDate.label = Change date ChangeDate.tooltip = Change date of selected file(s) ChangePermissions.label = Change permissions ChangePermissions.tooltip = Change permissions of selected file(s) CheckForUpdates.label = Check for updates CompareFolders.label = Compare folders CompareFolderFiles.label = Compare files CompareFolderFiles.tooltip = Compare files and mark changed in current folder ConnectToServer.label = Connect to server ConnectToServer.tooltip = Connect to a remote server TextEditorsList.label = Show text editors list TextEditorsList.tooltip = Show text editors list View.label = View ViewAs.label = View as InternalView.label = View (internal) viewer_type.text = Text file viewer_type.hex = Binary file viewer_type.image = Image viewer_type.pdf = PDF document viewer_type.djvu = DjVu document viewer_type.audio = Audio file viewer_type.html = HTML document View.tooltip = View selected file Edit.label = Edit EditAs.label = Edit as InternalEdit.label = Edit (internal) Calculator.label = Calculator CreateSymlink.label = Create symlink LocateSymlink.label = Goto symlink target ShowFoldersSize.label = Show folders size EditCommands.label = Edit commands EditCommands.group.view = Viewers EditCommands.group.edit = Editors EditCommands.group.others = Others CompareFiles.label = Compare files CompareFiles.tooltip = Compare text files (FileMerge) EditCommands.new = New EditCommands.alias = Alias EditCommands.command = Command EditCommands.display_name = Display name EditCommands.filemask = File mask symboliclinkeditor.edit = Edit symlink symboliclinkeditor.create = Symbolic link symboliclinkeditor.target_file_create = Existing filename (filename symlink will point to) symboliclinkeditor.target_file_edit = Symlink '%s' points to symboliclinkeditor.link_name = Symbolic link filename Edit.tooltip = Edit selected file Copy.label = Copy Copy.tooltip = Copy marked files LocalCopy.label = Local Copy LocalCopy.tooltip = Copy selected file into current folder Move.label = Move Move.tooltip = Move marked files Rename.label = Rename Rename.tooltip = Rename selected file Mkdir.label = Make dir Mkdir.tooltip = Create a directory in current folder Mkfile.label = Make file Mkfile.tooltip = Create a file in current folder Delete.label = Delete Delete.tooltip = Delete marked files using the system trash when possible PermanentDelete.label = Delete permanently PermanentDelete.tooltip = Delete marked files without using the system trash Refresh.label = Refresh Refresh.tooltip = Refresh current folder CloseWindow.label = Close window CloseWindow.tooltip = Close this window CopyFileNames.label = Copy name(s) CopyFileBaseNames.label = Copy basename(s) CopyFilePaths.label = Copy path(s) CopyFilesToClipboard.label = Copy file(s) PasteClipboardFiles.label = Paste file(s) Email.label = Send by email Email.tooltip = Send marked files as email attachments GoBack.label = Go back GoBack.tooltip = Go to previous folder GoForward.label = Go forward GoForward.tooltip = Go to next folder GoToHome.label = Go to home folder GoToParent.label = Go to parent GoToParent.tooltip = Go to parent folder GoToParentInOtherPanel.label = Go to parent in other panel GoToParentInBothPanels.label = Go to parent in both panels GoToRoot.label = Go to root SortByName.label = Sort by Name SortByDate.label = Sort by Date SortBySize.label = Sort by Size SortByExtension.label = Sort by Extension SortByPermissions.label = Sort by Permissions SortByOwner.label = Sort by Owner SortByGroup.label = Sort by Group MarkGroup.label = Mark files MarkGroup.tooltip = Mark a group of files UnmarkGroup.label = Unmark files UnmarkGroup.tooltip = Unmark a group of files MarkAll.label = Mark all UnmarkAll.label = Unmark all MarkSelectedFile.label = Mark/unmark MarkSelectedFile.tooltip = Mark/Unmark selected file MarkNextBlock.label = Mark one block down MarkPreviousBlock.label = Mark one block up MarkNextRow.label = Mark one row down MarkPreviousRow.label = Mark one row up MarkNextPage.label = Mark one page up MarkPreviousPage.label = Mark one page down MarkToFirstRow.label = Mark files to the beginning MarkToLastRow.label = Mark files to the end MarkExtension.label = Mark extension MarkEmpty.label = Mark empty InvertSelection.label = Invert selection SwapFolders.label = Swap folders SwapFolders.tooltip = Swap left and right folders SetSameFolder.label = Set same folder SetSameFolder.tooltip = Set same directory to left and right panels ToggleTableViewModeFull.label = Full Mode ToggleTableViewModeFull.tooltip = Toggle Full View Mode ToggleTableViewModeCompact.label = Compact Mode ToggleTableViewModeCompact.tooltip = Toggle Compact View Mode ToggleTableViewModeShort.label = Short Mode ToggleTableViewModeShort.tooltip = Toggle Short View Mode TogglePanelPreviewMode.label = Quick view TogglePanelPreviewMode.tooltip = Toggle quick view mode NewWindow.label = New window NewWindow.tooltip = Open a new window Open.label = Open Open.tooltip = Enter folder / Enter archive / Execute OpenNatively.label = Open natively OpenNatively.tooltip = Execute selected file with system's file associations OpenInNewTab.label = Open in new tab OpenInOtherPanel.label = Open in other panel OpenInBothPanels.label = Open in both panels OpenLeftInRightPanel.label = Open left in right panel OpenRightInLeftPanel.label = Open right in left panel RevealInDesktop.label = Reveal in %1 RunCommand.label = Run command RunCommand.tooltip = Run a command in current folder Pack.label = Pack Pack.tooltip = Pack marked files into an archive Unpack.label = Unpack Unpack.tooltip = Unpack marked archive files ShowFileProperties.label = Properties ShowFileProperties.tooltip = Show properties of marked files ShowFilePopupMenu.label = Popup menu ShowFilePopupMenu.tooltip = Show context popup menu for selected file ShowPreferences.label = Preferences ShowPreferences.tooltip = Configure trolCommander ShowServerConnections.label = Show open connections Quit.label = Quit ReverseSortOrder.label = Reverse order ToggleAutoSize.label = Auto-size columns Stop.label = Stop folder change ToggleColumn.show = Show %1 column ToggleColumn.hide = Hide %1 column ToggleCommandBar.show = Show command bar ToggleCommandBar.hide = Hide command bar ToggleHiddenFiles.label = Show hidden files ToggleToolBar.show = Show toolbar ToggleToolBar.hide = Hide toolbar CustomizeCommandBar.label = Customize command bar ToggleStatusBar.show = Show status bar ToggleStatusBar.hide = Hide status bar ToggleShowFoldersFirst.label = Show folders first ToggleFoldersAlwaysAlphabetical.label = Sort Folders always alphabetical ToggleTree.label = Show tree view ToggleSinglePanel.label = Toggle single panel PopupLeftDriveButton.label = Change left folder PopupRightDriveButton.label = Change right folder RecallPreviousWindow.label = Recall previous window RecallNextWindow.label = Recall next window RecallWindow.label = Recall window #%1 RecallWindow1.label = Recall window #%1 RecallWindow2.label = Recall window #%1 RecallWindow3.label = Recall window #%1 RecallWindow4.label = Recall window #%1 RecallWindow5.label = Recall window #%1 RecallWindow6.label = Recall window #%1 RecallWindow7.label = Recall window #%1 RecallWindow8.label = Recall window #%1 RecallWindow9.label = Recall window #%1 RecallWindow10.label = Recall window #%1 BringAllToFront.label = Bring all to front SwitchActiveTable.label = Switch between left and right panels SelectNextBlock.label = Jump down one block SelectPreviousBlock.label = Jump up one block SelectNextPage.label = Jump down one page SelectPreviousPage.label = Jump up one page SelectNextRow.label = Jump down one row SelectPreviousRow.label = Jump up one row SelectFirstRow.label = Select first file in current folder SelectLastRow.label = Select last file in current folder LeftArrowAction.label = Navigate left RightArrowAction.label = Navigate right SplitEqually.label = Split equally SplitVertically.label = Split vertically SplitHorizontally.label = Split horizontally ShowKeyboardShortcuts.label = Keyboard shortcuts GoToWebsite.label = Go to website GoToForums.label = Go to forums ReportBug.label = Report a bug Donate.label = Make a donation ShowAbout.label = About trolCommander OpenTrash.label = Open Trash EmptyTrash.label = Empty Trash CalculateChecksum.label = Calculate checksum MaximizeWindow.label = Maximize MaximizeWindow.label.mac_os_x = Zoom MinimizeWindow.label = Minimize GoToDocumentation.label = Online documentation ShowParentFoldersQL.label = Parent folders ShowRecentLocationsQL.label = Recent locations ShowRecentExecutedFilesQL.label = Recently executed files ShowRootFoldersQL.label = Root folders ShowTabsQL.label = Open tabs ShowRecentViewedFilesQL.label = Recently viewed files ShowRecentEditedFilesQL.label = Recently edited files SplitFile.label = Split SplitFile.tooltip = Split a file into multiple parts CombineFiles.label = Combine CombineFiles.tooltip = Combine split file parts to recreate the original file ShowDebugConsole.label = Debug console FocusPrevious.label = Focus previous component FocusNext.label = Focus next component ShowBookmarksQL.label = Bookmarks ShowEditorBookmarksQL.label = File bookmarks EjectDrive.label = Eject drive EjectDrive.tooltip = Safely remove drive file_menu = File file_menu.open_with = Open with file_menu.open_as = Open as mark_menu = Mark view_menu = View view_menu.show_hide_columns = Show/Hide columns view_menu.table_mode = Mode go_menu = Go tools_menu = Tools eject_menu = Eject drive bookmarks_menu = Bookmarks bookmarks_menu.no_bookmark = No bookmark drive_popup.network_shares = Network shares quick_lists_menu = Quick lists window_menu = Window help_menu = Help status_bar.selected_files = %1 of %2 selected status_bar.connecting_to_folder = Connecting to folder, press ESCAPE to cancel. status_bar.volume_free = Free: %1 status_bar.volume_capacity = Capacity: %1 status_bar.quick_search.press_esc_to_stop_search = press Esc to stop search shortcuts_panel.title = Shortcuts shortcuts_panel.restore_defaults = Restore defaults shortcuts_panel.show = Show shortcuts_panel.search = Search shortcuts_panel.default_message = Press Enter or double-click the shortcut you wish to edit shortcuts_table.action_description = Action description shortcuts_table.shortcut = Shortcut shortcuts_table.alternate_shortcut = Alternate Shortcut shortcuts_table.type_in_a_shortcut = Type in a shortcut command_bar_dialog.help = Drag buttons to customize the command bar table.folder_access_error_title = Folder access error table.folder_access_error = Unable to read folder content table.download_or_browse = Would you like to browse or download this file ? AddTab.label = Add tab AddTab.tooltip = Add new tab in the active panel ToggleLockTab.lock = Lock ToggleLockTab.unlock = Unlock ToggleLockTab.label = Lock/Unlock ToggleLockTab.tooltip = Change tab's locking state DuplicateTab.label = Duplicate DuplicateTab.tooltip = Duplicate active tab in the same panel CloseDuplicateTabs.label = Close duplicates CloseDuplicateTabs.tooltip = Close duplicate tabs CloseOtherTabs.label = Close others CloseOtherTabs.tooltip = Close other tabs CloseTab.label = Close CloseTab.tooltip = Close tab MoveTabToOtherPanel.label = Move to other panel MoveTabToOtherPanel.tooltip = Move tab to other panel CloneTabToOtherPanel.label = Clone to other panel CloneTabToOtherPanel.tooltip = Add similar tab in the other panel NextTab.label = Next tab NextTab.tooltip = Switch to the tab on the right PreviousTab.label = Previous tab PreviousTab.tooltip = Switch to the tab on the left SetTabTitle.label = Set title SetTabTitle.tooltip = Set fixed title for the tab version_dialog.no_new_version_title = No new version version_dialog.no_new_version = Congratulations, you already have the latest version. version_dialog.new_version_title = New version available version_dialog.new_version = A new version of trolCommander is available. version_dialog.new_version_url = A new version of trolCommander is available at %1. version_dialog.not_available_title = Server not reachable version_dialog.not_available = Unable to get version information from server. version_dialog.install_and_restart = Install and restart version_dialog.preparing_for_update = Preparing for update... quit_dialog.title = Quit trolCommander quit_dialog.desc = You have %1 window(s) open. Are you sure you want to quit ? quit_dialog.show_next_time = Show next time destination_dialog.file_exists_action = Default action when file exists destination_dialog.verify_integrity = Verify data integrity destination_dialog.skip_errors = Skip errors destination_dialog.background_mode = In background file_collision_dialog.title = File collision rename_dialog.new_name = New name copy_dialog.destination = Copy selected file(s) to copy_dialog.error_title = Copy error copy_dialog.copying = Copying files copy_dialog.copying_file = Copying %1 pack_dialog.packing = Packing files pack_dialog.packing_file = Compressing %1 pack_dialog.error_title = Pack error pack_dialog_description = Add selected files to pack_dialog.archive_format = Archive format unpack_dialog.destination = Unpack selected file(s) to unpack_dialog.error_title = Unpack error unpack_dialog.unpacking = Unpacking files unpack_dialog.unpacking_file = Unpacking %1 optimizing_archive = Optimizing archive %1 error_while_optimizing_archive = Error while optimizing archive %1 move_dialog.move_description = Move to move_dialog.error_title = Move error move_dialog.moving = Moving files move_dialog.moving_file = Moving %1 download_dialog.description = Download file to download_dialog.download = Download download_dialog.error_title = Download error download_dialog.downloading = Downloading download_dialog.downloading_file = Downloading %1 mkfile_dialog.allocate_space = Allocate space mkfile_dialog.open_in_editor = Open in text editor mkfile_dialog.make_executable = Executable file mkfile_dialog.convert_whitespace = Convert whitespace delete_dialog.permanently_delete.confirmation = Permanently delete selected files ? delete_dialog.permanently_delete.confirmation_1 = Permanently delete selected file ? delete_dialog.permanently_delete.symlink_confirmation_1 = Permanently delete selected symlink ? delete_dialog.move_to_trash.confirmation = Delete selected files ? delete_dialog.move_to_trash.confirmation_1 = Delete selected file ? delete_dialog.move_to_trash.confirmation_details = Files will be moved to the trash. delete_dialog.move_to_trash.confirmation_details_1 = File will be moved to the trash. delete_dialog.move_to_trash.option = Move to trash delete_dialog.move_to_trash.failed = One or more files could not be moved to the trash. delete_dialog.deleting = Deleting delete_dialog.error_title = Delete error delete.deleting_file = Deleting %1 email_dialog.prefs_not_set_title = Mail not configured email_dialog.prefs_not_set = You need to set your mail parameters first. email_dialog.from = From email_dialog.to = To email_dialog.subject = Subject email_dialog.send = Send email_dialog.error_title = Email files error email_dialog.read_error = Unable to read files in subfolders. email_dialog.sending = Sending files email.sending_file = Sending %1 email.connecting_to_server = Connecting to %1 email.server_unavailable = Unable to contact mail server %1, check your mail preferences or try again later. email.connection_closed = Connection closed by server, mail not sent. email.goodbye_failed = Error while closing connection, mail may not have been sent. email.send_file_error = Unable to send file %1, mail not sent. split_file_dialog.error_title = Split file error split_file_dialog.file_to_split = File to split split_file_dialog.target_directory = Target directory split_file_dialog.part_size = Size of each part split_file_dialog.parts = Number of parts split_file_dialog.generate_CRC = Generate CRC file split_file_dialog.max_parts = Maximum allowed number of parts is %1 split_file_dialog.auto = Auto split_file_dialog.insert_new_media = Insert new media combine_files_dialog.error_title = Combine file error combine_files_job.no_crc_file = Combine succeeded. No CRC file. combine_files_job.crc_read_error = Error while reading CRC file. combine_files_job.crc_check_failed = CRC mismatch: expected %2, found %1 combine_files_job.crc_ok = Combine succeeded. CRC checksum ok. file_selection_dialog.mark = Mark file_selection_dialog.unmark = Unmark file_selection_dialog.mark_description = Mark files file_selection_dialog.unmark_description = Unmark files file_selection_dialog.case_sensitive = Case sensitive file_selection_dialog.include_folders = Include folders progress_dialog.starting = Transfer starting... progress_dialog.transferred = Transferred %1 at %2 progress_dialog.elapsed_time = Elapsed time progress_dialog.advanced = Advanced progress_dialog.current_speed = Current speed progress_dialog.limit_speed = Limit speed progress_dialog.close_when_finished = Close window when finished progress_dialog.processing_files = Processing files progress_dialog.processing_file = Processing %1 progress_dialog.verifying_file = Verifying %1 progress_dialog.job_finished = Job finished progress_dialog.job_error = Job error progress_dialog.hide = Hide properties_dialog.file_properties = %1 Properties properties_dialog.contents = Contents properties_dialog.calculating = Calculating... calculate_checksum_dialog.checksum_algorithm = Checksum algorithm calculate_checksum_dialog.temporary_file = Temporary file change_date_dialog.now = Now change_date_dialog.specific_date = Specific date run_dialog.run_command_description = Run in current folder run_dialog.run_in_home_description = Run in home folder run_dialog.command_output = Command output run_dialog.run = Run run_dialog.stop = Stop run_dialog.clear_history = Clear history server_connect_dialog.server_type = Connection type server_connect_dialog.server = Server server_connect_dialog.share = Share server_connect_dialog.domain = Domain server_connect_dialog.username = Username server_connect_dialog.initial_dir = Initial directory server_connect_dialog.port = Port server_connect_dialog.server_url = Server URL server_connect_dialog.http_url = Web site URL server_connect_dialog.connect = Connect server_connect_dialog.protocol = Protocol server_connect_dialog.nfs_version = NFS version server_connect_dialog.private_key = Private key server_connect_dialog.passphrase = Passphrase ftp_connect.passive_mode = Enable passive mode ftp_connect.anonymous_user = Anonymous user ftp_connect.nb_connection_retries = Number of connection retries ftp_connect.retry_delay = Delay between retries (in seconds) http_connect.basic_authentication = HTTP Basic Authentication (optional) server_connections_dialog.disconnect = Disconnect server_connections_dialog.connection_busy = Busy server_connections_dialog.connection_idle = Idle vsphere_connections_dialog.guest_server = Guest Server %1 vsphere_connections_dialog.guest_user = Guest Username vsphere_connections_dialog.guest_password = Guest Password bonjour.bonjour_services = Bonjour services bonjour.no_service_discovered = No service discovered bonjour.bonjour_disabled = Bonjour disabled auth_dialog.title = Authentication auth_dialog.desc = Please enter a login and password auth_dialog.server = Server auth_dialog.store_credentials = Store login and password (weak encryption) auth_dialog.connect_as = Connect as auth_dialog.authentication_failed = Authentication failed sortable_list.move_up = Move up sortable_list.move_down = Move down add_bookmark_dialog.add = Add edit_bookmarks_dialog.location = Location edit_bookmarks_dialog.new = New edit_bookmarks_dialog.is_separator = The specified name defines a separator file_viewer.view_error_title = View error file_viewer.view_error = Unable to view file. file_viewer.file_menu = File file_viewer.close = Close file_viewer.large_file_warning = This file may be too large for this operation. file_viewer.open_anyway = Open anyway file_viewer.open_hex = Hex view text_viewer.edit = Edit text_viewer.copy = Copy text_viewer.select_all = Select All text_viewer.find = Find text_viewer.find_button = Find text_viewer.find_next = Find next text_viewer.find_previous = Find previous text_viewer.find.case_sensitive = Case sensitive text_viewer.find.whole_word = Whole word text_viewer.find.regexp = Regexp text_viewer.find.mark_all = Mark all text_viewer.find.direction = Direction text_viewer.find.up = Up text_viewer.find.down = Down text_viewer.view = View text_viewer.line_wrap = Line wrap text_viewer.line_numbers = Line numbers text_viewer.binary_file_warning = This appears to be a binary file text_viewer.goto_line = Goto line text_viewer.line = Line text_viewer.open_file_error = Can't open file image_viewer.controls_menu = Controls image_viewer.zoom_in = Zoom in image_viewer.zoom_out = Zoom out file_editor.edit_error_title = Edit error file_editor.edit_error = Unable to edit file. file_editor.save = Save file_editor.save_as = Save as... file_editor.save_warning = Save changes made to this file before closing ? file_editor.cannot_write = Unable to write file. file_editor.file_menu = File file_editor.close = Close file_editor.open_anyway = Open anyway file_editor.save_anyway = Save anyway file_editor.overwrite_readonly = The file is read-only file_editor.files = Files file_editor.files.list = Select file file_editor.show_file_manager = Show file manager file_editor.add_to_bookmark = Bookmark file file_editor.remove_from_bookmark = Remove bookmark file_editor.goto_header_source = Go to Header/Source text_editor.cut = Cut text_editor.paste = Paste text_editor.undo = Undo text_editor.redo = Redo text_editor.edit = Edit text_editor.copy = Copy text_editor.select_all = Select all text_editor.view = View text_editor.find = Find text_editor.find_next = Find next text_editor.find_previous = Find previous text_editor.search = Search text_editor.replace = Replace text_editor.replace_menu = Replace... text_editor.replace_button = Replace text_editor.replace_with = Replace with text_editor.replace_all = Replace all text_editor.replaced = Replaced text_editor.occurrences = occurrences text_editor.line_wrap = Line wrap text_editor.line_numbers = Line numbers text_editor.syntax = Syntax text_editor.format = Format text_editor.writing = Writing... text_editor.modified = Modified text_editor.saved = File saved text_editor.text_not_found = Text not found text_editor.found = Found text_editor.matches = matches text_editor.cant_save_file = Can't save file text_editor.press_alt_enter_to_open_file = Press Alt+Enter to open file text_editor.tools=Tools text_editor.build=Build text_editor.invisible_chars = Invisible chars shortcuts_dialog.quick_search = Quick search shortcuts_dialog.quick_search.start_search = Type in any character to start a quick search shortcuts_dialog.quick_search.cancel_search = Cancel quick search shortcuts_dialog.quick_search.remove_last_char = Remove last character from quick search string shortcuts_dialog.quick_search.jump_to_previous = Jump to previous quick search result shortcuts_dialog.quick_search.jump_to_next = Jump to next quick search result shortcuts_dialog.quick_search.mark_jump_next = Mark/Unmark current file and jump to next search result theme_editor.title = Theme editor theme_editor.folder_tab = Folder pane theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Shell history theme_editor.terminal_tab = Terminal theme_editor.statusbar_tab = Status bar theme_editor.free_space = Free space theme_editor.free_space.ok = OK theme_editor.free_space.warning = Warning theme_editor.free_space.critical = Critical theme_editor.locationbar_tab = Location bar theme_editor.editor_tab = File editor theme_editor.font = Font theme_editor.active_panel = Active theme_editor.inactive_panel = Inactive theme_editor.general = General theme_editor.theme_warning_predefined = Build-in themes cannot be modified. Do you whish to create a new theme? theme_editor.could_not_save_theme = Unable to write theme %1 theme_editor.border = Border theme_editor.background = Background theme_editor.alternate_background = Alternate background theme_editor.unfocused_background = Background (without focus) theme_editor.copy_colors = Copy %1 theme_editor.quick_search = Quick search theme_editor.quick_search.unmatched_file = Unmatched file theme_editor.text = Text theme_editor.progress = Progress theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (without focus) theme_editor.selected = Selected theme_editor.selected_unfocused = Selected (without focus) theme_editor.color = Color theme_editor.colors = Colors theme_editor.plain_file = Plain file theme_editor.marked_file = Marked file theme_editor.hidden_folder = Hidden folder theme_editor.hidden_file = Hidden file theme_editor.folder = Folder theme_editor.archive_file = Archive theme_editor.symbolic_link = Symbolic link theme_editor.executable_file = Executable file theme_editor.header = Header theme_editor.current = Current line theme_editor.file_groups = File groups theme_editor.group_ = Group theme_editor.normal_color = Normal color theme_editor.selected_color = Selected color theme_editor.filemask = File masks theme_editor.group_file_ = File of group theme_editor.item = Item theme_editor.text_editor_tab = Text viewer and editor theme_editor.hex_viewer_tab = Hex viewer theme_editor.normal_hex = Hex theme_editor.normal_offset = Offset theme_editor.normal_ascii = ASCII theme_editor.alternate = Alternate theme_editor.selected_hex = Selected hex theme_editor.selected_ascii = Selected ASCII command_bar_customize_dialog.available_actions = Available Actions command_bar_customize_dialog.modifier = Modifier prefs_dialog.title = Preferences prefs_dialog.general_tab = General prefs_dialog.day = Day prefs_dialog.month = Month prefs_dialog.year = Year prefs_dialog.language = Language (requires restart) prefs_dialog.date_time = Date & time format prefs_dialog.time = Time prefs_dialog.date = Date prefs_dialog.date_separator = Separator prefs_dialog.time_12_hour = 12-hour format prefs_dialog.time_24_hour = 24-hour format prefs_dialog.show_seconds = Show seconds prefs_dialog.show_century = Show century prefs_dialog.check_for_updates_on_startup = Check for updates on startup prefs_dialog.show_splash_screen = Show splash screen prefs_dialog.folders_tab = Folders prefs_dialog.startup_folders = Startup folders prefs_dialog.left_folder = Left folder prefs_dialog.right_folder = Right folder prefs_dialog.last_folder = Last visited folder prefs_dialog.custom_folder = Custom folder prefs_dialog.quick_search = Quick search prefs_dialog.quick_search_timeout = Stop quick search mode after inactivity prefs_dialog.quick_search_timeout_never = Never prefs_dialog.quick_search_timeout_sec = Sec prefs_dialog.show_quick_search_matches_first = Show matches first prefs_dialog.show_hidden_files = Show hidden files prefs_dialog.show_ds_store_files = Show .DS_Store files prefs_dialog.show_system_folders = Show system folders prefs_dialog.compact_file_size = Round displayed file sizes prefs_dialog.follow_symlinks_when_cd = Follow symlinks when changing current directory prefs_dialog.show_tab_header = Always show tab header prefs_dialog.appearance_tab = Appearance prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Icon size prefs_dialog.toolbar_icons = Toolbar prefs_dialog.command_bar_icons = Command bar prefs_dialog.file_icons = File types prefs_dialog.use_system_file_icons = Use system file icons prefs_dialog.use_system_file_icons.always = Always prefs_dialog.use_system_file_icons.never = Never prefs_dialog.use_system_file_icons.applications = For applications only prefs_dialog.edit_current_theme = Edit current theme... prefs_dialog.themes = Themes prefs_dialog.syntax_themes = Editor syntax coloring theme prefs_dialog.import_theme = Import theme prefs_dialog.import_look_and_feel = Import look & feel prefs_dialog.no_look_and_feel = No look & feel was found. prefs_dialog.error_in_import = Error while importing theme %1. prefs_dialog.cannot_read_theme = Cannot load theme from file %1 prefs_dialog.export_theme = Export %1 prefs_dialog.import = Import prefs_dialog.export = Export prefs_dialog.theme_type = Type: %1 prefs_dialog.delete_theme = Permanently delete theme %1 ? prefs_dialog.delete_look_and_feel = Permanently delete look & feel %1 ? prefs_dialog.rename_failed = Failed to rename theme %1 prefs_dialog.xml_file = XML file prefs_dialog.jar_file = JAR file prefs_dialog.mail_tab = Mail prefs_dialog.mail_settings = Outgoing mail settings prefs_dialog.mail_name = Your name prefs_dialog.mail_address = Your email address prefs_dialog.mail_server = SMTP server prefs_dialog.misc_tab = Misc prefs_dialog.use_brushed_metal = Use 'brushed metal' look (requires restart) prefs_dialog.confirm_on_quit = Show confirmation dialog on quit prefs_dialog.shell = Run command prefs_dialog.default_shell = Use default system shell prefs_dialog.custom_shell = Use custom shell prefs_dialog.shell_encoding = Shell encoding prefs_dialog.auto_detect_shell_encoding = Auto-detect prefs_dialog.external_terminal = External terminal prefs_dialog.default_terminal = Use default Terminal program prefs_dialog.custom_terminal = Use custom command prefs_dialog.iterm_terminal = Use iTerm prefs_dialog.builtin_terminal = Builtin terminal prefs_dialog.enable_bonjour_discovery = Enable Bonjour services discovery prefs_dialog.enable_system_notifications = Enable system notifications prefs_dialog.calculate_folder_size_on_mark = Calculate folder size on mark debug_console_dialog.level = Level debug_console_dialog.threads = Threads debug_console_dialog.active_threads = Active threads unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mo duration.years = %1y duration.infinite = ∞ theme.custom_theme = Custom theme theme.custom = Customized theme.built_in = Built-in theme.add_on = Add-on theme.current = current theme_could_not_be_loaded = An error occurred while loading this theme. cannot_open_cyclic_symlink = Cannot open the selected link because it is cyclic setup.title = Welcome to trolCommander setup.intro = Please choose the way you want trolCommander to behave. setup.look_and_feel = Select your Look & feel setup.theme = Select your theme font_chooser.font_size = Size font_chooser.font_bold = Bold font_chooser.font_italic = Italic color_chooser.red = Red color_chooser.green = Green color_chooser.blue = Blue color_chooser.hue = Hue color_chooser.brightness = Brightness color_chooser.swatches = Swatches color_chooser.saturation = Saturation color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Recent color_chooser.alpha = Alpha transparency color_chooser.title = Pick a color batch_rename_dialog.mask = Rename pattern batch_rename_dialog.search_replace = Search & Replace batch_rename_dialog.search_for = Search for batch_rename_dialog.replace_with = Replace with batch_rename_dialog.counter = Counter batch_rename_dialog.start_at = Start at batch_rename_dialog.step_by = Step by batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Upper/Lowercase batch_rename_dialog.no_change = Unchanged batch_rename_dialog.lower_case = lowercase batch_rename_dialog.upper_case = UPPERCASE batch_rename_dialog.first_upper = First letter uppercase batch_rename_dialog.word = First Of Each Word batch_rename_dialog.regexp = RegExp batch_rename_dialog.regexp_error = Syntax error in regexp batch_rename_dialog.old_name = Old name batch_rename_dialog.new_name = New name batch_rename_dialog.block_name = Preserve batch_rename_dialog.range = Range batch_rename_dialog.proceed_renaming = %1 files out of %2 will be renamed. Do you wish to proceed? batch_rename_dialog.duplicate_names = Duplicate names! batch_rename_dialog.names_conflict = Names conflict! Same values in old and new names. parent_folders_quick_list.empty_message = Current location has no parent recent_locations_quick_list.empty_message = No recent location recent_executed_files_quick_list.empty_message = No recently executed file recent_edited_files_quick_list.empty_message = No recently edited file recent_viewed_files_quick_list.empty_message = No recently viewed file roots_quick_list.empty_message = No root folders available tabs_quick_list.empty_message = Only one tab is presented editor_bookmarks_quick_list.empty_message = No files editor_bookmarks_quick_list.file_not_found = File not found editor_bookmarks_quick_list.press_f4_to_edit_list = Press F4 to edit bookmarks list #server_connect_dialog.auth_error = Invalid login or password. #move_dialog.cannot_move_to_itself = Cannot move files to a subfolder. #pack.error_on_file = Error while compressing %1 #pack_dialog.cannot_write = Unable to create file in destination folder. #unpack.unable_to_open_zip = Unable to open zip file %1. #image_viewer.previous_image = Previous image #image_viewer.next_image = Next image #mkdir_dialog.title = Make directory #mkdir_dialog.error_title = Creation error #edit_bookmarks_dialog.remove = Remove #mkdir_dialog.description = Create directory #mkfile_dialog.description = Create new empty file #done = Done #move_dialog.rename_description = Rename file to #theme_editor.shell_font = Shell font #theme_editor.history_font = History font #theme_editor.shell_colors = Shell colors #theme_editor.history_colors = History colors #ToggleHiddenFiles.hide = Do not show hidden files #auth_dialog.error_was = Error was: %1 #table.hide_column = Hide column #delete.symlink_warning_title = Symlink found #delete.symlink_warning = This file looks like a symbolic link:\n\n File: %1\n Links to: %2\n\nDelete symlink only or\nFollow symlink and delete folder (CAUTION) ? #delete.delete_link_only = Delete link #delete.delete_linked_folder = Delete folder #Unpack.label = Unpack files #Pack.label = Pack files find_dialog.name = File name find_dialog.contains = Contains find_dialog.initial_directory = Start at find_dialog.search_subdirectories = Search subdirectories find_dialog.search_archives = Search archives find_dialog.case_sensitive = Case sensitive find_dialog.ignore_hidden = Ignore hidden find_dialog.search_results = Search results find_dialog.found = Found files find_dialog.encoding = Text encoding find_dialog.search_hex = Search hex image_viewer.next_image = Next image image_viewer.previous_image = Previous image hex_viewer.offset = Offset hex_viewer.ascii_dump = ASCII dump hex_viewer.view = View hex_viewer.goto = Goto hex_viewer.goto.offset = Offset hex_viewer.search = Search hex_viewer.searchNext = Find next hex_viewer.searchPrev = Find previous hex_viewer.find = Find hex_view.text = Search for hex_viewer.hex = Hex hex_viewer.search_not_found = Pattern not found calculator.calculator = Calculator calculator.expression = Expression calculator.error = Error in expression replication = Replication factor blocksize = Block Size ChangeReplication.label = Change Replication replication.number = Replication factor adb.android_devices = Android adb.no_devices = No devices eject.no_mounted_devices = No mounted devices retry_as_root = Retry as root ================================================ FILE: src/main/resources/dictionary_ar_SA.properties ================================================ ok = موافق yes = نعم no = لا cancel = إلغاء edit = تحرير close = إغلاق reset = تصفير rename = إعادة تسمية apply = تطبيق change = تغيير save = حفظ dont_save = عدم الحفظ replace = استبدال dont_replace = عدم الاستبدال delete = حذف skip = تخطي skip_all = تخطي الكل retry = حاول مجدداً resume = أكمل overwrite = كتابة على overwrite_if_older = الكتابة على القديم duplicate = تكرار apply_to_all = تطبيق على الكل copy = نسخ move = نقل pack = أرشفة unpack = استخراج download = تنزيل split = تفسيم combine = دمج browse = تصفح ask = اسأل stop = إيقاف pause = إيقاف مؤقت quick_search = بحث سريع file_manager = مدير الملفات create = إنشاء creating_file = إنشاء %1 choose = اختيار customize = تخصيص choose_folder = اختيار مجلد login = ولوج password = كلمة السر user = مستخدم encoding = ترميز preferred_encodings = الترميز المفضل license = ترخيص name = اسم size = حجم date = تاريخ extension = لاحقة permissions = صلاحيات owner = مالك group = مجموعة location = موقع untitled = غير معنون source = مصدر destination = الوجهة recurse_directories = تكرار معالجة الأدلة المحددة go_to = الذهاب إلى example = مثال preview = معاينة comment = تعليق sample_text = عينة نص nb_files = %1 ملفـ(ات) nb_folders = %1 مجلد(ات) loading = تحميل... this_operation_cannot_be_undone = هذه العملية لا يمكن إلغاؤها. remove = حذف details = تفاصيل warning = تحذير error = خطأ generic_error = حدث خطأ أثناء إنجاز العملية المطلوبة folder_does_not_exist = هذا المجلد غير موجود أو أنه غير متاح. this_folder_does_not_exist = المجلد التالي غير موجود أو أنه غير متاح: %1 this_file_does_not_exist = الملف التالي غير موجود أو أنه غير متاح: %1 invalid_path = مسار خاطئ: %1 directory_already_exists = الدليل %1 موجود مسبقاً file_exists_in_destination = الملف موجود مسبقاً في الوجهة المحددة source_parent_of_destination = محاولة نقل مجلد إلى أحد مجلداته الفرعية cannot_read_file = لا يمكن قراءة الملف %1 cannot_write_file = لا يمكن كتابة الملف %1 cannot_create_folder = لا يمكن إنشاء الدليل %1 cannot_read_folder = لا يمكن قراءة محتويات المجلد %1 cannot_delete_file = لا يمكن حذف المجلد %1 cannot_delete_folder = لا يمكن حذف المجلد %1 error_while_transferring = خطأ أثناء نقل الملف %1 same_source_destination = مجلد المصدر هو نفسه مجلد الوجهة. file_already_exists = %1 موجود مسبقاً، هل تريد استبداله؟ write_error = خطأ كتابة read_error = خطأ قراءة integrity_check_error = فشل التحقق من التكامل: المصدر والوجهة غير متطابقين startup_error = حدث خطأ منع من تشغيل trolCommander. permissions.read = قراءة permissions.write = كتابة permissions.executable = تنفيذ permissions.group = مجموعة permissions.other = آخر permissions.octal_notation = تدوين ثماني action_categories.all = الكل action_categories.navigation = تصفح action_categories.selection = تحديد action_categories.view = عرض action_categories.file_operations = عمليات الملفات action_categories.windows = نوافذ Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = إضافة علامة AddBookmark.tooltip = إضافة المجلد الحالي إلى قائمة العلامات BatchRename.label = إعادة تسمية بالملفات الدفعية EditBookmarks.label = تحرير العلامات ExploreBookmarks.label = استكشاف العلامات EditCredentials.label = تحرير الاعتمادات ChangeLocation.label = تغيير الموقع الحالي ChangeDate.label = تغيير التاريخ ChangeDate.tooltip = تغيير تاريخ الملفـ(ات) المحددة ChangePermissions.label = تغيير الصلاحيات ChangePermissions.tooltip = تغيير صلاحيات الملفـ(ات) المحددة. CheckForUpdates.label = التحقق من التحديثات CompareFolders.label = مقارنة المجلدات ConnectToServer.label = الاتصال بالخادم ConnectToServer.tooltip = الاتصال بخادم بعيد View.label = عرض InternalView.label = عرض (محلي) View.tooltip = عرض الملف المحدد InternalEdit.label = تحرير (محلي) Edit.tooltip = تحرير الملف المحدد Copy.tooltip = نسخ الملفات المحددة LocalCopy.label = نسخة محلية LocalCopy.tooltip = نسخ الملف المحدد في المجلد الحالي Move.tooltip = نقل الملفات المحددة Rename.tooltip = إعادة تسمية الملف المحدد Mkdir.label = إنشاء مجلد Mkdir.tooltip = إنشاء دليل في المجلد الحالي Mkfile.label = إنشاء ملف Mkfile.tooltip = إنشاء ملف في المجلد الحالي Delete.tooltip = حذف الملفات المحددة باستخدام سلة المحذوفات إن أمكن PermanentDelete.label = حذف نهائي PermanentDelete.tooltip = حذف الملفات المحددة بدون استخدام سلة المحذوفات Refresh.label = تحديث Refresh.tooltip = تحديث المجلد الحالي CloseWindow.label = إغلاق النافذة CloseWindow.tooltip = إغلاق هذه النافذة CopyFileNames.label = نسخ الأسمـ(اء) CopyFilePaths.label = نسخ المسار(ات) CopyFilesToClipboard.label = نسخ الملفـ(ات) PasteClipboardFiles.label = إلصاق الملفـ(ات) Email.label = إرسال بالبريد Email.tooltip = إرسال الملفات المحددة كمرفقات بريد GoBack.label = ارجع للخلف GoBack.tooltip = ارجع للمجلد السابق GoForward.label = تقدم للأمام GoForward.tooltip = اذهب للمجلد التالي GoToHome.label = اذهب لمجلد المنزل GoToParent.label = اذهب للأب GoToParent.tooltip = اذهب للمجلد الأب GoToParentInOtherPanel.label = اذهب للأب في لوحة أخرى GoToParentInBothPanels.label = اذهب للأب في كلا اللوحتين GoToRoot.label = اذهب للجذر SortByName.label = فرز حسب الاسم SortByDate.label = فرز حسب التاريخ SortBySize.label = فرز حسب الحجم SortByExtension.label = فرز حسب اللاحقة SortByPermissions.label = فرز حسب الصلاحيات SortByOwner.label = فرز حسب المالك SortByGroup.label = فرز حسب المجموعة MarkGroup.label = تحديد ملفات MarkGroup.tooltip = تحديد مجموعة من الملفات UnmarkGroup.label = إلغاء تحديد ملفات UnmarkGroup.tooltip = إلغاء تحديد مجموعة من الملفات MarkAll.label = تحديد الكل UnmarkAll.label = إلغاء تحديد الكل MarkSelectedFile.label = تحديد/إلغاء MarkSelectedFile.tooltip = تحديد/إلغاء الملفات المعينة MarkNextBlock.label = تحديد كتلة واحدة للأسفل MarkPreviousBlock.label = تحديد كتلة واحدة للأعلى MarkNextRow.label = تحديد صف واحد للأسفل MarkPreviousRow.label = تحديد صف واحد للأعلى MarkNextPage.label = تحديد صفحة للأسفل MarkPreviousPage.label = تحديد صفحة للأعلى MarkToFirstRow.label = تحديد الملفات حتى البداية MarkToLastRow.label = تحديد الملفات حتى النهاية MarkExtension.label = تحديد لاحقة InvertSelection.label = عكس التحديد SwapFolders.label = تبديل المجلدات SwapFolders.tooltip = تبديل المجلدين الأيمن والأيسر SetSameFolder.label = تعيين نفس المجلد SetSameFolder.tooltip = تعيين نفس الدليل للمجلدين الأيمن والأيسر NewWindow.label = نافذة جديدة NewWindow.tooltip = فتح نافذة جديدة Open.label = فتح Open.tooltip = دخول المجلد / دخول الأرشيف / تنفيذ OpenNatively.label = فتح عادي OpenNatively.tooltip = تنفيذ الملف المحدد حسب إعدادات النظام OpenInOtherPanel.label = فتح في لوحة أخرى OpenInBothPanels.label = فتح في كلا اللوحتين RevealInDesktop.label = استكشاف في %1 RunCommand.label = تنفيذ أمر RunCommand.tooltip = تنفيذ أمر في المجلد الحالي Pack.tooltip = أرشفة الملفات المحددة في أرشيف Unpack.tooltip = استخراج الملفات المحددة من الأرشيف ShowFileProperties.label = خصائص ShowFileProperties.tooltip = إظهار خصائص الملفات المحددة ShowPreferences.label = تفضيلات ShowPreferences.tooltip = ضبط trolCommander ShowServerConnections.label = إظهار الاتصالات المفتوحة Quit.label = إنهاء ReverseSortOrder.label = عكس الترتيب ToggleAutoSize.label = تحجيم الأعمدة تلقائياً Stop.label = إيقاف تغيير الملفات ToggleColumn.show = إظهار عمود %1 ToggleColumn.hide = إخفاء عمود %1 ToggleCommandBar.show = إظهار شريط الأوامر ToggleCommandBar.hide = إخفاء شريط الأوامر ToggleToolBar.show = إظهار شريط الأدوات ToggleToolBar.hide = إخفاء شريط الأدوات CustomizeCommandBar.label = تخصيص ToggleStatusBar.show = إظهار شريط الحالة ToggleStatusBar.hide = إخفاء شريط الحالة ToggleShowFoldersFirst.label = إظهار المجلدات أولاً ToggleTree.label = إظهار العرض الشجري PopupLeftDriveButton.label = تغيير المجلد الأيسر PopupRightDriveButton.label = تغيير المجلد الأيمن RecallPreviousWindow.label = إعادة النافذة السابقة RecallNextWindow.label = إعادة النافذة التالية RecallWindow.label = إعادة النافذة #%1 BringAllToFront.label = إحضار جميع النوافذ للمقدمة SwitchActiveTable.label = تبديل بين اللوحتين اليمنى واليسرى SelectNextBlock.label = قفز كتلة واحدة للأسفل SelectPreviousBlock.label = قفز كتلة واحدة للأعلى SelectNextPage.label = قفز صفحة واحدة للأسفل SelectPreviousPage.label = قفز صفحة واحدة للأعلى SelectNextRow.label = قفز صف واحد للأسفل SelectPreviousRow.label = قفز صف واحد للأعلى SelectFirstRow.label = اختيار الملف الأول في المجلد الحالي SelectLastRow.label = اختيار الملف الأخير في المجلد الحالي SplitEqually.label = تقسيم بشكل متساوي SplitVertically.label = تقسيم بشكل رأسي SplitHorizontally.label = تقسيم بشكل أفقي ShowKeyboardShortcuts.label = اختصارات لوحة المفاتيح GoToWebsite.label = اذهب إلى الموقع GoToForums.label = اذهب إلى المنتديات ReportBug.label = تبليغ عن خلل Donate.label = قم بالتبرع ShowAbout.label = حول trolCommander OpenTrash.label = افتح سلة المحذوفات EmptyTrash.label = تفريغ سلة المحذوفات CalculateChecksum.label = حساب تدقيق المجموع MaximizeWindow.label = تكبير MinimizeWindow.label = تصغير GoToDocumentation.label = الاتصال بالوثائق ShowParentFoldersQL.label = المجلدات الأب ShowRecentLocationsQL.label = المواقع الأخيرة ShowRecentExecutedFilesQL.label = آخر الملفات تشغيلاً SplitFile.tooltip = تقسيم ملف إلى عدة أجزاء CombineFiles.tooltip = إعادة دمج أقسام ملف وتجميع الملف الأصلي ShowDebugConsole.label = لوحة التنقيح FocusPrevious.label = التركيز على المكون السابق FocusNext.label = التركيز على المكون التالي file_menu = ملف file_menu.open_with = فتح بواسطة mark_menu = تحديد view_menu = عرض view_menu.show_hide_columns = إظهار/إخفاء الأعمدة go_menu = اذهب bookmarks_menu = علامات bookmarks_menu.no_bookmark = لا توجد علامات quick_lists_menu = القوائم السريعة window_menu = نوافذ help_menu = مساعدة status_bar.selected_files = %1 من %2 محدد status_bar.connecting_to_folder = جاري الاتصال بالمجلد، اضغط خروج للإلغاء. status_bar.volume_free = مساحة فارغة: %1 status_bar.volume_capacity = السعة: %1 shortcuts_panel.title = الاختصارات shortcuts_panel.restore_defaults = استعادة الافتراضيات shortcuts_panel.show = إظهار shortcuts_panel.default_message = اضغط إدخال أو مزدوجة بزر الفأرة على الاختصار الذي تود تعديله shortcuts_table.action_description = وصف الحدث shortcuts_table.shortcut = اختصار shortcuts_table.alternate_shortcut = اختصار بديل shortcuts_table.type_in_a_shortcut = إدخال اختصار command_bar_dialog.help = اسحب الأزرار لتخصيص شريط الأوامر table.folder_access_error_title = خطأ في الوصول إلى المجلد table.folder_access_error = لا مكن قراءة محتويات المجلد table.download_or_browse = هل تريد تصفح أو تنزيل هذا الملف؟ version_dialog.no_new_version_title = لا إصدارات جديدة version_dialog.no_new_version = تهانينا، أنت لديك الإصدار الأخير. version_dialog.new_version_title = توفر إصدار جديد version_dialog.new_version = توفر إصدار جديد من trolCommander. version_dialog.new_version_url = توفر إصدار جديد من trolCommander على %1. version_dialog.not_available_title = لا يمكن الوصول للخادم version_dialog.not_available = لا يمكن الحصول على معلومات الإصدار من الخادم. version_dialog.install_and_restart = تنصيب وإعادة تشغيل version_dialog.preparing_for_update = التحضير للتحديثات... quit_dialog.title = إنهاء trolCommander quit_dialog.desc = لديك %1 من النوافذ المفتوحة، هل أنت متأكد أنك تريد الخروج؟ quit_dialog.show_next_time = إظهار في المرة القادمة destination_dialog.file_exists_action = التصرف الافتراضي عند وجود الملف destination_dialog.verify_integrity = تدقيق تكامل البيانات destination_dialog.skip_errors = تجاوز الأخطاء file_collision_dialog.title = تصادم ملفات rename_dialog.new_name = الاسم الجديد copy_dialog.destination = نسخ الملفـ(ات) المحددة إلى copy_dialog.error_title = خطأ نسخ copy_dialog.copying = نسخ الملفات copy_dialog.copying_file = نسخ %1 pack_dialog.packing = أرشفة الملفات pack_dialog.packing_file = ضغط %1 pack_dialog.error_title = خطأ أرشفة pack_dialog_description = إضافة الملفات المحددة إلى pack_dialog.archive_format = صيغة الأرشيف unpack_dialog.destination = استخراج الملفـ(ات) المحددة إلى unpack_dialog.error_title = خطأ استخراج unpack_dialog.unpacking = استخراج الملفات unpack_dialog.unpacking_file = استخراج %1 optimizing_archive = تعديل الأرشيف %1 error_while_optimizing_archive = خطأ أثناء تعديل الأرشيف %1 move_dialog.move_description = نقل إلى move_dialog.error_title = خطأ نقل move_dialog.moving = نقل الملفات move_dialog.moving_file = نقل %1 download_dialog.description = تنزيل الملف إلى download_dialog.error_title = خطأ تنزيل download_dialog.downloading = تنزيل download_dialog.downloading_file = تنزيل %1 mkfile_dialog.allocate_space = المساحة المحجوزة delete_dialog.permanently_delete.confirmation = حذف الملفـ(ات) نهائياً؟ delete_dialog.move_to_trash.confirmation = حذف الملف(ات) المحددة؟ delete_dialog.move_to_trash.confirmation_details = الملفات سوف تنقل إلى سلة المحذوفات delete_dialog.move_to_trash.option = نقل إلى سلة المحذوفات delete_dialog.move_to_trash.failed = ملف أو أكثر لا يمكن نقله إلى سلة المحذوفات. delete_dialog.deleting = حذف delete_dialog.error_title = خطأ حذف delete.deleting_file = حذف %1 email_dialog.prefs_not_set_title = لم يتم إعداد البريد email_dialog.prefs_not_set = تحتاج أولاً إلى تعيين وسائط البريد. email_dialog.from = المرسل email_dialog.to = إلى email_dialog.subject = العنوان email_dialog.send = إرسال email_dialog.error_title = خطأ في إرسال الملفات email_dialog.read_error = لا يمكن قراءة الملفات في المجلدات الفرعية. email_dialog.sending = إرسال الملفات email.sending_file = إرسال %1 email.connecting_to_server = الاتصال بـ to %1 email.server_unavailable = غير قادر على الاتصال بخادم البريد %1، تأكد من إعدادات البريد أو حاول مرة أخرى. email.connection_closed = الخادم أغلق الاتصال، لم يتم إرسال البريد. email.goodbye_failed = خطأ أثناء إغلاق الاتصال، قد لا يكون البريد قد أرسل. email.send_file_error = لا يمكن إرسال الملف %1، لم يتم إرسال البريد split_file_dialog.error_title = خطأ في تقسيم الملف split_file_dialog.file_to_split = تقسيم الملف split_file_dialog.target_directory = الدليل الهدف split_file_dialog.part_size = حجم الجزء الواحد split_file_dialog.parts = عدد الأجزاء split_file_dialog.generate_CRC = توليد ملف CRC split_file_dialog.max_parts = العدد الأكبر من الأجزاء هو %1 split_file_dialog.auto = آلي split_file_dialog.insert_new_media = أدخل قرص جديد combine_files_dialog.error_title = خطأ في دمج الملفات combine_files_job.no_crc_file = نجح الدمج، لا يوجد ملف CRC. combine_files_job.crc_read_error = خطأ أثناء قراءة ملف CRC. combine_files_job.crc_check_failed = عدم تطابق CRC: الموجود %1، المتوقع %2 combine_files_job.crc_ok = تم الدمج. نجح اختبار تحقق CRC. file_selection_dialog.mark = تحديد file_selection_dialog.unmark = إلغاء تحديد file_selection_dialog.mark_description = تحديد الملفات باسم file_selection_dialog.unmark_description = إلغاء تحديد الملفات باسم file_selection_dialog.case_sensitive = حساس لحالة الأحرف file_selection_dialog.include_folders = تضمين المجلدات progress_dialog.starting = يبدأ النقل... progress_dialog.transferred = نقل %1 في %2 progress_dialog.elapsed_time = الوقت الماضي progress_dialog.advanced = متقدم progress_dialog.current_speed = السرعة الحالية progress_dialog.limit_speed = تحديد السرعة progress_dialog.close_when_finished = إغلاق النافذة عند الانتهاء progress_dialog.processing_files = معالجة الملفات progress_dialog.processing_file = معالجة %1 progress_dialog.verifying_file = تدقيق %1 progress_dialog.job_finished = انتهت المهمة progress_dialog.job_error = خطأ في المهمة properties_dialog.file_properties = %1 خصائص properties_dialog.contents = محتويات properties_dialog.calculating = حساب... calculate_checksum_dialog.checksum_algorithm = خوارزمية تدقيق المجموع calculate_checksum_dialog.temporary_file = ملف مؤقت change_date_dialog.now = الآن change_date_dialog.specific_date = تاريخ معين run_dialog.run_command_description = تشغيل في المجلد الحالي run_dialog.run_in_home_description = تشغل في مجلد المنزل run_dialog.command_output = خرج الأمر run_dialog.run = تشغيل run_dialog.clear_history = مسح المحفوظات server_connect_dialog.server_type = نوع الاتصال server_connect_dialog.server = الخادم server_connect_dialog.share = مشاركة server_connect_dialog.domain = نطاق server_connect_dialog.username = اسم المستخدم server_connect_dialog.initial_dir = الدليل الأولي server_connect_dialog.port = منفذ server_connect_dialog.server_url = عنوان الخادم server_connect_dialog.http_url = عنوان موقع الويب server_connect_dialog.connect = اتصال server_connect_dialog.protocol = بروتوكول server_connect_dialog.nfs_version = إصدار NFS server_connect_dialog.private_key = المفتاح الخاص server_connect_dialog.passphrase = كلمة السر ftp_connect.passive_mode = تفعيل النمط السلبي ftp_connect.anonymous_user = مستخدم مجهول ftp_connect.nb_connection_retries = عدد إعادة محاولات الاتصال ftp_connect.retry_delay = المدة بين محاولتي الاتصال (بالثواني) http_connect.basic_authentication = مصادقة HTTP البدائية (اختياري) server_connections_dialog.disconnect = قطع الاتصال server_connections_dialog.connection_busy = مشغول server_connections_dialog.connection_idle = ساكن bonjour.bonjour_services = خدمات Bonjour bonjour.no_service_discovered = خدمة بدون كلمة سر bonjour.bonjour_disabled = Bonjour معطلة auth_dialog.title = مصادقة auth_dialog.desc = الرجاء إدخال المعرف وكلمة السر auth_dialog.server = الخادم auth_dialog.store_credentials = تخزين المعرف وكلمة السر (تشفير ضعيف) auth_dialog.connect_as = الاتصال كـ auth_dialog.authentication_failed = فشل في المصادقة sortable_list.move_up = التحريك للأعلى sortable_list.move_down = التحريك للأسفل add_bookmark_dialog.add = إضافة edit_bookmarks_dialog.new = جديد file_viewer.view_error_title = خطأ عرض file_viewer.view_error = لا يمكن عرض الملف file_viewer.file_menu = ملف file_viewer.close = إغلاق file_viewer.large_file_warning = هذا الملف قد يكون كبيراً جداً على هذه العملية file_viewer.open_anyway = فتح على أية حال text_viewer.edit = تحرير text_viewer.copy = نسخ text_viewer.select_all = تحديد الكل text_viewer.find = بحث text_viewer.find_next = بحث عن التالي text_viewer.find_previous = بحث عن السابق text_viewer.binary_file_warning = يبدو وكأنه ملف ثنائي (Binary) image_viewer.controls_menu = تحكمات image_viewer.zoom_in = تكبير image_viewer.zoom_out = تصغير file_editor.edit_error_title = خطأ تحرير file_editor.edit_error = لا يمكن تحرير الملف file_editor.save = حفظ file_editor.save_as = حفظ باسم... file_editor.save_warning = حفظ التغييرات على هذا الملف قبل الإغلاق؟ file_editor.cannot_write = لا يمكن الكتابة للملف. text_editor.cut = قص text_editor.paste = إلصاق shortcuts_dialog.quick_search.start_search = اضغط أي محرف لتبدأ البحث السريع shortcuts_dialog.quick_search.cancel_search = إلغاء البحث السريع shortcuts_dialog.quick_search.remove_last_char = حذف آخر محرف من نص البحث السريع shortcuts_dialog.quick_search.jump_to_previous = استحضار نتيجة البحث السريع السابقة shortcuts_dialog.quick_search.jump_to_next = استحضار نتيجة البحث السريع التالية shortcuts_dialog.quick_search.mark_jump_next = تحديد/إلغاء الملف الحالي واستحضار نتيجة البحث التالية theme_editor.title = محرر الثمة theme_editor.folder_tab = لوح المجلد theme_editor.shell_tab = صدفة theme_editor.shell_history_tab = محفوظات الصدفة theme_editor.statusbar_tab = شريط المعلومات theme_editor.free_space = المساحة الحرة theme_editor.free_space.ok = موافق theme_editor.free_space.warning = تحذير theme_editor.free_space.critical = خطير theme_editor.locationbar_tab = شريط الموقع theme_editor.editor_tab = محرر الملف theme_editor.font = خط theme_editor.active_panel = فعال theme_editor.inactive_panel = غير فعال theme_editor.general = عام theme_editor.could_not_save_theme = لا يمكن كتابة الثمة %1 theme_editor.border = إطار theme_editor.background = خلفية theme_editor.alternate_background = خلفية بديلة theme_editor.unfocused_background = الخلفية (بدون تركيز) theme_editor.quick_search.unmatched_file = ملفان غير متطابقان theme_editor.text = نص theme_editor.progress = تقدم theme_editor.normal = طبيعي theme_editor.normal_unfocused = طبيعي (بدون تركيز) theme_editor.selected = محدد theme_editor.selected_unfocused = محدد (بدون تركيز) theme_editor.color = لون theme_editor.colors = ألوان theme_editor.plain_file = ملف بسيط theme_editor.marked_file = ملف محدد theme_editor.hidden_file = ملف مخفي theme_editor.folder = مجلد theme_editor.archive_file = أرشيف theme_editor.symbolic_link = وصلة رمزية theme_editor.header = الرأس theme_editor.item = عنصر command_bar_customize_dialog.available_actions = الأفعال المتاحة command_bar_customize_dialog.modifier = المعدل prefs_dialog.title = تفضيلات prefs_dialog.general_tab = عام prefs_dialog.day = يوم prefs_dialog.month = شهر prefs_dialog.year = سنة prefs_dialog.language = اللغة (تحتاج إلى إعادة تشغيل) prefs_dialog.date_time = صيغة الوقت والتاريخ prefs_dialog.time = الوقت prefs_dialog.date = التاريخ prefs_dialog.date_separator = فاصل prefs_dialog.time_12_hour = صيغة 12 ساعة prefs_dialog.time_24_hour = صيغة 24 ساعة prefs_dialog.show_seconds = إظهار الثواني prefs_dialog.show_century = إظهار القرن prefs_dialog.check_for_updates_on_startup = التحقق من التحديثات عند بدء التشغيل prefs_dialog.show_splash_screen = إظهار شاشة البداية prefs_dialog.folders_tab = المجلدات prefs_dialog.startup_folders = مجلدات بداية التشغيل prefs_dialog.left_folder = المجلد الأيسر prefs_dialog.right_folder = المجلد الأيمن prefs_dialog.last_folder = آخر المجلدات زيارة prefs_dialog.custom_folder = مجلد مخصص prefs_dialog.show_hidden_files = إظهار المجلدات المخفية prefs_dialog.show_ds_store_files = إظهار ملفات .DS_Store prefs_dialog.show_system_folders = إظهار مجلدات النظام prefs_dialog.compact_file_size = كتابة الصيغةالأقرب في حجم الملف (بدون فواصل) prefs_dialog.follow_symlinks_when_cd = اتبع الوصلات الرمزية عند تغيير المجلد prefs_dialog.appearance_tab = المظهر prefs_dialog.look_and_feel = المظهر والإحساس prefs_dialog.icons_size = حجم الأيقونات prefs_dialog.toolbar_icons = شريط الأدوات prefs_dialog.command_bar_icons = شريط الأوامر prefs_dialog.file_icons = أنواع الملفات prefs_dialog.use_system_file_icons = استخدام أيقونات النظام للملفات prefs_dialog.use_system_file_icons.always = دائماً prefs_dialog.use_system_file_icons.never = أبداً prefs_dialog.use_system_file_icons.applications = فقط للتطبيقات prefs_dialog.edit_current_theme = تعديل الثمة الحالية... prefs_dialog.themes = ثمات prefs_dialog.import_theme = إضافة ثمة prefs_dialog.import_look_and_feel = إضافة شكل وإحساس prefs_dialog.no_look_and_feel = لا يوجد شكل وإحساس prefs_dialog.error_in_import = خطأ عند إضافة الثمة %1. prefs_dialog.cannot_read_theme = لا يمكن تحميل الثمة من الملف %1 prefs_dialog.export_theme = تصدير %1 prefs_dialog.import = استيراد prefs_dialog.export = تصدير prefs_dialog.theme_type = نوع %1 prefs_dialog.delete_theme = حذف الثمة %1 بشكل نهائي؟ prefs_dialog.delete_look_and_feel = حذف المظهر والإحساس %1 بشكل نهائي؟ prefs_dialog.rename_failed = فشل في إعادة تسمية الثمة %1 prefs_dialog.xml_file = ملف XML prefs_dialog.jar_file = ملف JAR prefs_dialog.mail_tab = بريد prefs_dialog.mail_settings = إعدادات البريد الصادر prefs_dialog.mail_name = اسمك prefs_dialog.mail_address = بريدك prefs_dialog.mail_server = خادم SMTP prefs_dialog.misc_tab = منوعات prefs_dialog.use_brushed_metal = استعمال مظهر 'المعدن الممسوح' (يحتاج إلى إعادة تشغيل) prefs_dialog.confirm_on_quit = إظهار نافذة التحقق عند الإنهاء prefs_dialog.default_shell = استخدام صدفة النظام الافتراضية prefs_dialog.custom_shell = استخدام صدفة مخصصة prefs_dialog.shell_encoding = ترميز الصدفة prefs_dialog.auto_detect_shell_encoding = اكتشاف تلقائي prefs_dialog.enable_bonjour_discovery = تفعيل الكشف عن خدمات Bonjour prefs_dialog.enable_system_notifications = تفعيل تنبيهات النظام debug_console_dialog.level = مرحلة unit.byte = بايت unit.bytes = بايتات unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/ثا duration.seconds = %1ثا duration.minutes = %1د duration.hours = %1سا duration.days = %1يوم duration.months = %1شهر duration.years = %1سنة theme.custom_theme = ثمة مخصصة theme.custom = مخصصة theme.built_in = مضمنة theme.add_on = إضافة theme.current = الحالية theme_could_not_be_loaded = حدث خطأ أثناء تحميل هذه الثمة setup.title = مرحباً إلى trolCommander setup.intro = رجاءً اختر سلوك trolCommander الذي تفضل. setup.look_and_feel = اختر المظهر والإحساس الخاص بك setup.theme = اختر ثمتك font_chooser.font_size = الحجم font_chooser.font_bold = عريض font_chooser.font_italic = مائل color_chooser.red = أحمر color_chooser.green = أخضر color_chooser.blue = أزرق color_chooser.hue = تدرج اللون color_chooser.brightness = السطوع color_chooser.swatches = النماذج color_chooser.saturation = التشبع color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = الأخير color_chooser.alpha = الشفافية color_chooser.title = اختيار لون batch_rename_dialog.mask = نمط إعادة التسمية batch_rename_dialog.search_replace = بحث واستبدال batch_rename_dialog.search_for = بحث عن batch_rename_dialog.replace_with = استبدال بـ batch_rename_dialog.counter = عداد batch_rename_dialog.start_at = يبدأ بـ batch_rename_dialog.step_by = خطوة بـ batch_rename_dialog.format = الصيغة batch_rename_dialog.upper_lower_case = حروف كبيرة/صغيرة batch_rename_dialog.no_change = غير متغير batch_rename_dialog.lower_case = حروف صغيرة batch_rename_dialog.upper_case = حروف كبيرة batch_rename_dialog.first_upper = أول حرف كبير batch_rename_dialog.word = أول حرف من كل كلمة batch_rename_dialog.old_name = الاسم القديم batch_rename_dialog.new_name = الاسم الجديد batch_rename_dialog.block_name = حفظ batch_rename_dialog.range = مدى batch_rename_dialog.proceed_renaming = %1 ملفات من %2 سيتم إعادة تسميتها. هل تريد التنفيذ؟ batch_rename_dialog.duplicate_names = أسماء متكررة! parent_folders_quick_list.empty_message = الموقع الحالي ليس لديه مجلد أب recent_locations_quick_list.empty_message = لا يوجد مواقع أخيرة recent_executed_files_quick_list.empty_message = لا يوجد ملفات مشغلة أخيراً #server_connect_dialog.auth_error = معرف خاطئ أو كلمة مرور خاطئة. #move_dialog.cannot_move_to_itself = لا يمكن نقل الملفات إلى المجلد الفرعي. #pack.error_on_file = خطأ أثناء الضغط %1 #pack_dialog.cannot_write = لا يمكن إنشاء الملف في المجلد الوجهة. #unpack.unable_to_open_zip = يمكن فتح الملف المضغوط %1. #image_viewer.previous_image = الصورة السابقة #image_viewer.next_image = الصورة التالية #mkdir_dialog.title = إنشاء دليل #mkdir_dialog.error_title = خطأ إنشاء #edit_bookmarks_dialog.remove = حذف #mkdir_dialog.description = إنشاء دليل #mkfile_dialog.description = إنشاء ملف جديد فارغ #done = Done:تم #progress_dialog.hide = إخفاء #move_dialog.rename_description = إعادة التسمية إلى #theme_editor.shell_font = خط الصدفة #theme_editor.history_font = محفوظات الخط #theme_editor.shell_colors = ألوان الصدفة #theme_editor.history_colors = ألوان المحفوظات #ToggleHiddenFiles.hide = لا تظهر الملفات المخفية #auth_dialog.error_was = الخطأ كان: %1 #table.hide_column = إخفاء عمود #delete.symlink_warning_title = تم العثور على وصلة رمزية #delete.symlink_warning = هذا الملف يبدو وكأنه وصلة رمزية:\n\n الملف: %1\n مرتبط بـ: %2\n\nحذف الوصلة فقط أم\nتتبع الوصلة وحذف المجلد (تحذير)؟ #delete.delete_link_only = حذف الوصلة #delete.delete_linked_folder = حذف المجلد #Unpack.label = استخراج الملفات #Pack.label = أرشفة الملفات ================================================ FILE: src/main/resources/dictionary_be_BY.properties ================================================ ok = OK yes = Так no = Не cancel = Скасаваць edit = Рэдагаваць close = Зачыніць reset = Скінуць rename = Зьмяніць назву apply = Ужыць change = Зьмяніць save = Захаваць dont_save = Не захоўваць replace = Замяніць dont_replace = Не замяняць delete = Выдаліць skip = Прапусьціць skip_all = Прапусьціць усё retry = Паўтарыць resume = Працягнуць overwrite = Замяніць overwrite_if_older = Толькі старыя duplicate = Падвоіць apply_to_all = Ужыць да ўсяго copy = Капіяваць move = Перанесьці pack = Запакаваць unpack = Распакаваць download = Спампаваць split = Разьбіць combine = Злучыць browse = Агляд ask = Запытацца stop = Спыніць pause = Прыпыніць quick_search = Хуткі пошук file_manager = Мэнэджэр файлаў create = Стварыць creating_file = Ствараецца %1 choose = Абярыце customize = Наладзіць choose_folder = Абярыце тэчку login = Лагін password = Пароль user = Карыстальнік encoding = Кадыроўка preferred_encodings = Абраныя кадыроўкі license = Ліцэнзія name = Імя size = Памер date = Дата extension = Пашырэньне permissions = Прывілеі owner = Уладальнік group = Група location = Разьмяшчэньне untitled = Без імя source = Крыніца destination = Атрымальнік recurse_directories = Апрацоўваць падтэчкі go_to = Перайсьці да example = Прыклад preview = Папярэдні прагляд comment = Камэнтары sample_text = Прыклад тэксту nb_files = %1 файл(аў) nb_folders = %1 тэчка(і) loading = Загрузка... this_operation_cannot_be_undone = Немагчыма скасаваць дадзеную апэрацыю remove = Выдаліць details = Падрабязьней warning = Папярэджаньне error = Памылка generic_error = Пры выкананьні апэрацыі ўзьнікла памылка folder_does_not_exist = Тэчка з такім імём не існуе альбо недасягальная this_folder_does_not_exist = Тэчка %1 не існуе альбо недасягальная this_file_does_not_exist = Файл %1 не існуе альбо недасягальны invalid_path = Няслушны шлях: %1 directory_already_exists = Тэчка %1 ужо існуе. file_exists_in_destination = Файл ужо існуе source_parent_of_destination = Спроба скапіяваць тэчку ў яе ўласную падтэчку cannot_read_file = Не атрымліваецца прачытаць файл %1 cannot_write_file = Не атрымліваецца запісаць файл %1 cannot_create_folder = Не атрымліваецца стварыць тэчку %1. cannot_read_folder = Не атрымліваецца прачытаць зьмест тэчкі %1 cannot_delete_file = Не атрымліваецца выдаліць файл %1 cannot_delete_folder = Не атрымліваецца стварыць тэчку %1 error_while_transferring = Памылка пры перадачы файлу %1 same_source_destination = Зыходная і канцавая тэчка - тая самая file_already_exists = Файл %1 ужо існуе. Замяніць? write_error = Памылка запісу read_error = Памылка чытаньня integrity_check_error = Ня пройдзена праверка цэласнасьці: файл-крыніца і файл-прымальнік не супадаюць startup_error = Пры запуску trolCommander'у ўзьнікла памылка. permissions.read = Чытаньне permissions.write = Запіс permissions.executable = Выкананьне permissions.group = Група permissions.other = Іншае permissions.octal_notation = Лікавае значэньне action_categories.all = Усе action_categories.navigation = Навігацыя action_categories.selection = Выбар action_categories.view = Прагляд action_categories.file_operations = Файлавыя апэрацыі action_categories.windows = Вакенцы Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Дадаць закладку AddBookmark.tooltip = Дадаць бягучую тэчку ў закладкі BatchRename.label = Групавая зьмена назвы EditBookmarks.label = Рэдагаваць закладкі ExploreBookmarks.label = Праглядзець закладкі EditCredentials.label = Рэдагаваць рэквізыты ChangeLocation.label = Зьмяніць месцазнаходжаньне ChangeDate.label = Зьмяніць дату ChangeDate.tooltip = Зьмяніць дату абраных файлаў ChangePermissions.label = Зьмяніць прывілеі ChangePermissions.tooltip = Зьмяніць прывілеі абраных файлаў CheckForUpdates.label = Праверыць абнаўленьні CompareFolders.label = Параўнаць тэчкі ConnectToServer.label = Злучыцца з сэрвэрам ConnectToServer.tooltip = Злучыцца з дыстанцыянаваным сэрвэрам View.label = Праглядзець InternalView.label = Прагляд (убудаваным) View.tooltip = Прагляд абранага файлу InternalEdit.label = Рэдагаваць (убудаваным) Edit.tooltip = Рэдагаваць абраны файл Copy.tooltip = Капіяваць вылучаныя файлы LocalCopy.label = Капіяваць лакальна LocalCopy.tooltip = Капіяваць абраны файл у дадзеную тэчку Move.tooltip = Перанесьці вылучаныя файлы Rename.tooltip = Зьмяніць назву абранага файлу Mkdir.label = Стварыць тэчку Mkdir.tooltip = Стварыць падтэчку ў дадзенай тэчцы Mkfile.label = Стварыць файл Mkfile.tooltip = Стварыць файл у дадзенай тэчцы Delete.tooltip = Выдаліць вылучаныя файлы ў Сьметніцу, калі гэта магчыма PermanentDelete.label = Выдаліць адразу PermanentDelete.tooltip = Выдаліць вылучаныя файлы назаўсёды не выкарыстоўваючы Сьметніцу Refresh.label = Абнавіць Refresh.tooltip = Абнаўленьне сьпісу файлаў у дадзенай тэчцы CloseWindow.label = Зачыніць CloseWindow.tooltip = Зачыніць вакенца CopyFileNames.label = Капіяваць імёны CopyFilePaths.label = Капіяваць шляхі CopyFilesToClipboard.label = Капіяваць файл(ы) PasteClipboardFiles.label = Уставіць файл(ы) Email.label = Даслаць файл(ы) па email Email.tooltip = Даслаць вылучаныя файлы як дадаткі па email GoBack.label = Назад GoBack.tooltip = Перайсьці да папярэдняй тэчкі GoForward.label = Наперад GoForward.tooltip = Перайсьці да наступнай тэчкі GoToHome.label = Перайсьці ў хатнюю тэчку GoToParent.label = Перайсьці на ўзровень вышэй GoToParent.tooltip = Перайсьці ў бацькоўскую тэчку GoToParentInOtherPanel.label = Перайсьці на ўзровень вышэй у іншай панэлі GoToParentInBothPanels.label = Перайсьці на ўзровень вышэй у абедзьвюх панэлях GoToRoot.label = Перайсьці ў корань SortByName.label = Сартаваць па імю SortByDate.label = Сартаваць па даце SortBySize.label = Сартаваць па памеру SortByExtension.label = Сартаваць па пашырэньню SortByPermissions.label = Сартаваць па прывілеях SortByOwner.label = Сартаваць па ўладальніку SortByGroup.label = Сартаваць па групе MarkGroup.label = Вылучыць файлы MarkGroup.tooltip = Вылучыць групу файлаў UnmarkGroup.label = Зьняць вылучэньне з файлаў UnmarkGroup.tooltip = Скасаваць вылучэньне групы файлаў MarkAll.label = Вылучыць усе UnmarkAll.label = Зьняць вылучэньне са ўсяго MarkSelectedFile.label = Вылучыць/зьняць MarkSelectedFile.tooltip = Вылучыць/зьняць вылучэньне абранага файлу MarkNextBlock.label = Вылучыць блок радкоў уніз MarkPreviousBlock.label = Вылучыць блок радкоў уверх MarkNextRow.label = Парадковае вылучэньне ўніз MarkPreviousRow.label = Парадковае вылучэньне ўверх MarkNextPage.label = Вылучыць файлы на адну старонку ўніз MarkPreviousPage.label = Вылучыць файлы на адну старонку ўверх MarkToFirstRow.label = Вылучыць файлы да пачатку MarkToLastRow.label = Вылучыць файлы да канца MarkExtension.label = Скасаваць вылучэньне InvertSelection.label = Інвэртаваць вылучэньне SwapFolders.label = Памяняць месцамі тэчкі SwapFolders.tooltip = Памяняць месцамі панэлі SetSameFolder.label = Зрабіць тэчкі аднолькавымі SetSameFolder.tooltip = Аднолькавыя тэчкі ў абедзьвюх панэлях NewWindow.label = Новае вакенца NewWindow.tooltip = Адчыніць новае вакенца Open.label = Адчыніць Open.tooltip = Увайсьці ў тэчку / Увайсьці ў архіў / Выканаць OpenNatively.label = Адчыніць у прывязанай праграме OpenNatively.tooltip = Выканаць абраны файл з дапамогай праграмы, якая зьвязана з ім у наладках сыстэмы OpenInOtherPanel.label = Адчыніць у іншай панэлі OpenInBothPanels.label = Адчыніць у абедзьвюх панэлях RevealInDesktop.label = Адлюстраваць у %1 RunCommand.label = Выканаць каманду RunCommand.tooltip = Выканаць каманду з дадзенай тэчкі Pack.tooltip = Запакаваць вылучаныя файлы ў архіў Unpack.tooltip = Распакаваць вылучаныя архіўныя файлы ShowFileProperties.label = Уласьцівасьці ShowFileProperties.tooltip = Паказаць уласьцівасьці вылучаных файлаў ShowPreferences.label = Наладкі ShowPreferences.tooltip = Наладзіць trolCommander ShowServerConnections.label = Адлюстраваць усталяваныя злучэньні Quit.label = Выхад ReverseSortOrder.label = У адваротнай пасьлядоўнасьці ToggleAutoSize.label = Аўтападбор памеру слупкоў Stop.label = Спыніць зьмяненьне тэчкі ToggleColumn.show = Паказаць слупок %1 ToggleColumn.hide = Прыбраць слупок %1 ToggleCommandBar.show = Паказаць панэль каманд ToggleCommandBar.hide = Схаваць панэль каманд ToggleToolBar.show = Паказаць панэль прылад ToggleToolBar.hide = Схаваць панэль прылад CustomizeCommandBar.label = Наладка панэлі каманд ToggleStatusBar.show = Паказаць панэль статусу ToggleStatusBar.hide = Схаваць панэль статусу ToggleShowFoldersFirst.label = Вынесьці тэчкі ў пачатак сьпісу ToggleTree.label = Паказаць структуру ў выглядзе дрэва PopupLeftDriveButton.label = Зьмяніць левую тэчку PopupRightDriveButton.label = Зьмяніць правую тэчку RecallPreviousWindow.label = Перайсьці ў папярэдняе вакенца RecallNextWindow.label = Перайсьці ў наступнае вакенца RecallWindow.label = Узнавіць вакенца #%1 BringAllToFront.label = Паказаць усе вакенцы SwitchActiveTable.label = Пераключэньне паміж левай і правай панэлямі SelectNextBlock.label = Спусьціцца на блок радкоў уніз SelectPreviousBlock.label = Падняцца на блок радкоў уверх SelectNextPage.label = Перайсьці ў канец старонкі SelectPreviousPage.label = Перайсьці ў пачатак старонкі SelectNextRow.label = Спусьціцца на адзін радок уніз SelectPreviousRow.label = Падняцца на адзін радок уверх SelectFirstRow.label = Перайсьці да першага файлу ў тэчцы SelectLastRow.label = Перайсьці да апошняга файлу ў тэчцы SplitEqually.label = Паздзяліць на роўныя часткі SplitVertically.label = Падзяліць вэртыкальна SplitHorizontally.label = Падзяліць гарызантальна ShowKeyboardShortcuts.label = Хуткія клявішы GoToWebsite.label = Перайсьці на хатнюю старонку праграмы GoToForums.label = Перайсьці на форум ReportBug.label = Паведаміць пра памылку Donate.label = Ахвяраваць на развіцьцё праграмы ShowAbout.label = Пра праграму trolCommander OpenTrash.label = Адчыніць Сьметніцу EmptyTrash.label = Ачысьціць Сьметніцу CalculateChecksum.label = Разьлік кантрольнай сумы MaximizeWindow.label = Разгарнуць MaximizeWindow.label.mac_os_x = Павялічыць MinimizeWindow.label = Згарнуць MinimizeWindow.label.mac_os_x = У Док GoToDocumentation.label = Докумэнтацыя ў сеціве ShowParentFoldersQL.label = Бацькоўскія тэчкі ShowRecentLocationsQL.label = Апошнія наведаныя тэчкі ShowRecentExecutedFilesQL.label = Апошнія выкарыстаныя файлы SplitFile.tooltip = Падзяліць файл на некалькі частак CombineFiles.tooltip = Сабраць падзелены на часткі файл у першапачатковы файл ShowDebugConsole.label = Кансоль Debug FocusPrevious.label = Перайсьці да наступнага кампанэнту FocusNext.label = Перайсьці да наступнага кампанэнту file_menu = Файл file_menu.open_with = Адчыніць з дапамогай mark_menu = Вылучэньне view_menu = Прагляд view_menu.show_hide_columns = Паказаць/схаваць слупкі go_menu = Перайсьці bookmarks_menu = Закладкі bookmarks_menu.no_bookmark = Няма закладак drive_popup.network_shares = Абагульнены сеткавы рэсурс quick_lists_menu = Хуткі сьпіс window_menu = Вакенцы help_menu = Дапамога status_bar.selected_files = %1 з %2 файлаў абрана status_bar.connecting_to_folder = Падлучаемся да тэчкі, націсьніце ESC каб скасаваць status_bar.volume_free = Вольна: %1 status_bar.volume_capacity = Ёмістасьць дыску: %1 shortcuts_panel.title = Камбінацыі клявіш shortcuts_panel.restore_defaults = Вярнуць стандартныя shortcuts_panel.show = Адлюстраваць shortcuts_panel.default_message = Націснуўшы Enter альбо зрабіўшы падвойны клік вы зможаце рэдагаваць камбінацыі клявіш shortcuts_table.action_description = Апісаньне дзеяньня shortcuts_table.shortcut = Камбінацыі клявіш shortcuts_table.alternate_shortcut = Альтэрнатыўныя камбінацыі shortcuts_table.type_in_a_shortcut = Націсьніце камбінацыю клявіш command_bar_dialog.help = Перацягвайце кнопкі для наладкі панэлі каманд table.folder_access_error_title = Памылка: няма доступу ў тэчку table.folder_access_error = Не атрымліваецца прачытаць зьмест тэчкі table.download_or_browse = Вы жадаеце праглядзець альбо спампаваць дадзены файл? version_dialog.no_new_version_title = Няма новай вэрсіі version_dialog.no_new_version = Вы карыстаецесь самай апошняй вэрсіяй version_dialog.new_version_title = Даступна новая вэрсія version_dialog.new_version = Даступна новая вэрсія trolCommander. version_dialog.new_version_url = Новая вэрсія trolCommander даступна па адрэсе %1. version_dialog.not_available_title = Сэрвэр недаступны version_dialog.not_available = Не атрымліваецца атрымаць інфармацыю пра новыя вэрсіі з сэрвэру. version_dialog.install_and_restart = Усталяваць і перазапусьціць version_dialog.preparing_for_update = Ідзе падрыхтоўка да абнаўленьня... quit_dialog.title = Выхад з trolCommander quit_dialog.desc = Зачыніць %1 вакенцаў і выйсьці з trolCommander ? quit_dialog.show_next_time = Паказаць у наступны раз destination_dialog.file_exists_action = Дзеяньне па змоўчаньні калі файл ужо існуе destination_dialog.verify_integrity = Зрабіць праверку цэласнасьці дадзеных destination_dialog.skip_errors = Прапусьціць памылкі file_collision_dialog.title = Супадзеньне файлаў rename_dialog.new_name = Новае імя copy_dialog.destination = Капіяваць абраныя файлы ў copy_dialog.error_title = Памылка пры капіяванні copy_dialog.copying = Капіяванне файлаў copy_dialog.copying_file = Капірую %1 pack_dialog.packing = Сьціскаюцца файлы pack_dialog.packing_file = Сьціскаецца файл %1 pack_dialog.error_title = Памылка архівацыі pack_dialog_description = Дадаць абраныя файлы ў pack_dialog.archive_format = Фармат архіву unpack_dialog.destination = Распакаваць абраныя файлы ў unpack_dialog.error_title = Памылка пры распакаваньні unpack_dialog.unpacking = Распакаваньне файлаў unpack_dialog.unpacking_file = Распакоўваецца файл %1 optimizing_archive = Аптымізацыя архіву %1 error_while_optimizing_archive = Памылка пры аптымізацыі архіву %1 move_dialog.move_description = Перанесьці ў move_dialog.error_title = Памылка пры пераносе move_dialog.moving = Перамяшчэньне файлаў move_dialog.moving_file = Перамяшчаецца файл %1 download_dialog.description = Запампаваць файл у download_dialog.error_title = Памылка пры запампоўцы download_dialog.downloading = Запампоўка download_dialog.downloading_file = Запампоўваецца файл %1 mkfile_dialog.allocate_space = Вылучыць прастору delete_dialog.permanently_delete.confirmation = Выдаліць абраныя файлы без магчымасьці ўзнаўленьня? delete_dialog.move_to_trash.confirmation = Выдаліць абраныя файл(ы)? delete_dialog.move_to_trash.confirmation_details = Файлы будуць перанесены ў Сьметніцу delete_dialog.move_to_trash.option = Перанесьці ў Сьметніцу delete_dialog.move_to_trash.failed = Адзін альбо некалькі файлаў ня могуць быць перанесены ў Сьметніцу. delete_dialog.deleting = Выдаленьне файлаў delete_dialog.error_title = Памылка пры выдаленьні файлу delete.deleting_file = Выдаляецца файл %1 email_dialog.prefs_not_set_title = Пошта не наладжана email_dialog.prefs_not_set = Вам неабходна наладзіць параметры пошты перад тым як нешта даслаць. email_dialog.from = Ад каго email_dialog.to = Каму email_dialog.subject = Тэма email_dialog.send = Даслаць email_dialog.error_title = Памылка пры дасыланьні файлаў email_dialog.read_error = Немагчыма прачытаць файлы ў падтэчках. email_dialog.sending = Дасыланьне файлаў email.sending_file = Дасылаецца %1 email.connecting_to_server = Злучэньне з %1 email.server_unavailable = Не атрымалася злучыцца з паштовым сэрвэрам %1, праверце наладкі пошты і паспрабуйце яшчэ раз. email.connection_closed = Злучэньне зачынена сэрвэрам, пошта не была даслана. email.goodbye_failed = Памылка пры завяршэньні злучэньня, магчыма, пошта не была даслана. email.send_file_error = Немагчыма даслаць файл %1, пошта не была даслана. split_file_dialog.error_title = Памылка разбіцьця файлу split_file_dialog.file_to_split = Файл для разбіцьця split_file_dialog.target_directory = Выніковая тэчка split_file_dialog.part_size = Памер кожнай часткі split_file_dialog.parts = Колькасьць частак split_file_dialog.generate_CRC = Сфармаваць CRC файл split_file_dialog.max_parts = Максымальная колькасьць частак %1 split_file_dialog.auto = Аўтаматычна split_file_dialog.insert_new_media = Устаўце новы носьбіт combine_files_dialog.error_title = Памылка аб'яднаньня файлу combine_files_job.no_crc_file = Аб'яднаньне прайшло пасьпяхова. Няма CRC файлу. combine_files_job.crc_read_error = Памылка чытаньня CRC файлу. combine_files_job.crc_check_failed = CRC разыходжаньні: чакаецца %2, атрымана %1 combine_files_job.crc_ok = Аб'яднаньне прайшло пасьпяхова. Праверка кантрольнай сумы CRC прайшла пасьпяхова. file_selection_dialog.mark = Вылучэньне file_selection_dialog.unmark = Скасаваньне вылучэньня file_selection_dialog.mark_description = Вылучыць файлы па масцы file_selection_dialog.unmark_description = Скасаваць вылучэньне па масцы file_selection_dialog.case_sensitive = З улікам рэгістру file_selection_dialog.include_folders = Уключаючы тэчкі progress_dialog.starting = Перадача файлаў пачалася... progress_dialog.transferred = Перададзена %1, у сярэднем %2 progress_dialog.elapsed_time = Спатрэбілася часу progress_dialog.advanced = Дадаткова progress_dialog.current_speed = Хуткасьць progress_dialog.limit_speed = Абмежаваньне хуткасьці progress_dialog.close_when_finished = Зачыніць вакенца пасля сканчэньня progress_dialog.processing_files = Апрацоўка файлаў progress_dialog.processing_file = Апрацоўваецца %1 progress_dialog.verifying_file = Ідзе праверка %1 progress_dialog.job_finished = Апрацоўка скончана progress_dialog.job_error = Памылка апрацоўкі properties_dialog.file_properties = Уласьцівасьці %1 properties_dialog.contents = Зьмест properties_dialog.calculating = Ідзе падлік... calculate_checksum_dialog.checksum_algorithm = Альгарытм падліку кантрольнай сумы calculate_checksum_dialog.temporary_file = Часовы файл change_date_dialog.now = Зараз change_date_dialog.specific_date = Указаная дата run_dialog.run_command_description = Запусьціць з гэтай тэчкі run_dialog.run_in_home_description = Выканаць у хатняй тэчцы run_dialog.command_output = Вынік выкананьня каманды run_dialog.run = Запусьціць run_dialog.clear_history = Ачысьціць буфэр каманд server_connect_dialog.server_type = Тып злучэньня server_connect_dialog.server = Сэрвэр server_connect_dialog.share = Агульны рэсурс server_connect_dialog.domain = Дамэн server_connect_dialog.username = Імя карыстальніка server_connect_dialog.initial_dir = Пачатковая тэчка server_connect_dialog.port = Порт server_connect_dialog.server_url = Адрэса сэрвэру server_connect_dialog.http_url = Адрэса інтэрнэт-старонкі server_connect_dialog.connect = Злучыцца server_connect_dialog.protocol = Пратакол server_connect_dialog.nfs_version = Вэрсія NFS server_connect_dialog.private_key = Прыватны ключ server_connect_dialog.passphrase = Ключавая фраза ftp_connect.passive_mode = Выкарыстоўваць пасыўны рэжым ftp_connect.anonymous_user = Ананімнае злучэньне ftp_connect.nb_connection_retries = Колькасьць спроб усталяваньня злучэньня ftp_connect.retry_delay = Затрымка паміж паўторамі (у сэкундах) http_connect.basic_authentication = Базавая аўтэнтыфікацыя HTTP (не абавязкова) server_connections_dialog.disconnect = Адлучыцца server_connections_dialog.connection_busy = Занята server_connections_dialog.connection_idle = Вольна bonjour.bonjour_services = Служба Bonjour bonjour.no_service_discovered = Служб не знойдзена bonjour.bonjour_disabled = Служба Bonjour адключана auth_dialog.title = Аўтэнтыфікацыя auth_dialog.desc = Увядзіце імя карыстальніка і пароль auth_dialog.server = Сэрвэр auth_dialog.store_credentials = Захаваць імя карыстальніка і пароль (са слабым шыфраваньнем) auth_dialog.connect_as = Далучыцца як auth_dialog.authentication_failed = Аўтэнтыфікацыя ня пройдзена sortable_list.move_up = Вышэй sortable_list.move_down = Ніжэй add_bookmark_dialog.add = Дадаць edit_bookmarks_dialog.new = Новая file_viewer.view_error_title = Памылка пры праглядзе file_viewer.view_error = Немагчыма праглядзець файл. file_viewer.file_menu = Файл file_viewer.close = Зачыніць file_viewer.large_file_warning = Гэты файл можа быць занадта вялікім для прагляду. file_viewer.open_anyway = Адчыніць усё-адно text_viewer.edit = Рэдагаваць text_viewer.copy = Капіяваць text_viewer.select_all = Абраць усе text_viewer.find = Пошук text_viewer.find_next = Шукаць далей text_viewer.find_previous = Шукаць папярэдні text_viewer.binary_file_warning = Гэты файл хутчэй за ўсё двойкавы image_viewer.controls_menu = Элементы кіравання image_viewer.zoom_in = Павялічыць image_viewer.zoom_out = Паменшыць file_editor.edit_error_title = Памылка пры рэдагаваньні file_editor.edit_error = Немагчыма рэдагаваць файл. file_editor.save = Захаваць file_editor.save_as = Захаваць як... file_editor.save_warning = Захаваць зробленыя зьмены перад выхадам ? file_editor.cannot_write = Не атрымліваецца запісаць файл. text_editor.cut = Выразаць text_editor.paste = Уставіць shortcuts_dialog.quick_search.start_search = Увядзіце адвольны сымбаль для пачатку хуткага пошуку shortcuts_dialog.quick_search.cancel_search = Скасаваньне хуткага пошуку shortcuts_dialog.quick_search.remove_last_char = Выдаліць апошні сымбаль з радку хуткага пошуку shortcuts_dialog.quick_search.jump_to_previous = Перайсьці да папярэдняга выніку хуткага пошуку shortcuts_dialog.quick_search.jump_to_next = Перайсьці да наступнага выніку хуткага пошуку shortcuts_dialog.quick_search.mark_jump_next = Вылучыць/скасаваць вылучэньне з файлу і перайсьці да наступнага выніку пошуку theme_editor.title = Рэдактар тэмы theme_editor.folder_tab = Панэль тэчкі theme_editor.shell_tab = Абалонка theme_editor.shell_history_tab = Гісторыя каманд theme_editor.statusbar_tab = Радок стану theme_editor.free_space = Вольная прастора theme_editor.free_space.ok = OK theme_editor.free_space.warning = Увага theme_editor.free_space.critical = Вельмі мала theme_editor.locationbar_tab = Указальнік месцазнаходжаньня theme_editor.editor_tab = Рэдактар файлаў theme_editor.font = Шрыфт theme_editor.active_panel = Актыўная theme_editor.inactive_panel = Неактыўная theme_editor.general = Агульны theme_editor.could_not_save_theme = Не атрымліваецца захаваць тэму %1 theme_editor.border = Рамка theme_editor.background = Фон theme_editor.alternate_background = Іншы фон theme_editor.unfocused_background = Фон (без фокусу) theme_editor.quick_search.unmatched_file = Файлы, што не супадаюць theme_editor.text = Тэкст theme_editor.progress = Індыкатар выкананьня theme_editor.normal = Звычайны theme_editor.normal_unfocused = Звычайны (без фокусу) theme_editor.selected = Абраны theme_editor.selected_unfocused = Абраны (без фокусу) theme_editor.color = Колер theme_editor.colors = Колеры theme_editor.plain_file = Звычайны файл theme_editor.marked_file = Вылучаны файл theme_editor.hidden_file = Схаваны файл theme_editor.folder = Тэчка theme_editor.archive_file = Архіў theme_editor.symbolic_link = Сымбальная спасылка theme_editor.header = Загаловак theme_editor.item = Элемент command_bar_customize_dialog.available_actions = Даступныя дзеяньні command_bar_customize_dialog.modifier = Пераключальнік prefs_dialog.title = Наладкі prefs_dialog.general_tab = Агульныя prefs_dialog.day = Дзень prefs_dialog.month = Месяц prefs_dialog.year = Год prefs_dialog.language = Мова (патрабуецца перазапуск) prefs_dialog.date_time = Фармат даты і часу prefs_dialog.time = Час prefs_dialog.date = Дата prefs_dialog.date_separator = Разьдзяляльнік prefs_dialog.time_12_hour = 12-гадзінавы фармат prefs_dialog.time_24_hour = 24-гадзінавы фармат prefs_dialog.show_seconds = Паказваць сэкунды prefs_dialog.show_century = Паказваць усе 4 лічбы году prefs_dialog.check_for_updates_on_startup = Правяраць наяўнасьць новых вэрсій пры запуску prefs_dialog.show_splash_screen = Паказваць застаўку prefs_dialog.folders_tab = Тэчкі prefs_dialog.startup_folders = Тэчкі пры запуску prefs_dialog.left_folder = Тэчка ў левай панэлі prefs_dialog.right_folder = Тэчка ў правай панэлі prefs_dialog.last_folder = Апошняя наведаная тэчка prefs_dialog.custom_folder = Зададзеная тэчка prefs_dialog.show_hidden_files = Паказваць схаваныя файлы prefs_dialog.show_ds_store_files = Паказваць файлы .DS_Store prefs_dialog.show_system_folders = Паказваць сыстэмныя тэчкі prefs_dialog.compact_file_size = Акругляць паказаныя памеры файлаў prefs_dialog.follow_symlinks_when_cd = Пераходзіць па спасылках пры зьмене тэчкі prefs_dialog.appearance_tab = Зьнешні выгляд prefs_dialog.look_and_feel = Наладка выгляду prefs_dialog.icons_size = Памер значак prefs_dialog.toolbar_icons = Панэль прыладаў prefs_dialog.command_bar_icons = Панэль каманд prefs_dialog.file_icons = Тыпы файлаў prefs_dialog.use_system_file_icons = Выкарыстоўваць сыстэмныя значкі файлаў prefs_dialog.use_system_file_icons.always = Заўсёды prefs_dialog.use_system_file_icons.never = Ніколі prefs_dialog.use_system_file_icons.applications = Толькі для праграм prefs_dialog.edit_current_theme = Рэдагаваць тэму аздабленьня... prefs_dialog.themes = Тэмы prefs_dialog.import_theme = Імпартаваць тэму prefs_dialog.import_look_and_feel = Імпарт наладак зьнешняга выгляду prefs_dialog.no_look_and_feel = Наладак зьнешняга выгляду ня знойдзена. prefs_dialog.error_in_import = Памылка імпарту тэмы %1. prefs_dialog.cannot_read_theme = Не атрымліваецца загрузіць тэму з файлу %1 prefs_dialog.export_theme = Экспарт %1 prefs_dialog.import = Імпарт prefs_dialog.export = Экспарт prefs_dialog.theme_type = Тып: %1 prefs_dialog.delete_theme = Выдаліць тэму %1 цалкам? prefs_dialog.delete_look_and_feel = Выдаліць наладкі %1 назаўсёды? prefs_dialog.rename_failed = Не атрымалася зьмяніць назву тэмы %1 prefs_dialog.xml_file = XML-файл prefs_dialog.jar_file = JAR-файл prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Наладкі дасыланьня пошты prefs_dialog.mail_name = Вашае імя prefs_dialog.mail_address = Ваша адрэса Email prefs_dialog.mail_server = SMTP-сэрвэр prefs_dialog.misc_tab = Дадаткова prefs_dialog.use_brushed_metal = Выкарыстоўваць тэму 'шліфаваны мэтал' (патрэбны перазапуск) prefs_dialog.confirm_on_quit = Запытваць пацьверджаньне пры выхадзе prefs_dialog.default_shell = Выкарыстоўваць сыстэмную абалонку па змоўчаньні prefs_dialog.custom_shell = Выкарыстоўваць указаную абалонку prefs_dialog.shell_encoding = Кадыроўка каманднага радку prefs_dialog.auto_detect_shell_encoding = Аўтавызначэньне prefs_dialog.enable_bonjour_discovery = Уключыць службу знаходжаньня Bonjour prefs_dialog.enable_system_notifications = Дазволіць сыстэмныя паведамленьні debug_console_dialog.level = Узровень unit.byte = байт unit.bytes = байтаў unit.bytes_short = б unit.kb = Кб unit.mb = Мб unit.gb = Гб unit.tb = Тб unit.speed = %1/сэк duration.seconds = %1с duration.minutes = %1хв duration.hours = %1г duration.days = %1д duration.months = %1мес duration.years = %1год theme.custom_theme = Уласная тэма theme.custom = Тэма карыстальніка theme.built_in = Убудаваная theme.add_on = Дадатак theme.current = бягучая theme_could_not_be_loaded = Пры запампоўцы тэмы адбылася памылка. setup.title = Вітаем у trolCommander setup.intro = Абярыце асноўныя наладкі trolCommander. setup.look_and_feel = Абярыце інтэрфэйс карыстальніка setup.theme = Абярыце тэму аздабленьня font_chooser.font_size = Памер font_chooser.font_bold = Паўтлусты font_chooser.font_italic = Курсыў color_chooser.red = Чырвоны color_chooser.green = Зялёны color_chooser.blue = Сіні color_chooser.hue = Адценьне color_chooser.brightness = Яркасьць color_chooser.swatches = Узоры color_chooser.saturation = Насычанасьць color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Папярэдні color_chooser.alpha = Празрыстасьць color_chooser.title = Выбар колеру batch_rename_dialog.mask = Шаблон зьмяненьня назвы batch_rename_dialog.search_replace = Пошук і замена batch_rename_dialog.search_for = Шукаць batch_rename_dialog.replace_with = Замяніць на batch_rename_dialog.counter = Лічыльнік batch_rename_dialog.start_at = Пачаць з batch_rename_dialog.step_by = Крок batch_rename_dialog.format = Фармат batch_rename_dialog.upper_lower_case = Верхні/ніжні рэгістр batch_rename_dialog.no_change = Без зьмяненьняў batch_rename_dialog.lower_case = ніжні рэгістр batch_rename_dialog.upper_case = ВЕРХНІ РЭГІСТР batch_rename_dialog.first_upper = Першая літара вялікая batch_rename_dialog.word = Першы ад кожнага слова batch_rename_dialog.old_name = Старое імя batch_rename_dialog.new_name = Новае імя batch_rename_dialog.block_name = Без зьмяненьняў batch_rename_dialog.range = Інтэрвал batch_rename_dialog.proceed_renaming = Будуць зьменены назвы ў %1 файлаў з %2. Пачаць працэс? batch_rename_dialog.duplicate_names = Супадзеньне імёнаў! batch_rename_dialog.names_conflict = Канфлікт імёнаў! Некаторыя значэньні супадаюць у старых і новых імёнах. parent_folders_quick_list.empty_message = Бягучае месцазнаходжаньне ня мае бацькоўскага recent_locations_quick_list.empty_message = Не знойдзена апошніх месцазнаходжаньняў recent_executed_files_quick_list.empty_message = Не знойдзена апошніх выкарыстаных файлаў ================================================ FILE: src/main/resources/dictionary_ca_ES.properties ================================================ ok = Accepta yes = Sí no = No cancel = Cancel·la edit = Edita close = Tanca reset = Reinicia rename = Reanomena apply = Aplica change = Canvia save = Desa dont_save = No desis replace = Substitueix dont_replace = No substitueixis delete = Esborra skip = Salta skip_all = Omet-los tots retry = Reintenta resume = Continua overwrite = Sobreescriu overwrite_if_older = Sobreescriu si és més antic duplicate = Duplica apply_to_all = Aplica a tot copy = Copia move = Mou pack = Comprimeix unpack = Descomprimeix download = Descarrega split = Divideix combine = Combina browse = Navega ask = Pregunta stop = Para pause = Pausa quick_search = Cerca ràpida file_manager = Gestor de fitxers create = Crea creating_file = Creant %1 choose = Selecciona customize = Personalitza choose_folder = Selecciona un directori login = Identificació password = Contrasenya user = Usuari encoding = Codificació preferred_encodings = Codificacions preferides license = Llicència name = Nom size = Mida date = Data extension = Extensió permissions = Permisos owner = Propietari group = Grup location = Localització untitled = Sense nom source = Origen destination = Destí recurse_directories = Processar els directoris seleccionats recursivament go_to = Ves a example = Exemple preview = Previsualitza comment = Comenta sample_text = Text d'exemple nb_files = %1 fitxer(s) nb_folders = %1 directori(s) loading = Carregant... this_operation_cannot_be_undone = Aquesta operació no es pot desfer remove = Esborrar details = Detalls warning = Atenció error = Error generic_error = S'ha produit un error mentre es realitzava l'operació sol·licitada folder_does_not_exist = El directori no existeix o no està disponible this_folder_does_not_exist = Aquest directori no existeix o no està disponible this_file_does_not_exist = Aquest fitxer no existeix o no està disponible invalid_path = Ruta incorrecta directory_already_exists = El directori %1 ja existeix file_exists_in_destination = El fitxer ja existeix al destí source_parent_of_destination = Intent de transferir un directori a un dels seus subdirectoris cannot_read_file = No es pot llegir el fitxer %1 cannot_write_file = No es pot escriure al fitxer %1 cannot_create_folder = No es pot crear el directori %1 cannot_read_folder = No es poden llegir els continguts del directori %1 cannot_delete_file = No es pot esborrar el fitxer %1 cannot_delete_folder = No es pot esborrar el directori %1 error_while_transferring = Error transferint el fitxer %1 same_source_destination = Els directoris d'origen i destí són els mateixos file_already_exists = %1 ja existeix, el vol substituir? write_error = Error d'escriptura read_error = Error de lectura integrity_check_error = Comprovació d'integritat fallada startup_error = Un error ha impedit iniciar l'trolCommander permissions.read = Lectura permissions.write = Escriptura permissions.executable = Executable permissions.group = Grup permissions.other = Altre permissions.octal_notation = Notació octal action_categories.all = Tots action_categories.navigation = Navegació action_categories.selection = Selecció action_categories.view = Visualitza action_categories.file_operations = Operacions amb fitxers action_categories.windows = Finestres Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Afegir preferit AddBookmark.tooltip = Afegir el directori actual als preferits BatchRename.label = Reanomena en bloc EditBookmarks.label = Edita els preferits ExploreBookmarks.label = Explora els preferits EditCredentials.label = Edita les credencials ChangeLocation.label = Canvia la localització actual ChangeDate.label = Canvia la data ChangeDate.tooltip = Canvia la data del(s) fitxer(s) seleccionat(s) ChangePermissions.label = Canvia els permisos ChangePermissions.tooltip = Canvia els permisos del(s) fitxer(s) seleccionat(s) CheckForUpdates.label = Comprova les actualitzacions CompareFolders.label = Compara directoris ConnectToServer.label = Connecta't a un servidor ConnectToServer.tooltip = Connecta't a un servidor remot View.label = Visualitza InternalView.label = Visualitza (intern) View.tooltip = Visualitza el fitxer seleccionat InternalEdit.label = Edita (intern) Edit.tooltip = Edita el fitxer seleccionat Copy.tooltip = Copia els fitxers seleccionats LocalCopy.label = Còpia local LocalCopy.tooltip = Copia el fitxer seleccionat al directori actual Move.tooltip = Mou els fitxers seleccionats Rename.tooltip = Reanomena els fitxers seleccionats Mkdir.label = Crea un directori Mkdir.tooltip = Crea un subdirectori al subdirectori actual Mkfile.label = Crea un fitxer Mkfile.tooltip = Crea un fitxer al directori actual Delete.tooltip = Esborra els fitxers seleccionats PermanentDelete.label = Esborra permanentment PermanentDelete.tooltip = Esborra els fitxers seleccionats sense utilitzar la paperera de reciclatge Refresh.label = Actualitza Refresh.tooltip = Actualitza el directori actual CloseWindow.label = Tanca la finestra CloseWindow.tooltip = Tanca aquesta finestra CopyFileNames.label = Copia el(s) nom(s) CopyFilePaths.label = Copia la(es) ruta(es) CopyFilesToClipboard.label = Copia el(s) fitxer(s) PasteClipboardFiles.label = Enganxa el(s) fitxer(s) Email.label = Envia per correu electrònic Email.tooltip = Envia els fitxers seleccionats en un correu electrònic GoBack.label = Ves enrere GoBack.tooltip = Ves al directori anterior GoForward.label = Ves endavant GoForward.tooltip = Ves al directori següent GoToHome.label = Ves al directori d'inici GoToParent.label = Ves al directori pare GoToParent.tooltip = Ves al directori pare GoToParentInOtherPanel.label = Ves al directori pare en un altre tauler GoToParentInBothPanels.label = Ves al directori pare a ambdós taulers GoToRoot.label = Ves a l'arrel SortByName.label = Ordena per nom SortByDate.label = Ordena per data SortBySize.label = Ordena per mida SortByExtension.label = Ordena per extensió SortByPermissions.label = Ordena per permisos SortByOwner.label = Ordena per propietari SortByGroup.label = Ordena per grup MarkGroup.label = Selecciona fitxers MarkGroup.tooltip = Selecciona un grup de fitxers UnmarkGroup.label = Deselecciona fitxers UnmarkGroup.tooltip = Deselecciona un gruup de fitxers MarkAll.label = Selecciona'ls tots UnmarkAll.label = Deselecciona'ls tots MarkSelectedFile.label = Selecciona/Deselecciona MarkSelectedFile.tooltip = Selecciona/Deselecciona el fitxer actual MarkNextBlock.label = Selecciona un bloc MarkPreviousBlock.label = Selecciona un bloc MarkNextRow.label = Selecciona una fila cap avall MarkPreviousRow.label = Selecciona una fila cap amunt MarkNextPage.label = Selecciona una pàgina amunt MarkPreviousPage.label = Selecciona una pàgina avall MarkToFirstRow.label = Selecciona els fitxers des del començament MarkToLastRow.label = Selecciona els fitxers fins al final MarkExtension.label = Selecciona les extensions InvertSelection.label = Inverteix la selecció SwapFolders.label = Intercanvia els directoris SwapFolders.tooltip = Intercanvia els directoris de l'esquerra i de la dreta SetSameFolder.label = Mostra el mateix directori SetSameFolder.tooltip = Mostra el mateix directori als taulers de l'esquerra i de la dreta NewWindow.label = Nova finestra NewWindow.tooltip = Obre una nova finestra Open.label = Obre Open.tooltip = Entra al directori / Entra al fitxer / Executa OpenNatively.label = Obre nativament OpenNatively.tooltip = Executa el fitxer seleccionat amb el programa associat al sistema OpenInOtherPanel.label = Obre en a l'altre tauler OpenInBothPanels.label = Obre a ambdós taulers RevealInDesktop.label = Mostra a %1 RunCommand.label = Executa una comanda RunCommand.tooltip = Executa una comanda en el directori actual Pack.tooltip = Comprimeix els fitxers seleccionats en un fitxer Unpack.tooltip = Descomprimeix el fitxer seleccionat ShowFileProperties.label = Propietats ShowFileProperties.tooltip = Mostra les propietats dels fitxers seleccionats ShowPreferences.label = Preferències ShowPreferences.tooltip = Configura trolCommander ShowServerConnections.label = Mostra les connexions obertes Quit.label = Surt ReverseSortOrder.label = Inverteix l'ordre ToggleAutoSize.label = Autodimensiona les columnes Stop.label = Atura el canvi de directori ToggleColumn.show = Mostra la columna %1 ToggleColumn.hide = Amaga la columna %1 ToggleCommandBar.show = Mostra la barra de comandes ToggleCommandBar.hide = Amaga la barra de comandes ToggleToolBar.show = Mostra la barra d'eines ToggleToolBar.hide = Amaga la barra d'eines CustomizeCommandBar.label = Personalitza la barra de comandes ToggleStatusBar.show = Mostra la barra d'estat ToggleStatusBar.hide = Amaga la barra d'estat ToggleShowFoldersFirst.label = Mostra els directoris primer ToggleTree.label = Mostra l'arbre PopupLeftDriveButton.label = Canvia el directori esquerre PopupRightDriveButton.label = Canvia el directori dret RecallPreviousWindow.label = Ves a la finestra anterior RecallNextWindow.label = Ves a la finestra següent RecallWindow.label = Ves a la finestra %1 BringAllToFront.label = Mostra-ho tot SwitchActiveTable.label = Intercanvia els taulers esquerre i dret SelectNextBlock.label = Salta un bloc avall SelectPreviousBlock.label = Salta un bloc amunt SelectNextPage.label = Salta una pàgina avall SelectPreviousPage.label = Salta una pàgina amunt SelectNextRow.label = Salta una fila avall SelectPreviousRow.label = Salta una fila amunt SelectFirstRow.label = Selecciona el primer fitxer del directori actual SelectLastRow.label = Selecciona l'últim fitxer del directori actual SplitEqually.label = Divideix equitativament SplitVertically.label = Divideix verticalment SplitHorizontally.label = Divideix horitzontalment ShowKeyboardShortcuts.label = Dreceres de teclat GoToWebsite.label = Ves al lloc web GoToForums.label = Ves als fòrums ReportBug.label = Informa d'un error Donate.label = Fes una donació ShowAbout.label = Sobre trolCommander OpenTrash.label = Obre la paperera EmptyTrash.label = Buida la paperera CalculateChecksum.label = Calcula el checksum MaximizeWindow.label = Maximitza MaximizeWindow.label.mac_os_x = Zoom MinimizeWindow.label = Minimitza MinimizeWindow.label.mac_os_x = GoToDocumentation.label = Documentació en línia ShowParentFoldersQL.label = Directoris pare ShowRecentLocationsQL.label = Localitzacions recents ShowRecentExecutedFilesQL.label = Fitxers executats recentment SplitFile.tooltip = Divideix un fitxer en múltiples parts CombineFiles.tooltip = Torna a crear un fitxer dividit en múltiples parts ShowDebugConsole.label = Consola de depuració FocusPrevious.label = Marca el component anterior FocusNext.label = Marca el component següent file_menu = Fitxer file_menu.open_with = Obre amb mark_menu = Selecciona view_menu = Visualitza view_menu.show_hide_columns = Mostra/Amaga columnes go_menu = Ves bookmarks_menu = Preferits bookmarks_menu.no_bookmark = No hi ha cap preferit drive_popup.network_shares = Xarxa compartida quick_lists_menu = Llistes ràpides window_menu = Finestra help_menu = Ajuda status_bar.selected_files = %1 de %2 seleccionats status_bar.connecting_to_folder = Connectant al directori, prem ESCAPE per a cancel·lar status_bar.volume_free = Lliure status_bar.volume_capacity = Capacitat shortcuts_panel.title = Dreceres shortcuts_panel.restore_defaults = Restaura per defecte shortcuts_panel.show = Mostra shortcuts_panel.default_message = Prem Enter o fes doble clic sobre la drecera per editar shortcuts_table.action_description = Descripció de l'acció shortcuts_table.shortcut = Drecera shortcuts_table.alternate_shortcut = Drecera alternativa shortcuts_table.type_in_a_shortcut = Escriu una drecera command_bar_dialog.help = Arrossega els botons per personalitzar la barra de comandes table.folder_access_error_title = Error en accedir al directori table.folder_access_error = No s'ha pogut llegir el contingut del directori table.download_or_browse = Vol explorar o descarregar aquest fitxer? version_dialog.no_new_version_title = No hi ha una versió nova version_dialog.no_new_version = Felicitats, ja té l'última versió version_dialog.new_version_title = Nova versió disponible version_dialog.new_version = Una nova versió d'trolCommander està disponible version_dialog.new_version_url = Una nova versió d'trolCommander està disponible a %1 version_dialog.not_available_title = No s'ha pogut accedir al servidor version_dialog.not_available = No s'ha pogut obtenir la informació del servidor version_dialog.install_and_restart = Instal·la i reinicia version_dialog.preparing_for_update = Preparant l'actualització... quit_dialog.title = Surt de l'trolCommander quit_dialog.desc = Té %1 finestra(es) oberta(es). N'està segur? quit_dialog.show_next_time = Mostra la propera vegada destination_dialog.file_exists_action = Acció per defecte quan el fitxer existeix destination_dialog.verify_integrity = Comprova la integritat de les dades destination_dialog.skip_errors = Salta els errors file_collision_dialog.title = Col·lisió de fitxers rename_dialog.new_name = Nou nom copy_dialog.destination = Copia el(s) fitxer(s) seleccionat(s) a copy_dialog.error_title = Error en copiar copy_dialog.copying = Copiant fitxers copy_dialog.copying_file = Copiant %1 pack_dialog.packing = Comprimint pack_dialog.packing_file = Comprimint %1 pack_dialog.error_title = Error de compressió pack_dialog_description = Afegeix els fitxers seleccionats a pack_dialog.archive_format = Format del fitxer unpack_dialog.destination = Descomprimeix el(s) fitxer(s) seleccionat(s) a unpack_dialog.error_title = Error en descomprimir unpack_dialog.unpacking = Descomprimint unpack_dialog.unpacking_file = Descomprimint %1 optimizing_archive = Optimitzant el fitxer %1 error_while_optimizing_archive = Error en comprimir el fitxer %1 move_dialog.move_description = Mou a move_dialog.error_title = Error en moure move_dialog.moving = Movent els fitxers move_dialog.moving_file = Movent %1 download_dialog.description = Baixar fitxer a download_dialog.error_title = Error de baixada download_dialog.downloading = Baixant download_dialog.downloading_file = Baixant %1 mkfile_dialog.allocate_space = Mida delete_dialog.permanently_delete.confirmation = Voleu eliminar permanentment el/s fitxer/s seleccionat/s? delete_dialog.move_to_trash.confirmation = Eliminar fitxer/s seleccionats? delete_dialog.move_to_trash.confirmation_details = Els fitxers s'enviaran a la paperera. delete_dialog.move_to_trash.option = Enviar a la paperera delete_dialog.move_to_trash.failed = Algun/s fitxer/s no s'han pogut enviar a la paperera. delete_dialog.deleting = Eliminant delete_dialog.error_title = Error eliminant delete.deleting_file = Eliminant %1 email_dialog.prefs_not_set_title = Correu electrònic sense configurar email_dialog.prefs_not_set = Primer heu de configurar el correu electrònic. email_dialog.from = De email_dialog.to = Per email_dialog.subject = Assumpte email_dialog.send = Enviar email_dialog.error_title = Error enviant email_dialog.read_error = No es poden llegir els fitxers dels dubdirectoris. email_dialog.sending = Enviant fitxers email.sending_file = Enviant %1 email.connecting_to_server = Connectant amb %1 email.server_unavailable = No es pot connectar amb el servidor %1, verifiqueu la vostra configuració de correu electrònic o intenteu-ho més tard. email.connection_closed = Connexió tancada pel servidor, el missatge no s'ha enviat. email.goodbye_failed = Error tancant la conneció, el missatge no s'ha enviat. email.send_file_error = No s'ha pogut enviar el fitxer %1, el missatge no s'ha enviat. split_file_dialog.error_title = Error en partir el fitxer split_file_dialog.file_to_split = Fitxer a trossejar split_file_dialog.target_directory = Directori de destí split_file_dialog.part_size = Mida de cada tros split_file_dialog.parts = Nombre de parts split_file_dialog.generate_CRC = Generar fitxer CRC split_file_dialog.max_parts = El màxim nombre de parts permés és %1 split_file_dialog.auto = Automàtic split_file_dialog.insert_new_media = Inserteu un nou suport combine_files_dialog.error_title = Error combinant fitxer combine_files_job.no_crc_file = Combinació finalitzada. No s'ha creat fitxer CRC. combine_files_job.crc_read_error = Error llegint fitxer CRC. combine_files_job.crc_check_failed = Desajust en CRC combine_files_job.crc_ok = Combinació finalitzada. Suma de verificació de CRC correcta. file_selection_dialog.mark = Marca file_selection_dialog.unmark = Desmarca file_selection_dialog.mark_description = Marca fitxers amb el nom file_selection_dialog.unmark_description = Desmarca fitxers amb el nom file_selection_dialog.case_sensitive = Diferencia Majúscules / minúscules file_selection_dialog.include_folders = Inclou directoris progress_dialog.starting = Iniciant la transferència... progress_dialog.transferred = %1 transferits a %2 progress_dialog.elapsed_time = Temps transcorregut progress_dialog.advanced = Avançades progress_dialog.current_speed = Velocitat actual progress_dialog.limit_speed = Limitar la velocitat progress_dialog.close_when_finished = Tancar la finestra en acabar progress_dialog.processing_files = Processant fitxers progress_dialog.processing_file = Processant %1 progress_dialog.verifying_file = Verificant %1 progress_dialog.job_finished = Tasca finalitzada progress_dialog.job_error = Error en tasca properties_dialog.file_properties = %1 Propietats properties_dialog.contents = Contingut properties_dialog.calculating = Calculant... calculate_checksum_dialog.checksum_algorithm = Algorisme de suma de verificació calculate_checksum_dialog.temporary_file = Fitxer temporal change_date_dialog.now = Ara change_date_dialog.specific_date = Data concreta run_dialog.run_command_description = Executar en el directori actual run_dialog.run_in_home_description = Executar en el directori del usuari run_dialog.command_output = Sortida de l'ordre run_dialog.run = Executar run_dialog.clear_history = Buidar l'històric server_connect_dialog.server_type = Tipus de connexió server_connect_dialog.server = Servidor server_connect_dialog.share = Compartir server_connect_dialog.domain = Domini server_connect_dialog.username = Nom d'usuari server_connect_dialog.initial_dir = Directori d'inici server_connect_dialog.port = Port server_connect_dialog.server_url = URL del servidor server_connect_dialog.http_url = URL de la pàgina web server_connect_dialog.connect = Connectar server_connect_dialog.protocol = Protocol server_connect_dialog.nfs_version = Versió de NFS server_connect_dialog.private_key = Clau privada server_connect_dialog.passphrase = Contrasenya ftp_connect.passive_mode = Activar en mode passiu ftp_connect.anonymous_user = Usuari anònim ftp_connect.nb_connection_retries = Nombre de intents de connexió ftp_connect.retry_delay = Retard entre intents (en segons) http_connect.basic_authentication = Autenticació bàsica d'HTTP (opcional) server_connections_dialog.disconnect = Desconnecta server_connections_dialog.connection_busy = Ocupat server_connections_dialog.connection_idle = Inactiu bonjour.bonjour_services = Serveis Bonjour bonjour.no_service_discovered = No s'han trobat serveis bonjour.bonjour_disabled = Bonjour deshabilitat auth_dialog.title = Autenticació auth_dialog.desc = Entreu un nom de usuari i una contrasenya auth_dialog.server = Servidor auth_dialog.store_credentials = Emmagatzema nom de usuari i contrasenya (encriptació feble) auth_dialog.connect_as = Connecta com auth_dialog.authentication_failed = Error autenticant sortable_list.move_up = Puja sortable_list.move_down = Baixa add_bookmark_dialog.add = Afegeix edit_bookmarks_dialog.new = Nou file_viewer.view_error_title = Error de visualització file_viewer.view_error = No es pot visualitzar el fitxer file_viewer.file_menu = Fitxer file_viewer.close = Tanca file_viewer.large_file_warning = Fitxer molt llarg; pot donar problemes durant la operació file_viewer.open_anyway = Obrir igualment text_viewer.edit = Edita text_viewer.copy = Còpia text_viewer.select_all = Selecciona-ho tot text_viewer.find = Busca text_viewer.find_next = Busca el següent text_viewer.find_previous = Busca l'anterior text_viewer.view = Vista text_viewer.line_wrap = Ajusta el text text_viewer.line_numbers = Numera línies text_viewer.binary_file_warning = El fitxer sembla binari image_viewer.controls_menu = Controls image_viewer.zoom_in = Apropar image_viewer.zoom_out = Allunyar file_editor.edit_error_title = Error d'edició file_editor.edit_error = No es pot editar l'arxiu. file_editor.save = Desa file_editor.save_as = Anomena i desa... file_editor.save_warning = Vols desar els canvis fets abans de tancar el fitxer? file_editor.cannot_write = No es pot escriure el fitxer text_editor.cut = Retalla text_editor.paste = Enganxa shortcuts_dialog.quick_search.start_search = Premeu qualsevol tecla per iniciar una cerca ràpida shortcuts_dialog.quick_search.cancel_search = Cancelar cerca ràpida shortcuts_dialog.quick_search.remove_last_char = Eliminar l'últim caràter de la cadena de la cerca ràpida shortcuts_dialog.quick_search.jump_to_previous = Anar al anterior resultat de la cerca ràpida shortcuts_dialog.quick_search.jump_to_next = Anar al resultat següent de la cerca ràpida shortcuts_dialog.quick_search.mark_jump_next = Marca/Desmarca fitxer actual i passa al següent resultat de la cerca theme_editor.title = Editor de temes theme_editor.folder_tab = Subfinestra de directori theme_editor.shell_tab = intèrpret d'ordres theme_editor.shell_history_tab = Historial del intèrpret d'ordres theme_editor.statusbar_tab = Barra d'estat theme_editor.free_space = Espai lliure theme_editor.free_space.ok = D'acord theme_editor.free_space.warning = Avís theme_editor.free_space.critical = Crític theme_editor.locationbar_tab = Barra de localització theme_editor.editor_tab = Editor de fitxers theme_editor.font = Tipus de lletra theme_editor.active_panel = Actiu theme_editor.inactive_panel = Inactiu theme_editor.general = General theme_editor.could_not_save_theme = No es pot escriure el tema %1 theme_editor.border = Vora theme_editor.background = Fons theme_editor.alternate_background = Alternar fons theme_editor.unfocused_background = Fons (sense focus) theme_editor.quick_search.unmatched_file = Fitxer sense coincidència theme_editor.text = Text theme_editor.progress = Realitzat theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (sense focus) theme_editor.selected = Seleccionat theme_editor.selected_unfocused = Seleccionat (sense focus) theme_editor.color = Color theme_editor.colors = Colors theme_editor.plain_file = Fitxer text net theme_editor.marked_file = Fitxer marcat theme_editor.hidden_file = Fitxer ocult theme_editor.folder = Directori theme_editor.archive_file = Arxiva fitxer theme_editor.symbolic_link = Enllaç simbòlic theme_editor.header = Capçalera theme_editor.item = Element command_bar_customize_dialog.available_actions = Accions disponibles command_bar_customize_dialog.modifier = Modificador prefs_dialog.title = Preferències prefs_dialog.general_tab = General prefs_dialog.day = Dia prefs_dialog.month = Mes prefs_dialog.year = Any prefs_dialog.language = Idioma (s'haurà de reiniciar) prefs_dialog.date_time = Format horari i de data prefs_dialog.time = Hora prefs_dialog.date = Data prefs_dialog.date_separator = Separador prefs_dialog.time_12_hour = Format 12 hores prefs_dialog.time_24_hour = Format 24 hores prefs_dialog.show_seconds = Mostrar segons prefs_dialog.show_century = Mostrar segle prefs_dialog.check_for_updates_on_startup = Buscar actualitzacions al iniciar prefs_dialog.show_splash_screen = Mostrar pantalla de presentació prefs_dialog.folders_tab = Directoris prefs_dialog.startup_folders = Directoris de inici prefs_dialog.left_folder = Directori esquerra prefs_dialog.right_folder = Directori dret prefs_dialog.last_folder = Últim directori visitat prefs_dialog.custom_folder = Directori personalitzat prefs_dialog.show_hidden_files = Mostrar fitxers ocults prefs_dialog.show_ds_store_files = Mostrar fitxers .DS_Store prefs_dialog.show_system_folders = Mostrar directoris de sistema prefs_dialog.compact_file_size = Arrodonir les mides dels fitxers mostrades prefs_dialog.follow_symlinks_when_cd = Seguir els enllaços simbòlics en canviar el directori actual prefs_dialog.appearance_tab = Aspecte prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Mida de les icones prefs_dialog.toolbar_icons = Barra d'eines prefs_dialog.command_bar_icons = Barra de comendes prefs_dialog.file_icons = Tipus de fitxers prefs_dialog.use_system_file_icons = Utilitzar les icones del sistema de fitxers prefs_dialog.use_system_file_icons.always = Sempre prefs_dialog.use_system_file_icons.never = Mai prefs_dialog.use_system_file_icons.applications = Només per programes prefs_dialog.edit_current_theme = Edita el tema actual... prefs_dialog.themes = Temes prefs_dialog.import_theme = Importa tema prefs_dialog.import_look_and_feel = Importa Look & Feel prefs_dialog.no_look_and_feel = No s'ha trobat cap Look & Feel. prefs_dialog.error_in_import = Error important el tema %1 prefs_dialog.cannot_read_theme = No s'ha pogut llegir el tema del fitxer %1 prefs_dialog.export_theme = Exporta %1 prefs_dialog.import = Importa prefs_dialog.export = Exporta prefs_dialog.theme_type = Tipus prefs_dialog.delete_theme = Eliminar el tema %1 permanentment? prefs_dialog.delete_look_and_feel = Eliminar el Look & Feel %1 permanentment? prefs_dialog.rename_failed = Error renombrant el tema %1 prefs_dialog.xml_file = Fitxer XML prefs_dialog.jar_file = Fitxer JAR prefs_dialog.mail_tab = Correu electrònic prefs_dialog.mail_settings = Configuració del correu de sortida prefs_dialog.mail_name = El vostre nom prefs_dialog.mail_address = El vostre correu electrònic prefs_dialog.mail_server = Servidor SMTP prefs_dialog.misc_tab = Varis prefs_dialog.use_brushed_metal = Utilitza l'aspecte 'raspatllat metàl·lic' (s'ha de reiniciar) prefs_dialog.confirm_on_quit = Demanar confirmació en sortir prefs_dialog.default_shell = Utilitzar l'intèrpret d'ordres del sistema prefs_dialog.custom_shell = Utilitzar un intèrpret d'ordres específic prefs_dialog.shell_encoding = Codificació del interpret d'ordres prefs_dialog.auto_detect_shell_encoding = Detecció automàtica prefs_dialog.enable_bonjour_discovery = Activar cerca de Serveis Bonjour prefs_dialog.enable_system_notifications = Activar les notificacions del sistema debug_console_dialog.level = Nivell unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = kB unit.mb = Mb unit.gb = Gb unit.tb = Tb unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1me duration.years = %1a theme.custom_theme = Tema personalitzat theme.custom = Personalitzat theme.built_in = Integrat theme.add_on = Complement theme.current = actual theme_could_not_be_loaded = S'ha produït un error en carregar aquest tema. setup.title = Benvinguts a trolCommander setup.intro = Trieu el comportament desitjat de trolCommander. setup.look_and_feel = Seleccioneu el vostre Look & Feel setup.theme = Seleccioneu el vostre tema font_chooser.font_size = Mida font_chooser.font_bold = Negreta font_chooser.font_italic = Cursiva color_chooser.red = Vermell color_chooser.green = Verd color_chooser.blue = Blau color_chooser.hue = To color_chooser.brightness = Brillantor color_chooser.swatches = Mostres color_chooser.saturation = Saturació color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Recent color_chooser.alpha = Transparència alpha color_chooser.title = Seleccioneu un color batch_rename_dialog.mask = Renombra patró batch_rename_dialog.search_replace = Cerca i reemplaça batch_rename_dialog.search_for = Cerca batch_rename_dialog.replace_with = Reemplaça amb batch_rename_dialog.counter = Comptador batch_rename_dialog.start_at = Comença a batch_rename_dialog.step_by = Salta'n batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Majúscules/Minúscules batch_rename_dialog.no_change = Sense canvis batch_rename_dialog.lower_case = minúscules batch_rename_dialog.upper_case = MAJÚSCULES batch_rename_dialog.first_upper = Primera lletra en majúscula batch_rename_dialog.word = Primera Lletra De Cada Paraula batch_rename_dialog.old_name = Nom antic batch_rename_dialog.new_name = Nom nou batch_rename_dialog.block_name = Protegeix batch_rename_dialog.range = Abast batch_rename_dialog.proceed_renaming = Es canviarà el nom de %1 fitxers de %2. Vols continuar? batch_rename_dialog.duplicate_names = Noms duplicats! batch_rename_dialog.names_conflict = Conflicte de noms! Coïncideixen alguns noms antics i nous. parent_folders_quick_list.empty_message = L'emplaçament actual no te pare recent_locations_quick_list.empty_message = No hi ha emplaçaments recents recent_executed_files_quick_list.empty_message = No hi ha fitxers executats recentment ================================================ FILE: src/main/resources/dictionary_cs_CZ.properties ================================================ ok = OK yes = Ano no = Ne cancel = Zrušit edit = Upravit close = Zavřít reset = Resetovat rename = Přejmenovat apply = Použít change = Změnit save = Uložit dont_save = Neukládat replace = Nahradit dont_replace = Nenahrazovat delete = Smazat skip = Přeskočit skip_all = Přeskočit vše overwrite_all = Přepsat vše retry = Opakovat resume = Pokračovat overwrite = Přepsat overwrite_if_older = Přepsat starší duplicate = Duplikovat apply_to_all = Použít na všechny copy = Kopírovat move = Přesunout pack = Komprimovat unpack = Dekomprimovat download = Stáhnout split = Rozdělit combine = Sloučit browse = Procházet ask = Dotázat se stop = Zastavit pause = Pauza quick_search = Rychlé hledání file_manager = Správce souborů create = Vytvořit creating_file = Vytváří se %1 choose = Vybrat customize = Přizpůsobit clean = Vyčistit search = Vyhledávat choose_folder = Vyberte složku login = Přihlašovací jméno password = Heslo user = Uživatel encoding = Kódování preferred_encodings = Předvolená kódování license = Licence name = Jméno size = Velikost date = Datum extension = Přípona permissions = Oprávnění owner = Vlastník group = Skupina location = Umístění untitled = Beze jména source = Zdroj destination = Cíl recurse_directories = Použít i vnořené složky go_to = Přejít na example = Příklad preview = Náhled comment = Komentář sample_text = Ukázkový text nb_files = %1x soubor nb_folders = %1x složka loading = Načítám obsah\u2026 this_operation_cannot_be_undone = Tato operace je nevratná. remove = Odstranit details = Podrobnosti title = Název warning = Upozornění error = Chyba files=soubor(y) generic_error = Při provádění požadované operace se vyskytla chyba. folder_does_not_exist = Tato složka neexistuje nebo není dostupná. this_folder_does_not_exist = Tato složka neexistuje nebo není dostupná: %1 this_file_does_not_exist = Tento soubor neexistuje nebo není dostupný: %1 failed_to_read_folder=Na\u010Dten\u00ED t\u00E9to slo\u017Eky selhalo. invalid_path = Neplatná cesta: %1 directory_already_exists = Složka %1 již existuje. file_exists_in_destination = Soubor již v cíli existuje source_parent_of_destination = Pokus o přesun složky do jedné z jejích podsložek cannot_read_file = Nelze přečíst soubor %1 cannot_write_file = Nelze zapsat soubor %1 overwrite_readonly_file = Soubor je pouze pro čtení: %1 cannot_create_folder = Nelze vytvořit složku %1 cannot_read_folder = Nelze přečíst obsah složky %1 cannot_delete_file = Nelze smazat soubor %1 cannot_delete_folder = Nelze smazat složku %1 error_while_transferring = Chyba při přenosu souboru %1 error_unsupported_operation=Operace nen\u00ED podporov\u00E1na same_source_destination = Zdojová a cílová složka se shodují file_already_exists = %1 již existuje, chcete jej přepsat? write_error = Chyba zápisu read_error = Chyba při čtení integrity_check_error = Kontrola integrity se nezdařila: zdroj a cíl se neshodují startup_error = Vyskytla se chyba bránící spuštění aplikace trolCommander. image_size = Velikost obrázku cannot_write_symlink = Nelze vytvořit symlink %1 cannot_write_symlink_already_exists = Symlink již existuje: %1 cannot_write_symlink_access_denied = Symlink nelze uložit %1, přístup zamítnut permissions.read = Čtení permissions.write = Zápis permissions.executable = Spouštění permissions.user = Uživatel permissions.group = Skupina permissions.other = Ostatní permissions.octal_notation = Zápis v osmičkové soustavě action_categories.all = Vše action_categories.navigation = Navigace action_categories.selection = Výběr action_categories.view = Zobrazit action_categories.file_operations = Operace se soubory action_categories.windows = Okna action_categories.tabs = Panely action_categories.commands = Vlastní příkazy action_categories.misc = Ostatní Terminal.label = Terminál Terminal.tooltip = Spustit okno terminálu v aktuální složce TerminalPanel.label = Terminál TerminalPanel.tooltip = Zobrazit/skrýt panel terminálu FindFile.label = Hledat soubor FindFile.tooltip = Hledat soubor AddBookmark.label = Přidat záložku AddBookmark.tooltip = Přidat stávající složku na seznam záložek BatchRename.label = Dávkové přejmenování EditBookmarks.label = Upravit záložky ExploreBookmarks.label = Procházet záložky EditCredentials.label = Upravit účty ChangeLocation.label = Změnit stávající umístění ChangeDate.label = Změnit datum ChangeDate.tooltip = Změnit datum vybraných souborů ChangePermissions.label = Změnit oprávnění ChangePermissions.tooltip = Změnit oprávnění u vybraných souborů CheckForUpdates.label = Zkontrolovat aktualizace CompareFolders.label = Porovnat složky CompareFolderFiles.label=Porovnat soubory CompareFolderFiles.tooltip=Porovnat soubory a ozna\u010Dit zm\u011Bn\u011Bn\u00E9 v aktu\u00E1ln\u00ED slo\u017Ece ConnectToServer.label = Připojit se k serveru ConnectToServer.tooltip = Připojit se k vzdálenému serveru TextEditorsList.label=Zobrazit seznam textov\u00FDch editor\u016F TextEditorsList.tooltip=Zobrazit seznam textov\u00FDch editor\u016F View.label = Zobrazit ViewAs.label = Zobrazit jako InternalView.label = Zobrazit (interně) viewer_type.text = Textový soubor viewer_type.hex = Binární soubor viewer_type.image = Obrázek viewer_type.pdf = Dokument PDF viewer_type.audio = Audio soubor viewer_type.html = Dokument HTML View.tooltip = Zobrazit vybraný soubor Edit.label = Upravit InternalEdit.label = Upravit (interně) Calculator.label = Kalkulačka CreateSymlink.label = Vytvořit symlink LocateSymlink.label = Přejít na cíl symlinku ShowFoldersSize.label = Zobrazit velikost složek EditCommands.label = Upravit příkazy EditCommands.group.view = Prohlížeče EditCommands.group.edit = Editory EditCommands.group.others = Ostatní CompareFiles.label=Porovnat soubory CompareFiles.tooltip=Porovnat textov\u00E9 soubory (FileMerge) EditCommands.new = Nový EditCommands.alias = Přezdívka EditCommands.command = Příkaz EditCommands.display_name = Zobrazený název EditCommands.filemask = Souborová maska symboliclinkeditor.edit = Upravit symlink symboliclinkeditor.create = Symbolický link symboliclinkeditor.target_file_create = Existující soubor (soubor, na který odkazuje symlink) symboliclinkeditor.target_file_edit = Symlink „%s“ odkazuje na symboliclinkeditor.link_name = Soubor symbolického linku Edit.tooltip = Upravit vybraný soubor Copy.label = Kopírovat Copy.tooltip = Kopírovat označené soubory LocalCopy.label = Lokální kopie LocalCopy.tooltip = Kopírovat vybraný soubor do stávající složky Move.label = Přesunout Move.tooltip = Přesunout označené soubory Rename.label = Přejmenovat Rename.tooltip = Přejmenovat vybraný soubor Mkdir.label = Vytvořit složku Mkdir.tooltip = Vytvořit složku ve stávající Mkfile.label = Vytvořit nový soubor Mkfile.tooltip = Vytvořit soubor ve stávající složce Delete.label = Smazat Delete.tooltip = Je-li možné, označené přesunout do Koše PermanentDelete.label = Smazat přímo PermanentDelete.tooltip = Smazat označené soubory bez použití systémového Koše Refresh.label = Načíst znovu Refresh.tooltip = Načíst znovu stávající složku CloseWindow.label = Zavřít okno CloseWindow.tooltip = Zavřít stávající okno CopyFileNames.label = Kopírovat jméno(a) CopyFileBaseNames.label = Kopírovat název(názvy) CopyFilePaths.label = Kopírovat cestu(y) CopyFilesToClipboard.label = Kopírovat soubor(y) PasteClipboardFiles.label = Vložit soubor(y) Email.label = Odeslat e-mailem Email.tooltip = Odeslat označené soubory jako přílohu GoBack.label = Zpět GoBack.tooltip = Předchozí složka GoForward.label = Vpřed GoForward.tooltip = Následující složka GoToHome.label = Domovská složka GoToParent.label = O úroveň výš GoToParent.tooltip = Nadřazená složka GoToParentInOtherPanel.label = O úroveň výš ve druhém panelu GoToParentInBothPanels.label = O úroveň výš v obou panelech GoToRoot.label = Kořenová složka SortByName.label = Řadit dle jména SortByDate.label = Řadit dle datumu SortBySize.label = Řadit dle velikosti SortByExtension.label = Řadit dle přípony SortByPermissions.label = Řadit dle oprávnění SortByOwner.label = Řadit dle vlastníka SortByGroup.label = Řadit dle skupiny MarkGroup.label = Označit soubory MarkGroup.tooltip = Označit skupinu souborů UnmarkGroup.label = Odznačit soubory UnmarkGroup.tooltip = Odznačit skupinu souborů MarkAll.label = Označit vše UnmarkAll.label = Odznačit vše MarkSelectedFile.label = Označit/odznačit MarkSelectedFile.tooltip = Označit/odznačit vybraný soubor MarkNextBlock.label = Označit blok následujících 5 objektů MarkPreviousBlock.label = Označit blok předchozích 5 objektů MarkNextRow.label = Označit následující řádek MarkPreviousRow.label = Označit předchozí řádek MarkNextPage.label = Označit po následující stránku MarkPreviousPage.label = Označit po předchozí stránku MarkToFirstRow.label = Označit soubory do začátku MarkToLastRow.label = Označit soubory do konce MarkExtension.label = Označit příponu MarkEmpty.label=Ozna\u010D pr\u00E1zdn\u00E9 InvertSelection.label = Obrátit výběr SwapFolders.label = Prohodit složky SwapFolders.tooltip = Prohodit složky vlevo a vpravo SetSameFolder.label = Nastavit stejnou složku SetSameFolder.tooltip = Nastavit stejnou složku vlevo a vpravo ToggleTableViewModeFull.label=Pln\u00FD re\u017Eim ToggleTableViewModeFull.tooltip=P\u0159epnout zobrazen\u00ED v pln\u00E9m re\u017Eimu ToggleTableViewModeCompact.label=Kompaktn\u00ED re\u017Eim ToggleTableViewModeCompact.tooltip=P\u0159epnout zobrazen\u00ED v kompaktn\u00EDm re\u017Eimu ToggleTableViewModeShort.label=Kr\u00E1tk\u00FD re\u017Eim ToggleTableViewModeShort.tooltip=P\u0159epnout zobrazen\u00ED v kr\u00E1tk\u00E9m re\u017Eimu TogglePanelPreviewMode.label=Rychl\u00FD re\u017Eim TogglePanelPreviewMode.tooltip=P\u0159epnout zobrazen\u00ED v rychl\u00E9m re\u017Eimu NewWindow.label = Nové okno NewWindow.tooltip = Otevřít nové okno Open.label = Otevřít Open.tooltip = Otevřít složku / Otevřít archív / Spustit OpenNatively.label = Otevřít výchozí aplikací OpenNatively.tooltip = Otevřít vybraný soubor asociovanou aplikací OpenInNewTab.label = Otevřít v novém panelu OpenInOtherPanel.label = Otevřít ve druhém panelu OpenInBothPanels.label = Otevřít v obou panelech OpenLeftInRightPanel.label = Otevřít levý v právém panelu OpenRightInLeftPanel.label = Otevřít pravý v levém panelu RevealInDesktop.label = Otevřít v aplikaci %1 RunCommand.label = Spustit příkaz RunCommand.tooltip = Spustit příkaz ve stávající složce Pack.label = Komprimovat Pack.tooltip = Komprimovat označené soubory do archívu Unpack.label = Dekomprimovat Unpack.tooltip = Dekomprimovat označené archívy ShowFileProperties.label = Vlastnosti ShowFileProperties.tooltip = Zobrazit vlastnosti označených souborů ShowFilePopupMenu.label=Kontextov\u00E1 nab\u00EDdka ShowFilePopupMenu.tooltip=Zobrazit konktextovou nab\u00EDdku pro zvolen\u00FD soubor ShowPreferences.label = Nastavení ShowPreferences.tooltip = Přizpůsobit trolCommander ShowServerConnections.label = Zobrazit otevřená připojení Quit.label = Ukončit ReverseSortOrder.label = Obrátit pořadí ToggleAutoSize.label = Automatická šířka sloupců Stop.label = Zastavit změnu složky ToggleColumn.show = Zobrazit sloupec %1 ToggleColumn.hide = Skrýt sloupec %1 ToggleCommandBar.show = Zobrazit lištu příkazů ToggleCommandBar.hide = Skrýt lištu příkazů ToggleHiddenFiles.label = Zobrazit skryté soubory ToggleToolBar.show = Zobrazit tlačítkovou lištu ToggleToolBar.hide = Skrýt tlačítkovou lištu CustomizeCommandBar.label = Přizpůsobit lištu příkazů ToggleStatusBar.show = Zobrazit stavový řádek ToggleStatusBar.hide = Skrýt stavový řádek ToggleShowFoldersFirst.label = Zobrazit nejdříve složky ToggleFoldersAlwaysAlphabetical.label=T\u0159idit slo\u017Eky v\u017Edy abecedn\u011B ToggleTree.label = Zobrazit strom složek ToggleSinglePanel.label=P\u0159epnout jedin\u00FD panel PopupLeftDriveButton.label = Změnit složku vlevo PopupRightDriveButton.label = Změnit složku vpravo RecallPreviousWindow.label = Přepnout do předchozího okna RecallNextWindow.label = Přepnout do následujícího okna RecallWindow.label = Přepnout do okna #%1 RecallWindow1.label = Přepnout do okna #%1 RecallWindow2.label = Přepnout do okna #%1 RecallWindow3.label = Přepnout do okna #%1 RecallWindow4.label = Přepnout do okna #%1 RecallWindow5.label = Přepnout do okna #%1 RecallWindow6.label = Přepnout do okna #%1 RecallWindow7.label = Přepnout do okna #%1 RecallWindow8.label = Přepnout do okna #%1 RecallWindow9.label = Přepnout do okna #%1 RecallWindow10.label = Přepnout do okna #%1 BringAllToFront.label = Přenést vše do popředí SwitchActiveTable.label = Přepnout mezi levým a pravým panelem SelectNextBlock.label = Přejít o blok (5 objektů) níž SelectPreviousBlock.label = Přejít o blok (5 objektů) výš SelectNextPage.label = Přejít o stránku níž SelectPreviousPage.label = Přejít o stránku výš SelectNextRow.label = Přejít o řádek níž SelectPreviousRow.label = Přejít o řádek výš SelectFirstRow.label = Vybrat první soubor ve stávající složce SelectLastRow.label = Vybrat poslední soubor ve stávající složce LeftArrowAction.label=Navigovat vlevo RightArrowAction.label=Navigovat vpravo SplitEqually.label = Rozdělit rovnoměrně SplitVertically.label = Rozdělit vertikálně SplitHorizontally.label = Rozdělit horizontálně ShowKeyboardShortcuts.label = Klávesové zkratky GoToWebsite.label = Webová stránka GoToForums.label = Diskuzní fórum ReportBug.label = Nahlásit chybu Donate.label = Darovat příspěvek ShowAbout.label = O aplikaci trolCommander OpenTrash.label = Otevřít Koš EmptyTrash.label = Vyprázdnit Koš CalculateChecksum.label = Kontrolní součet MaximizeWindow.label = Maximalizovat MaximizeWindow.label.mac_os_x = Měřítko MinimizeWindow.label = Minimalizovat GoToDocumentation.label = Dokumentace na webu ShowParentFoldersQL.label = Nadřazené složky ShowRecentLocationsQL.label = Dříve navštívená umístění ShowRecentExecutedFilesQL.label = Dříve spuštěné soubory ShowRootFoldersQL.label = Kořenové složky ShowTabsQL.label = Otevřít panely ShowRecentViewedFilesQL.label = Nedávno zobrazené soubory ShowRecentEditedFilesQL.label = Nedávno upravené soubory SplitFile.label = Rozdělit SplitFile.tooltip = Rozdělit soubor na více částí CombineFiles.label = Sloučit CombineFiles.tooltip = Sloučit části rozděleného souboru do původního souboru ShowDebugConsole.label = Ladící konzole FocusPrevious.label = Zaměřit fokus na předchozí komponentu FocusNext.label = Zaměřit fokus na následující komponentu ShowBookmarksQL.label = Záložky ShowEditorBookmarksQL.label=Z\u00E1lo\u017Eky soubor\u016F EjectDrive.label=Vysunout za\u0159\u00EDzen\u00ED EjectDrive.tooltip=Bezpe\u010Dn\u011B odebrat za\u0159\u00EDzen\u00ED file_menu = Soubor file_menu.open_with = Otevřít pomocí file_menu.open_as=Otev\u0159\u00EDt jako mark_menu = Označit view_menu = Zobrazit view_menu.show_hide_columns = Zobrazit/skrýt sloupce view_menu.table_mode=Re\u017Eim go_menu = Přejít tools_menu = Nástroje eject_menu=Vysunout za\u0159\u00EDzen\u00ED bookmarks_menu = Záložky bookmarks_menu.no_bookmark = Bez záložek drive_popup.network_shares = Síťové sdílení quick_lists_menu = Rychlé přehledy window_menu = Okno help_menu = Nápověda status_bar.selected_files = Vybráno: %1 z %2 status_bar.connecting_to_folder = Připojuji se, stiskem ESCAPE akci zrušíte. status_bar.volume_free = Volné místo: %1 status_bar.volume_capacity = Kapacita: %1 status_bar.quick_search.press_esc_to_stop_search=p\u0159eru\u0161it kl\u00E1vesou Esc shortcuts_panel.title = Zkratky shortcuts_panel.restore_defaults = Obnovit výchozí shortcuts_panel.show = Zobrazit shortcuts_panel.search = Vyhledávat shortcuts_panel.default_message = Stiskněte Enter na zkratce nebo poklepejte na zkratku, kterou chcete upravit shortcuts_table.action_description = Popis akce shortcuts_table.shortcut = Zkratka shortcuts_table.alternate_shortcut = Alternativní zkratka shortcuts_table.type_in_a_shortcut = Zadejte zkratku command_bar_dialog.help = Přetáhněte vybraná tlačítka na náhled lišty table.folder_access_error_title = Chyba při přístupu k složce table.folder_access_error = Nelze číst obsah složky table.download_or_browse = Chcete si soubor prohlédnout nebo jej stáhnout? AddTab.label = Přidat panel AddTab.tooltip = Přidat nový panel v aktivním panelu ToggleLockTab.lock = Zamknout ToggleLockTab.unlock = Odemknout ToggleLockTab.label = Zamknout/odemknout ToggleLockTab.tooltip = Zmenit stav zámku panelu DuplicateTab.label = Duplikovat DuplicateTab.tooltip = Duplikovat aktivní panel ve stejném panelu CloseDuplicateTabs.label = Zavřít duplikáty CloseDuplicateTabs.tooltip = Zavřít duplicitní panely CloseOtherTabs.label = Zavřít ostatní CloseOtherTabs.tooltip = Zavřít ostatní panely CloseTab.label = Zavřít CloseTab.tooltip = Zavřít panel MoveTabToOtherPanel.label = Přesunout na jiný panel MoveTabToOtherPanel.tooltip = Přesunout panel na jiný panel CloneTabToOtherPanel.label = Klonovat na jiný panel CloneTabToOtherPanel.tooltip = Přidat podobný panel na jiný panel NextTab.label = Další panel NextTab.tooltip = Přepnout na panel vpravo PreviousTab.label = Předchozí panel PreviousTab.tooltip = Přepnout na panel vlevo SetTabTitle.label = Nastavit název SetTabTitle.tooltip = Nastavit fixní název panelu version_dialog.no_new_version_title = Není nová verze version_dialog.no_new_version = Blahopřejeme, máte aktuální verzi. version_dialog.new_version_title = K dispozici je novější verze. version_dialog.new_version = K dispozici je nová verze aplikace trolCommander. version_dialog.new_version_url = Novou verzi aplikace trolCommander naleznete na %1. version_dialog.not_available_title = Server není dostupný version_dialog.not_available = Nelze získat ze serveru informace o verzi. version_dialog.install_and_restart = Nainstalovat a restartovat version_dialog.preparing_for_update = Aktualizace se připravuje\u2026 quit_dialog.title = Ukončit trolCommander quit_dialog.desc = Chcete zavřít všechna otevřená okna (%1) a ukončit trolCommander ? quit_dialog.show_next_time = Zobrazit příště destination_dialog.file_exists_action = Výchozí akce pro existující soubor destination_dialog.verify_integrity = Ověřit integritu destination_dialog.skip_errors = Ignorovat chyby destination_dialog.background_mode = Na pozadí file_collision_dialog.title = Kolize souboru rename_dialog.new_name = Nové jméno copy_dialog.destination = Kopírovat vybrané soubory do copy_dialog.error_title = Chyba při kopírování copy_dialog.copying = Kopírování souborů copy_dialog.copying_file = Kopírování %1\u2026 pack_dialog.packing = Komprimace pack_dialog.packing_file = Komprimace %1\u2026 pack_dialog.error_title = Chyba komprimace pack_dialog_description = Přidat vybrané soubory do pack_dialog.archive_format = Formát archívu unpack_dialog.destination = Dekomprimovat do unpack_dialog.error_title = Chyba dekomprimace unpack_dialog.unpacking = Dekomprimace unpack_dialog.unpacking_file = Dekomprimace %1\u2026 optimizing_archive = Optimalizace archívu %1\u2026 error_while_optimizing_archive = Chyba při optimalizaci archívu %1 move_dialog.move_description = Přesunout do move_dialog.error_title = Chyba při přesouvání move_dialog.moving = Přesouvání souborů move_dialog.moving_file = Přesouvání %1\u2026 download_dialog.description = Stáhnout soubor do download_dialog.download = Stáhnout download_dialog.error_title = Chyba při stahování download_dialog.downloading = Stahování download_dialog.downloading_file = Stahování %1\u2026 mkfile_dialog.allocate_space = Alokovaný prostor mkfile_dialog.open_in_editor = Otevřít v textovém editoru mkfile_dialog.make_executable = Spustitelný soubor mkfile_dialog.convert_whitespace=P\u0159ev\u00E9st netisknuteln\u00E9 znaky delete_dialog.permanently_delete.confirmation = Trvale smazat vybrané soubory? delete_dialog.permanently_delete.confirmation_1=Trvale smazat vybran\u00FD soubor? delete_dialog.move_to_trash.confirmation = Chcete smazat vybrané soubory? delete_dialog.move_to_trash.confirmation_1=Chcete smazat vybran\u00FD soubor? delete_dialog.move_to_trash.confirmation_details = Soubory budou přesunuty do Koše. delete_dialog.move_to_trash.confirmation_details_1=Soubor bude p\u0159esunut do Ko\u0161e. delete_dialog.move_to_trash.option = Přesunout do Koše delete_dialog.move_to_trash.failed = Některý soubor nebylo možné přesunout do Koše. delete_dialog.deleting = Odstraňování delete_dialog.error_title = Chyba při mazání delete.deleting_file = Odstraňování %1\2026 email_dialog.prefs_not_set_title = Chybějící nastavení elektronické pošty email_dialog.prefs_not_set = Nejprve nastavte parametry pro odesílání elektronické pošty. email_dialog.from = Od email_dialog.to = Komu email_dialog.subject = Předmět email_dialog.send = Odeslat email_dialog.error_title = Chyba při odesílání email_dialog.read_error = Nelze číst soubory v podsložkách. email_dialog.sending = Odesílání souborů email.sending_file = Odesílání %1\u2026 email.connecting_to_server = Připojování k %1\u2026 email.server_unavailable = Nelze se připojit k poštovnímu serveru %1, zkontrolujte nastavení nebo opakujte akci později. email.connection_closed = Server ukončil připojení, pošta nebyla odeslána. email.goodbye_failed = Chyba při ukončování připojení, pošta pravděpodobně nebyla odeslána. email.send_file_error = Nelze odeslat soubor %1, zpráva nebyla odeslána. split_file_dialog.error_title = Chyba při rozdělování souboru split_file_dialog.file_to_split = Soubor k rozdělení split_file_dialog.target_directory = Cílová složka split_file_dialog.part_size = Velikost každé části split_file_dialog.parts = Počet částí split_file_dialog.generate_CRC = Vytvořit kontrolní součet split_file_dialog.max_parts = Maximální počet částí je %1 split_file_dialog.auto = Automaticky split_file_dialog.insert_new_media = Vložte nové médium combine_files_dialog.error_title = Chyba při slučování combine_files_job.no_crc_file = Sloučení proběhlo úspěšně (bez kontrolního součtu). combine_files_job.crc_read_error = Chyba při čtení kontrolního součtu. combine_files_job.crc_check_failed = Kontrolní součet nesouhlasí: očekáváno %2, zjištěno %1 combine_files_job.crc_ok = Sloučení proběhlo úspěšně. Kontrolní součet je v pořádku. file_selection_dialog.mark = Označit file_selection_dialog.unmark = Odznačit file_selection_dialog.mark_description = Označit soubory, jejichž jméno file_selection_dialog.unmark_description = Odznačit soubory, jejichž jméno file_selection_dialog.case_sensitive = Rozlišovat VELKÁ/malá písmena file_selection_dialog.include_folders = Zahrnout složky progress_dialog.starting = Přenos zahájen\u2026 progress_dialog.transferred = Přeneseno %1 při rychlosti %2 progress_dialog.elapsed_time = Uplynulý čas progress_dialog.advanced = Pokročilé progress_dialog.current_speed = Aktuální rychlost progress_dialog.limit_speed = Rychlostní limit progress_dialog.close_when_finished = Po dokončení zavřít okno progress_dialog.processing_files = Zpracování souborů progress_dialog.processing_file = Zpracování %1\u2026 progress_dialog.verifying_file = Ověřování %1\u2026 progress_dialog.job_finished = Úkol dokončen progress_dialog.job_error = Chyba progress_dialog.hide = Skrýt properties_dialog.file_properties = Vlastnosti objektu „%1“ properties_dialog.contents = Obsah properties_dialog.calculating = výpočet\u2026 calculate_checksum_dialog.checksum_algorithm = Algoritmus výpočtu calculate_checksum_dialog.temporary_file = Dočasný soubor change_date_dialog.now = Aktuální change_date_dialog.specific_date = Jiné datum run_dialog.run_command_description = Spustit v aktuální složce run_dialog.run_in_home_description = Spustit v domovské složce run_dialog.command_output = Výstup příkazu run_dialog.run = Spustit run_dialog.stop = Zastavit run_dialog.clear_history = Vymazat historii server_connect_dialog.server_type = Typ připojení server_connect_dialog.server = Server server_connect_dialog.share = Sdílet server_connect_dialog.domain = Doména server_connect_dialog.username = Přihlašovací jméno server_connect_dialog.initial_dir = Počáteční složka server_connect_dialog.port = Port server_connect_dialog.server_url = Adresa serveru server_connect_dialog.http_url = Adresa webu server_connect_dialog.connect = Připojit server_connect_dialog.protocol = Protokol server_connect_dialog.nfs_version = Verze NFS server_connect_dialog.private_key = Soukromý klíč server_connect_dialog.passphrase = Heslo ftp_connect.passive_mode = Povolit pasivní mód ftp_connect.anonymous_user = Anonymní uživatel ftp_connect.nb_connection_retries = Počet opakování pokusů o připojení ftp_connect.retry_delay = Prodleva mezi pokusy (v sekundách) http_connect.basic_authentication = Základní ověření HTTP (volitelné) server_connections_dialog.disconnect = Odpojit server_connections_dialog.connection_busy = Zaneprázdněný server_connections_dialog.connection_idle = Neaktivní vsphere_connections_dialog.guest_server = Host server %1 vsphere_connections_dialog.guest_user = Host jméno vsphere_connections_dialog.guest_password = Host heslo bonjour.bonjour_services = Služba Bonjour bonjour.no_service_discovered = Služba nenalezena bonjour.bonjour_disabled = Služba Bonjour nepovolena auth_dialog.title = Ověření auth_dialog.desc = Zadejte prosím přihlašovací jméno a heslo auth_dialog.server = Server auth_dialog.store_credentials = Uložit přihlašovací jméno a heslo (slabé šifrování) auth_dialog.connect_as = Připojit jako auth_dialog.authentication_failed = Ověření se nezdařilo sortable_list.move_up = Posun výš sortable_list.move_down = Posun níž add_bookmark_dialog.add = Přidat edit_bookmarks_dialog.location = Umístění edit_bookmarks_dialog.new = Nová edit_bookmarks_dialog.is_separator=Specifikovan\u00FD n\u00E1zev definuje separ\u00E1tor file_viewer.view_error_title = Chyba zobrazení file_viewer.view_error = Nelze zobrazit soubor. file_viewer.file_menu = Soubor file_viewer.close = Zavřít file_viewer.large_file_warning = Vybraný soubor může být pro tuto operaci příliš velký. file_viewer.open_anyway = Přesto otevřít file_viewer.open_hex = Zobrazit binárně text_viewer.edit = Upravit text_viewer.copy = Kopírovat text_viewer.select_all = Vybrat vše text_viewer.find = Najít text_viewer.find_button=Naj\u00EDt text_viewer.find_next = Najít následující text_viewer.find_previous = Najít předchozí text_viewer.find.case_sensitive=Rozli\u0161ovat velikost text_viewer.find.whole_word=Cel\u00E9 slovo text_viewer.find.regexp=Regexp text_viewer.find.mark_all=Ozna\u010D v\u0161e text_viewer.find.direction=Sm\u011Br text_viewer.find.up=Nahoru text_viewer.find.down=Dol\u016F text_viewer.view = Zobrazit text_viewer.line_wrap = Zalamovat řádky text_viewer.line_numbers = Čísla řádků text_viewer.binary_file_warning = Toto je zřejmě binární soubor text_viewer.goto_line = Skočit na řádek text_viewer.line = Řádek image_viewer.controls_menu = Ovládání image_viewer.zoom_in = Přiblížit image_viewer.zoom_out = Oddálit file_editor.edit_error_title = Chyba při provádění úprav file_editor.edit_error = Soubor nelze upravovat. file_editor.save = Uložit file_editor.save_as = Uložit jako\u2026 file_editor.save_warning = Chcete před zavřením uložit provedené změny? file_editor.cannot_write = Nelze zapisovat do souboru. file_editor.file_menu = Soubor file_editor.close = Zavřít file_editor.open_anyway = Přesto otevřít file_editor.save_anyway = Přesto zavřít file_editor.overwrite_readonly = Soubor je jenom pro čtení file_editor.files=Soubory file_editor.files.list=Vyberte soubor file_editor.show_file_manager=Zobrazit souborov\u00E9ho spr\u00E1vce file_editor.add_to_bookmark=P\u0159idat do z\u00E1lo\u017Eek file_editor.remove_from_bookmark=Odebrat ze z\u00E1lo\u017Eek file_editor.goto_header_source=P\u0159ejit na hlavi\u010Dku/zdroj text_editor.cut = Vyjmout text_editor.paste = Vložit text_editor.undo = Zpět text_editor.redo = Znovu text_editor.edit = Změnit text_editor.copy = Kopírovat text_editor.select_all = Vybrat vše text_editor.view = Zobrazit text_editor.find = Hledat text_editor.find_next = Hledat další text_editor.find_previous = Hledat předchozí text_editor.search=Vyhled\u00E1vat text_editor.replace=Nahradit text_editor.replace_menu=Nahradit\u2026 text_editor.replace_button=Nahradit text_editor.replace_with=Nahradit \u010D\u00EDm text_editor.replace_all=Nahradit v\u0161e text_editor.replaced=Nahrazeno text_editor.occurrences=v\u00FDskyt\u016F text_editor.line_wrap = Zalamování řádků text_editor.line_numbers = Čísla řádků text_editor.syntax = Zvýrazňování text_editor.format = Formát text_editor.writing = Ukládání\u2026 text_editor.modified = Změněno text_editor.saved = Soubor uložen text_editor.text_not_found = Text nenalezen text_editor.found=Nalezeno text_editor.matches=shody text_editor.cant_save_file = Soubor nelze uložit text_editor.press_alt_enter_to_open_file=Stiskem Alt+Enter otev\u0159\u00EDt soubor text_editor.tools=N\u00E1stroje text_editor.build=Sestaven\u00ED text_editor.invisible_chars=Neviditeln\u00E9 znaky shortcuts_dialog.quick_search = Rychlé vyhled\u00E1ní shortcuts_dialog.quick_search.start_search = Vepsáním libovolného znaku zahájíte rychlé hledání shortcuts_dialog.quick_search.cancel_search = Ukončit rychlé hledání shortcuts_dialog.quick_search.remove_last_char = Odstranit z hledaného řetězce poslední znak shortcuts_dialog.quick_search.jump_to_previous = Přejít na předchozí výsledek hledání shortcuts_dialog.quick_search.jump_to_next = Přejít na následující výsledek hledání shortcuts_dialog.quick_search.mark_jump_next = Označit/odznačit stávající soubor a přejít na následující výsledek hledání theme_editor.title = Editor motivů theme_editor.folder_tab = Panely souborů theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Historie shellu theme_editor.terminal_tab = Terminál theme_editor.statusbar_tab = Stavový řádek theme_editor.free_space = Volné místo theme_editor.free_space.ok = OK theme_editor.free_space.warning = Upozornění theme_editor.free_space.critical = Kritické theme_editor.locationbar_tab = Adresní řádek theme_editor.editor_tab = Editor souborů theme_editor.font = Písmo theme_editor.active_panel = Aktivní theme_editor.inactive_panel = Neaktivní theme_editor.general = Obecné theme_editor.theme_warning_predefined=Vestav\u011Bn\u00E9 motivy nelze m\u011Bnit. Chcete vytvo\u0159it nov\u00FD motiv? theme_editor.could_not_save_theme = Motiv „%1“ nelze uložit motiv theme_editor.border = Ohraničení theme_editor.background = Pozadí theme_editor.alternate_background = Alternativní pozadí theme_editor.unfocused_background = Pozadí (bez fokusu) theme_editor.quick_search = Rychlé vyhledávání theme_editor.quick_search.unmatched_file = Nevyhovující soubor theme_editor.text = Text theme_editor.progress = Průběh theme_editor.normal = Normální theme_editor.normal_unfocused = Normální (bez fokusu) theme_editor.selected = Výběr theme_editor.selected_unfocused = Výběr (bez fokusu) theme_editor.color = Barva theme_editor.colors = Barvy theme_editor.plain_file = Běžný soubor theme_editor.marked_file = Označený soubor theme_editor.hidden_file = Skrytý soubor theme_editor.folder = Složka theme_editor.archive_file = Archív theme_editor.symbolic_link = Symbolický odkaz theme_editor.executable_file = Spustitelný soubor theme_editor.header = Záhlaví theme_editor.current = Řádek theme_editor.file_groups = Skupiny souborů theme_editor.group_ = Skupina theme_editor.normal_color = Normální barva theme_editor.selected_color = Barva výběru theme_editor.filemask = Souborové masky theme_editor.group_file_ = Sobor skupiny theme_editor.item = Položka theme_editor.text_editor_tab=Prohl\u00ED\u017Ee\u010D a editor textu theme_editor.hex_viewer_tab=Prohl\u00ED\u017Ee\u010D dat theme_editor.normal_hex=Hex theme_editor.normal_offset=Offset theme_editor.normal_ascii=ASCII theme_editor.alternate=Alternativn\u00ED theme_editor.selected_hex=Vybran\u00FD bin\u00E1rn\u00ED theme_editor.selected_ascii=Vybran\u00FD ASCII command_bar_customize_dialog.available_actions = Dostupné akce command_bar_customize_dialog.modifier = Modifikátor prefs_dialog.title = Nastavení prefs_dialog.general_tab = Obecné prefs_dialog.day = Den prefs_dialog.month = Měsíc prefs_dialog.year = Rok prefs_dialog.language = Jazyk (vyžaduje restart) prefs_dialog.date_time = Formát datumu a času prefs_dialog.time = Čas prefs_dialog.date = Datum prefs_dialog.date_separator = Oddělovač prefs_dialog.time_12_hour = 12-hodinový formát prefs_dialog.time_24_hour = 24-hodinový formát prefs_dialog.show_seconds = Zobrazit sekundy prefs_dialog.show_century = Zobrazit století prefs_dialog.check_for_updates_on_startup = Při spuštění hledat aktualizace prefs_dialog.show_splash_screen = Zobrazit uvítací obrázek prefs_dialog.folders_tab = Složky prefs_dialog.startup_folders = Počáteční složky prefs_dialog.left_folder = Složka vlevo prefs_dialog.right_folder = Složka vpravo prefs_dialog.last_folder = Poslední navštívená složka prefs_dialog.custom_folder = Vlastní složka prefs_dialog.quick_search=Rychl\u00E9 hled\u00E1n\u00ED prefs_dialog.quick_search_timeout=Zastavit rychl\u00E9 hled\u00E1n\u00ED po neaktivit\u011B prefs_dialog.quick_search_timeout_never=Nikdy prefs_dialog.quick_search_timeout_sec=Sek prefs_dialog.show_quick_search_matches_first=Zobrazit shodn\u00E9 prvn\u00ED prefs_dialog.show_hidden_files = Zobrazit skryté soubory prefs_dialog.show_ds_store_files = Zobrazit soubory .DS_Store prefs_dialog.show_system_folders = Zobrazit systémové složky prefs_dialog.compact_file_size = Zaokrouhlit zobrazenou velikost souborů prefs_dialog.follow_symlinks_when_cd = Následovat symbolické odkazy při změnách složky prefs_dialog.show_tab_header = Vždy zobrazovat hlavičku panelu prefs_dialog.appearance_tab = Vzhled prefs_dialog.look_and_feel = Vzhled a chování prefs_dialog.icons_size = Velikost ikon prefs_dialog.toolbar_icons = Tlačítková lišta prefs_dialog.command_bar_icons = Lišta příkazů prefs_dialog.file_icons = Typy souborů prefs_dialog.use_system_file_icons = Použít systémové ikony prefs_dialog.use_system_file_icons.always = Vždy prefs_dialog.use_system_file_icons.never = Nikdy prefs_dialog.use_system_file_icons.applications = Jen u aplikací prefs_dialog.edit_current_theme = Změnit stávající motiv\u2026 prefs_dialog.themes = Motivy prefs_dialog.syntax_themes = Editor motivu zvýraznění prefs_dialog.import_theme = Importovat motiv prefs_dialog.import_look_and_feel = Importovat Vzhled a chování prefs_dialog.no_look_and_feel = Žádný Vzhled a chování nebyl nalezen. prefs_dialog.error_in_import = Chyba při importu motivu „%1“. prefs_dialog.cannot_read_theme = Nelze načíst motiv ze souboru „%1“ prefs_dialog.export_theme = Exportovat „%1“ prefs_dialog.import = Importovat prefs_dialog.export = Exportovat prefs_dialog.theme_type = Typ: %1 prefs_dialog.delete_theme = Chcete trvale smazat motiv „%1“? prefs_dialog.delete_look_and_feel = Chcete trvale smazat Vzhled a chování „%1“? prefs_dialog.rename_failed = Nepodařilo se přejmenovat motiv „%1“ prefs_dialog.xml_file = Soubor XML prefs_dialog.jar_file = Soubor JAR prefs_dialog.mail_tab = E-mail prefs_dialog.mail_settings = Nastavení odchozí pošty prefs_dialog.mail_name = Vaše jméno prefs_dialog.mail_address = Váš e-mail prefs_dialog.mail_server = Server SMTP prefs_dialog.misc_tab = Různé prefs_dialog.use_brushed_metal = Použít vzhled „brushed metal“ (vyžaduje restart) prefs_dialog.confirm_on_quit = Při ukončení zobrazovat potvrzovací dialog prefs_dialog.shell = Spustit příkaz prefs_dialog.default_shell = Použít výchozí systémový shell prefs_dialog.custom_shell = Použít vlastní chell prefs_dialog.shell_encoding = Kódování textu shellu prefs_dialog.auto_detect_shell_encoding = Automatické rozpoznání prefs_dialog.external_terminal = Externí terminál prefs_dialog.default_terminal = Použít výchozí terminál prefs_dialog.custom_terminal = Použít vlastní příkaz prefs_dialog.builtin_terminal = Vestavěný terminál prefs_dialog.default_shell = Použít výchozí shell prefs_dialog.custom_shell = Použít vlastní příkaz prefs_dialog.enable_bonjour_discovery = Povolit služby Bonjour discovery prefs_dialog.enable_system_notifications = Zobrazovat oznámení systému prefs_dialog.calculate_folder_size_on_mark = Vypočítat velikost složky po označení debug_console_dialog.level = Úroveň záznamu debug_console_dialog.threads = Vlákna debug_console_dialog.active_threads=Aktivn\u00ED vl\u00E1kna unit.byte = bajt unit.bytes = bajtů unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1 sek. duration.minutes = %1 min. duration.hours = %1 hod. duration.days = %1 d duration.months = %1 měs. duration.years = %1 let duration.infinite = ∞ theme.custom_theme = Vlastní motiv theme.custom = Vlastní theme.built_in = Vestavěné theme.add_on = Doplněk theme.current = stávající theme_could_not_be_loaded = Chyba při zavádění motivu. cannot_open_cyclic_symlink = Nelze otevřít zvolený odkaz, neboť odkazuje sám na sebe setup.title = Vítá Vás trolCommander setup.intro = Prosím, zvolte vlastní nastavení aplikace trolCommander. setup.look_and_feel = Vyberte si Vzhled a chování setup.theme = Vyberte si motiv font_chooser.font_size = Velikost font_chooser.font_bold = Tučné font_chooser.font_italic = Kurzíva color_chooser.red = Červená color_chooser.green = Zelená color_chooser.blue = Modrá color_chooser.hue = Odstín color_chooser.brightness = Jas color_chooser.swatches = Barevný mixér color_chooser.saturation = Sytost color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Naposledy použité color_chooser.alpha = Průhlednost alfa color_chooser.title = Vyberte barvu batch_rename_dialog.mask = Maska pro přejmenování batch_rename_dialog.search_replace = Najít a nahradit batch_rename_dialog.search_for = Hledaný výraz batch_rename_dialog.replace_with = Bude nahrazen batch_rename_dialog.counter = Počítadlo batch_rename_dialog.start_at = Počítat od batch_rename_dialog.step_by = Přírůstek batch_rename_dialog.format = Formát batch_rename_dialog.upper_lower_case = Velikost písmen batch_rename_dialog.no_change = Beze změny batch_rename_dialog.lower_case = malá písmena batch_rename_dialog.upper_case = VELKÁ PÍSMENA batch_rename_dialog.first_upper = První písmeno velké batch_rename_dialog.word = První Písmena Velká batch_rename_dialog.regexp = RegExp batch_rename_dialog.regexp_error = Chyba syntaxe RegExp batch_rename_dialog.old_name = Původní jméno batch_rename_dialog.new_name = Nové jméno batch_rename_dialog.block_name = Zachovat batch_rename_dialog.range = Rozsah batch_rename_dialog.proceed_renaming = %1 z %2 souborů bude přejmenováno. Chcete pokračovat? batch_rename_dialog.duplicate_names = Duplicitní jméno! batch_rename_dialog.names_conflict = Konflikt jmen! Stejná hodnota pro staré a nové jméno. parent_folders_quick_list.empty_message = Stávající umístění nemá nadřazenou složku recent_locations_quick_list.empty_message = Dříve navštívená umístění nebyla nalezena recent_executed_files_quick_list.empty_message = Dříve spuštěné soubory nebyly nalezeny recent_edited_files_quick_list.empty_message = Žádné dříve editované soubory recent_viewed_files_quick_list.empty_message = Žádné dříve zobrazované soubory roots_quick_list.empty_message = Žádné kořenové složky tabs_quick_list.empty_message = Je dostupný pouze jeden panel editor_bookmarks_quick_list.empty_message=\u017D\u00E1dn\u00E9 soubory #server_connect_dialog.auth_error = Nesprávné uživatelské jméno nebo heslo. #move_dialog.cannot_move_to_itself = Nemůžu přesunout soubory do podsložky. #pack.error_on_file = Chyba při kompresi %1 #pack_dialog.cannot_write = V cílové složce nelze vytvořit soubor. #unpack.unable_to_open_zip = Chyba při otevírání souboru %1. #image_viewer.previous_image = Předchozí obrázek #image_viewer.next_image = Následující obrázek #mkdir_dialog.title = Vytvořit složku #mkdir_dialog.error_title = Chyba při vytváření složky #edit_bookmarks_dialog.remove = Odstranit #mkdir_dialog.description = Vytvořit složku #mkfile_dialog.description = Vytvořit nový prázdný soubor #done = Hotovo #progress_dialog.hide = Skrýt #move_dialog.rename_description = Přejmenovat soubor na #theme_editor.shell_font = Písmo shellu #theme_editor.history_font = Písmo historie #theme_editor.shell_colors = Barvy shellu #theme_editor.history_colors = Barvy historie #ToggleHiddenFiles.hide = Skrýt skryté soubory #auth_dialog.error_was = Chyba: %1 #table.hide_column = Skrýt sloupec #delete.symlink_warning_title = Nalezen symbolický odkaz #delete.symlink_warning = Tento soubor vypadá jako symbolický odkaz:\n\n Soubor: %1\n odkazujuje na: %2\n\nChcete smazat jen odkaz nebo\ni cíl odkazu (UPOZORNĚNÍ) ? #delete.delete_link_only = Smazat odkaz #delete.delete_linked_folder = Smazat cíl odkazu #Unpack.label = Dekomprimovat #Pack.label = Komprimovat find_dialog.name = Jméno souboru find_dialog.contains = Obsahuje find_dialog.initial_directory = Začíná na find_dialog.search_subdirectories = Prohledávat podsložky find_dialog.search_archives = Prohledávat archívy find_dialog.case_sensitive = Rozlišovat velikost písmen find_dialog.ignore_hidden = Ignorovat skryté find_dialog.search_results = Výsledky hledání find_dialog.found = Nalezené soubory find_dialog.encoding = Kódování textu find_dialog.search_hex = Vyhledávat binárně image_viewer.next_image = Další obrázek image_viewer.previous_image = Předchozí obrázek hex_viewer.offset = Posun hex_viewer.ascii_dump = Výpis ASCII hex_viewer.view = Zobrazit hex_viewer.goto = Skočit na hex_viewer.goto.offset = Posun hex_viewer.search = Hledat hex_viewer.searchNext = Najit další hex_viewer.searchPrev = Najit předchozí hex_viewer.find = Najít hex_view.text = Hledat hex_viewer.hex = Hex hex_viewer.search_not_found = Vzor nenalezen calculator.calculator = Kalkulačka calculator.expression = Výraz calculator.error = Chyba ve výrazu replication=Faktor replikace blocksize=Velikost bloku ChangeReplication.label=Zm\u011Bnit replikaci replication.number=Faktor replikace adb.android_devices=Android adb.no_devices=\u017D\u00E1dn\u00E9 za\u0159\u00EDzen\u00ED eject.no_mounted_devices=\u017D\u00E1dn\u00E9 p\u0159ipojen\u00E9 za\u0159\u00EDzen\u00ED ================================================ FILE: src/main/resources/dictionary_da_DA.properties ================================================ ok = OK yes = Ja no = Nej cancel = Annuller edit = Rediger close = Luk reset = Nulstil rename = Omdøb apply = Anvend change = Ændre save = Gem dont_save = Gem ikke replace = Erstat dont_replace = Erstat ikke delete = Slet skip = Spring over skip_all = Spring over alle retry = Forsøg igen resume = Forsæt overwrite = Overskriv overwrite_if_older = Overskriv hvis ældre duplicate = Dupliker apply_to_all = Anvend på alle copy = Kopier move = Flyt pack = Komprimer unpack = Udpak download = Hent split = Del combine = Sammenflet browse = Gennemse ask = Spørg stop = Stop pause = Pause quick_search = Hurtig søgning file_manager = Filhåndterer create = Opret creating_file = Opret %1 choose = Vælg customize = Tilpas choose_folder = Vælg mappe login = Brugernavn password = Kodeord user = Ejer encoding = Tegnkodning preferred_encodings = Foretrukket indkodning license = Licens name = Navn size = Størrelse date = Dato extension = Filtype permissions = Rettigheder owner = Ejer group = Gruppe location = Placering untitled = Unavngivet source = Kilde destination = Destination recurse_directories = Behandel markerede mapper rekursivt go_to = Gå til example = Eksempel preview = Eksempel comment = Kommentar sample_text = Eksempel på tekst nb_files = %1 fil(er) nb_folders = %1 mappe(r) loading = Indlæser... this_operation_cannot_be_undone = Du kan ikke fortryde denne handling. remove = Fjern details = Detaljer warning = Advarsel error = Fejl generic_error = En fejl opstod under den ønskede handling folder_does_not_exist = Mappen findes ikke eller er ikke tilgængelig this_folder_does_not_exist = Mappen findes ikke eller er ikke tilgængelig: %1 this_file_does_not_exist = Filen findes ikke eller er ikke tilgængelig: %1 invalid_path = Ugyldig sti: %1 directory_already_exists = Mappen %1 findes allerede. file_exists_in_destination = Filen findes allerede på denne lokation. source_parent_of_destination = Forsøg på at flytte mappen til en af dens undermapper cannot_read_file = Kan ikke læse filen %1 cannot_write_file = Kan ikke skrive til filen %1 cannot_create_folder = Kan ikke oprette mappen %1 cannot_read_folder = Kan ikke læse indholdet af mappen %1 cannot_delete_file = Kan ikke slette filen %1 cannot_delete_folder = Kan ikke slette mappen %1 error_while_transferring = Fejl ved overførsel af filen %1 same_source_destination = Kilden og destinationen er den samme file_already_exists = %1 findes allerede, vil du erstatte den? write_error = Skrivefejl read_error = Læsefejl integrity_check_error = Integritetstjek fejlede: kilden og destinationen stemmer ikke overens startup_error = En fejl forhindrede trolCommander fra at starte. permissions.read = Læsbar permissions.write = Skrivbar permissions.executable = Eksekverbar permissions.group = Gruppe permissions.other = Andre permissions.octal_notation = Oktal notation action_categories.all = Alle action_categories.navigation = Navigation action_categories.selection = Markering action_categories.view = Vis action_categories.file_operations = Fil handlinger action_categories.windows = Vinduer Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Nyt bogmærke AddBookmark.tooltip = Tilføj den nuværende mappe til bogmærker BatchRename.label = Multi omdøbning EditBookmarks.label = Rediger bogmærker ExploreBookmarks.label = Vis bogmærker EditCredentials.label = Rediger akkreditiver ChangeLocation.label = Ændre den nuværende sti ChangeDate.label = Ændre dato ChangeDate.tooltip = Ændre dato på markered(e) fil(er) ChangePermissions.label = Ændre rettigheder ChangePermissions.tooltip = Ændre rettigheder på markered(e) fil(er) CheckForUpdates.label = Søg efter opdateringer CompareFolders.label = Sammenlign mapper ConnectToServer.label = Forbind til server ConnectToServer.tooltip = Forbind til en fjernserver View.label = Vis InternalView.label = Vis (indbygget) View.tooltip = Vis markerede fil InternalEdit.label = Rediger (indbygget) Edit.tooltip = Rediger markerede fil Copy.tooltip = Kopier markerede filer LocalCopy.label = Lokal kopi LocalCopy.tooltip = Kopier markeret fil til den aktuelle mappe Move.tooltip = Flyt markerede filer Rename.tooltip = Omdøb markeret fil Mkdir.label = Ny mappe Mkdir.tooltip = Opret en ny mappe i den aktuelle mappe Mkfile.label = Ny fil Mkfile.tooltip = Opret en ny fil i den aktuelle mappe Delete.tooltip = Slet markerede filer via papirkurven når det er muligt PermanentDelete.label = Slet permanent PermanentDelete.tooltip = Slet markerede filer uden at bruge papirkurven Refresh.label = Opdater Refresh.tooltip = Opdater aktuelle mappe CloseWindow.label = Luk vindue CloseWindow.tooltip = Luk dette vindue CopyFileNames.label = Kopier navn(e) CopyFilePaths.label = Kopier sti(er) CopyFilesToClipboard.label = Kopier fil(er) PasteClipboardFiles.label = Indsæt fil(er) Email.label = E-mail filer Email.tooltip = Send de markerede filer i en mail GoBack.label = Tilbage GoBack.tooltip = Foregående mappe GoForward.label = Frem GoForward.tooltip = Næste mappe GoToHome.label = Hjemmemappe GoToParent.label = Forrige GoToParent.tooltip = Til forrige mappe GoToParentInOtherPanel.label = Gå til forrige mappe i modsatte panel GoToParentInBothPanels.label = Gå til forrige mappe i begge paneler GoToRoot.label = Gå til roden SortByName.label = Sorter efter navn SortByDate.label = Sorter efter dato SortBySize.label = Sorter efter størrelse SortByExtension.label = Sorter efter filendelse SortByPermissions.label = Sorter efter rettigheder SortByOwner.label = Sorter efter ejer SortByGroup.label = Sorter efter gruppe MarkGroup.label = Marker filer MarkGroup.tooltip = Marker en gruppe filer UnmarkGroup.label = Afmarker filer UnmarkGroup.tooltip = Afmarker en gruppe filer MarkAll.label = Marker alt UnmarkAll.label = Afmarker alt MarkSelectedFile.label = Marker/afmarker MarkSelectedFile.tooltip = Marker/afmarker valgte fil MarkNextBlock.label = Marker en blok ned MarkPreviousBlock.label = Marker en blok op MarkNextRow.label = Marker en række ned MarkPreviousRow.label = Marker en række op MarkNextPage.label = Marker side ned MarkPreviousPage.label = Marker side op MarkToFirstRow.label = Marker filer til begyndelsen MarkToLastRow.label = Marker filer til enden MarkExtension.label = Marker filendelse InvertSelection.label = Omvend markering SwapFolders.label = Ombyt mapper SwapFolders.tooltip = Ombyt venstre og højre panel SetSameFolder.label = Vis samme mappe SetSameFolder.tooltip = Vis samme mappe i venstre og højre panel NewWindow.label = Nyt vindue NewWindow.tooltip = Åben et nyt vindue Open.label = Åben Open.tooltip = Åben mappe / Åben arkiv / Kør OpenNatively.label = Åben normalt OpenNatively.tooltip = Åben markeret fil med systemets predefinerede filtilknytning OpenInOtherPanel.label = Åben i andet panel OpenInBothPanels.label = Åben i begge paneler RevealInDesktop.label = Vis i %1 RunCommand.label = Kør kommando RunCommand.tooltip = Kør en kommando i aktuel mappe Pack.tooltip = Komprimer markerede filer til et arkiv Unpack.tooltip = Udpak markerede filarkiver ShowFileProperties.label = Egenskaber ShowFileProperties.tooltip = Vis egenskaber for markerede filer ShowPreferences.label = Indstillinger ShowPreferences.tooltip = Konfigurer trolCommander ShowServerConnections.label = Vis åbne forbindelser Quit.label = Afslut ReverseSortOrder.label = Omvend sortering ToggleAutoSize.label = Automatisk kolonne bredde Stop.label = Stop ombytning af mapper ToggleColumn.show = Vis %1 kolonne ToggleColumn.hide = Skjul %1 kolonne ToggleCommandBar.show = Vis kommandolinien ToggleCommandBar.hide = Skjul kommandolinien ToggleToolBar.show = Vis værktøjslinie ToggleToolBar.hide = Skjul værktøjslinie CustomizeCommandBar.label = Tilpas kommandolinien ToggleStatusBar.show = Vis statuslinie ToggleStatusBar.hide = Skjul statuslinie ToggleShowFoldersFirst.label = Vis mapper først ToggleTree.label = Træ visning PopupLeftDriveButton.label = Ændre venstre mappe PopupRightDriveButton.label = Ændre højre mappe RecallPreviousWindow.label = Fremkald foregående vindue RecallNextWindow.label = Fremkald næste vindue RecallWindow.label = Fremkald vindue #%1 BringAllToFront.label = Læg alle øverst SwitchActiveTable.label = Skift i mellem venstre og højre panel SelectNextBlock.label = Spring en blok ned SelectPreviousBlock.label = Spring en blok op SelectNextPage.label = Spring en side ned SelectPreviousPage.label = Spring en side op SelectNextRow.label = Spring en række ned SelectPreviousRow.label = Spring en række op SelectFirstRow.label = Vælg første fil i aktuel mappe SelectLastRow.label = Vælg sidste fil i aktuel mappe SplitEqually.label = Del ligeligt SplitVertically.label = Del lodret SplitHorizontally.label = Del vandret ShowKeyboardShortcuts.label = Tastatur genveje GoToWebsite.label = Gå til hjemmeside GoToForums.label = Gå til forum ReportBug.label = Rapporter en fejl Donate.label = Doner ShowAbout.label = Om trolCommander OpenTrash.label = Åben papirkurv EmptyTrash.label = Tøm papirkurv CalculateChecksum.label = Udregn kontrolsum MaximizeWindow.label = Maksimer MinimizeWindow.label = Minimer GoToDocumentation.label = Online dokumentation ShowParentFoldersQL.label = Ovenliggende mapper til den nuværende placering ShowRecentLocationsQL.label = Nyeligt tilgåede placeringer ShowRecentExecutedFilesQL.label = Nyeligt kørte filer SplitFile.tooltip = Del en fil op i flere dele CombineFiles.tooltip = Sammenflet delte filer for at genoprette de oprindelige filer ShowDebugConsole.label = Fejlsøgningskonsol FocusPrevious.label = Fokuser på tidligere komponent FocusNext.label = Fokuser på næste komponent file_menu = Filer file_menu.open_with = Åben med mark_menu = Marker view_menu = Vis view_menu.show_hide_columns = Vis/skjul kolonner go_menu = Gå bookmarks_menu = Bogmærker bookmarks_menu.no_bookmark = Ingen bogmærker quick_lists_menu = Hurtig lister window_menu = Vindue help_menu = Hjælp status_bar.selected_files = %1 af %2 markerede status_bar.connecting_to_folder = Forbinder til mappe, tryk på ESCAPE for at annullere. status_bar.volume_free = Ledigt: %1 status_bar.volume_capacity = Kapacitet: %1 shortcuts_panel.title = Genveje shortcuts_panel.restore_defaults = Standard indstillinger shortcuts_panel.show = Vis shortcuts_panel.default_message = Tryk Enter/dobbelt klik på den genvej du gerne vil redigere shortcuts_table.action_description = Handlingsbeskrivelse shortcuts_table.shortcut = Genvej shortcuts_table.alternate_shortcut = Alternativ genvej shortcuts_table.type_in_a_shortcut = Indtast en genvej command_bar_dialog.help = Træk knapper for at tilpasse kommandolinien table.folder_access_error_title = Fejl ved mappe adgang table.folder_access_error = Mappens indhold kunne ikke læses table.download_or_browse = Vil du gennemse eller hente denne fil? version_dialog.no_new_version_title = Der findes ingen ny version version_dialog.no_new_version = Tillykke, du har allerede den nyeste version. version_dialog.new_version_title = Ny version tilgængelig version_dialog.new_version = En ny version af trolCommander er tilgængelig. version_dialog.new_version_url = En ny version af trolCommander er tilgængelig på: %1. version_dialog.not_available_title = Serveren er ikke tilgængelig version_dialog.not_available = Det er ikke muligt at få versions information fra serveren. version_dialog.install_and_restart = Installer og genstart version_dialog.preparing_for_update = Forbereder opdatering... quit_dialog.title = Afslut trolCommander quit_dialog.desc = Du har %1 vinduer åbne. Er du sikker på du vil afslutte? quit_dialog.show_next_time = Vis næste gang destination_dialog.file_exists_action = Standardhandling når filen allerede findes destination_dialog.verify_integrity = Verificer data integritet destination_dialog.skip_errors = Ignorer fejl file_collision_dialog.title = Filen findes allerede rename_dialog.new_name = Nyt navn copy_dialog.destination = Kopier markerede fil(er) til copy_dialog.error_title = Kopieringsfejl copy_dialog.copying = Kopierer filer copy_dialog.copying_file = Kopierer %1 pack_dialog.packing = Komprimerer filer pack_dialog.packing_file = Komprimerer %1 pack_dialog.error_title = Komprimeringsfejl pack_dialog_description = Tilføj markerede filer til pack_dialog.archive_format = Arkivformat unpack_dialog.destination = Udpak markerede fil(er) til unpack_dialog.error_title = Udpakningsfejl unpack_dialog.unpacking = Udpakker filer unpack_dialog.unpacking_file = Udpakker %1 optimizing_archive = Optimerer arkiv %1 error_while_optimizing_archive = Fejl under optimering af arkiv %1 move_dialog.move_description = Flyt til move_dialog.error_title = Fejl under flytning move_dialog.moving = Flytter filer move_dialog.moving_file = Flytter %1 download_dialog.description = Hent fil til download_dialog.error_title = Fejl under hentning download_dialog.downloading = Henter download_dialog.downloading_file = Henter %1 mkfile_dialog.allocate_space = Alloker plads delete_dialog.permanently_delete.confirmation = Slet makerede fil(er) permanent? delete_dialog.move_to_trash.confirmation = Slet markerede fil(er)? delete_dialog.move_to_trash.confirmation_details = Filerne flyttes til papirkurven. delete_dialog.move_to_trash.option = Flyt til papirkurven delete_dialog.move_to_trash.failed = En eller flere filer kunne ikke flyttes til papirkurven. delete_dialog.deleting = Sletter delete_dialog.error_title = Fejl under sletning delete.deleting_file = Sletter %1 email_dialog.prefs_not_set_title = E-mail er ikke konfigureret email_dialog.prefs_not_set = Du skal indstille din e-mail konfiguration først. email_dialog.from = Fra email_dialog.to = Til email_dialog.subject = Emne email_dialog.send = Send email_dialog.error_title = Fejl under afsendelse af filer email_dialog.read_error = Filerne i undermapperne kunne ikke læses. email_dialog.sending = Sender filer email.sending_file = Sender %1 email.connecting_to_server = Forbinder til %1 email.server_unavailable = Kunne ikke kontakte mailserveren %1, kontroller dine e-mail indstillinger eller forsøg igen senere. email.connection_closed = Forbindelsen blev afsluttet af serveren, mailen er ikke sendt. email.goodbye_failed = Fejl under lukning af forbindelse, mailen blev måske ikke sendt. email.send_file_error = Kunne ikke sende filen %1, mailen er ikke sendt. split_file_dialog.error_title = Fejl under deling af fil split_file_dialog.file_to_split = Fil som skal deles split_file_dialog.target_directory = Destinations mappe split_file_dialog.part_size = Størrelse per del split_file_dialog.parts = Antal dele split_file_dialog.generate_CRC = Generer CRC fil split_file_dialog.max_parts = Maksimal antal dele er %1 split_file_dialog.auto = Automatisk split_file_dialog.insert_new_media = Indsæt nyt medie combine_files_dialog.error_title = Fejl under sammenfletning af filer combine_files_job.no_crc_file = Sammenfletning fuldført. Ingen CRC fil. combine_files_job.crc_read_error = Fejl under løsning af CRC fil. combine_files_job.crc_check_failed = CRC fejl: forventede %2 men fik %1 combine_files_job.crc_ok = Sammenfletning fuldført. CRC kontrolsum ok. file_selection_dialog.mark = Marker file_selection_dialog.unmark = Afmarker file_selection_dialog.mark_description = Marker filer hvis navn file_selection_dialog.unmark_description = Afmarker filer hvis navn file_selection_dialog.case_sensitive = Forskel på store og små bogstaver file_selection_dialog.include_folders = Inkluder mapper progress_dialog.starting = Starter overførsel... progress_dialog.transferred = Overført %1 med %2 progress_dialog.elapsed_time = Forløbet tid progress_dialog.advanced = Avanceret progress_dialog.current_speed = Aktuel hastighed progress_dialog.limit_speed = Begræns hastighed progress_dialog.close_when_finished = Luk vinduet når færdig progress_dialog.processing_files = Bearbejder filer progress_dialog.processing_file = Bearbejder %1 progress_dialog.verifying_file = Verificerer %1 progress_dialog.job_finished = Job færdigt progress_dialog.job_error = Fejl under job properties_dialog.file_properties = %1 Egenskaber properties_dialog.contents = Indeholder properties_dialog.calculating = Udregner... calculate_checksum_dialog.checksum_algorithm = Kontrolsum algoritme calculate_checksum_dialog.temporary_file = Midlertidig fil change_date_dialog.now = Nu change_date_dialog.specific_date = Angiv dato run_dialog.run_command_description = Kør i aktuel mappe run_dialog.run_in_home_description = Kør i hjemmemappen run_dialog.command_output = Kommando uddata run_dialog.run = Kør run_dialog.clear_history = Ryd historik server_connect_dialog.server_type = Tilslutningstype server_connect_dialog.server = Server server_connect_dialog.share = Del server_connect_dialog.domain = Domæne server_connect_dialog.username = Brugernavn server_connect_dialog.initial_dir = Startmappe server_connect_dialog.port = Port server_connect_dialog.server_url = Server adresse server_connect_dialog.http_url = Hjemmeside adresse server_connect_dialog.connect = Forbind server_connect_dialog.protocol = Protokol server_connect_dialog.nfs_version = NFS-version server_connect_dialog.private_key = Privat nøgle server_connect_dialog.passphrase = Løsen ftp_connect.passive_mode = Aktiver passiv tilstand ftp_connect.anonymous_user = Anonym bruger ftp_connect.nb_connection_retries = Antal gentagende forbindelses forsøg ftp_connect.retry_delay = Forsinkelse i mellem forsøg (i sekunder) http_connect.basic_authentication = HTTP basal godkendelse (valgfrit) server_connections_dialog.disconnect = Afbryd server_connections_dialog.connection_busy = Optaget server_connections_dialog.connection_idle = Inaktiv bonjour.bonjour_services = Bonjour tjenester bonjour.no_service_discovered = Ingen tjenester kunne findes bonjour.bonjour_disabled = Bonjour deaktiveret auth_dialog.title = Godkendelse auth_dialog.desc = Indtast venligst brugernavn og kodeord auth_dialog.server = Server auth_dialog.store_credentials = Husk brugernavn og kodeord (svag kryptering) auth_dialog.connect_as = Forbind som auth_dialog.authentication_failed = Godkendelse fejlet sortable_list.move_up = Flyt op sortable_list.move_down = Flyt ned add_bookmark_dialog.add = Tilføj edit_bookmarks_dialog.new = Ny file_viewer.view_error_title = Visningsfejl file_viewer.view_error = Filen kunne ikke vises. file_viewer.file_menu = Filer file_viewer.close = Luk file_viewer.large_file_warning = Filen kan være for stor til denne handling. file_viewer.open_anyway = Åben alligevel text_viewer.edit = Rediger text_viewer.copy = Kopier text_viewer.select_all = Marker alt text_viewer.find = Søg text_viewer.find_next = Find næste text_viewer.find_previous = Find forrige text_viewer.binary_file_warning = Dette lader til at være en binær fil image_viewer.controls_menu = Funktioner image_viewer.zoom_in = Zoom ind image_viewer.zoom_out = Zoom ud file_editor.edit_error_title = Redigeringsfejl file_editor.edit_error = Kunne ikke redigere filen. file_editor.save = Gem file_editor.save_as = Gem som... file_editor.save_warning = Gem ændringer til filen inden lukning? file_editor.cannot_write = Kunne ikke læse filen. text_editor.cut = Klip ud text_editor.paste = Sæt ind shortcuts_dialog.quick_search.start_search = Indtast et tegn for at starte en hurtig søgning shortcuts_dialog.quick_search.cancel_search = Annuller hurtig søgning shortcuts_dialog.quick_search.remove_last_char = Fjern det sidste tegn fra hurtig-søg strengen shortcuts_dialog.quick_search.jump_to_previous = Gå til forrige hurtig-søg resultat shortcuts_dialog.quick_search.jump_to_next = Gå til næste hurtig-søg resultat shortcuts_dialog.quick_search.mark_jump_next = Marker/afmarker aktuel fil og gå til næste søgeresultat theme_editor.title = Tema behandler theme_editor.folder_tab = Mappe panel theme_editor.shell_tab = Terminal theme_editor.shell_history_tab = Terminal historik theme_editor.statusbar_tab = Statuslinie theme_editor.free_space = Ledigt plads theme_editor.free_space.ok = OK theme_editor.free_space.warning = Advarsel theme_editor.free_space.critical = Kritisk theme_editor.locationbar_tab = Lokationslinie theme_editor.editor_tab = Fil behandler theme_editor.font = Skrifttype theme_editor.active_panel = Aktiv theme_editor.inactive_panel = Inaktiv theme_editor.general = Generelt theme_editor.could_not_save_theme = Kunne ikke gemme temaet %1 theme_editor.border = Kant theme_editor.background = Baggrund theme_editor.alternate_background = Alternativ baggrund theme_editor.unfocused_background = Baggrund (uden fokus) theme_editor.quick_search.unmatched_file = Filer som ikke matcher theme_editor.text = Tekst theme_editor.progress = Fremskridt theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (i fokus) theme_editor.selected = Markeret theme_editor.selected_unfocused = Markeret (uden fokus) theme_editor.color = Farve theme_editor.colors = Farver theme_editor.plain_file = Simple fil theme_editor.marked_file = Markeret fil theme_editor.hidden_file = Skjult fil theme_editor.folder = Mappe theme_editor.archive_file = Arkiv theme_editor.symbolic_link = Symbolsk henvisning theme_editor.header = Overskrift theme_editor.item = Element command_bar_customize_dialog.available_actions = Tilgængelige handlinger command_bar_customize_dialog.modifier = Ændrer prefs_dialog.title = Indstillinger prefs_dialog.general_tab = Generelt prefs_dialog.day = Dag prefs_dialog.month = Måned prefs_dialog.year = År prefs_dialog.language = Sprog (kræver genstart) prefs_dialog.date_time = Dato og tidsformat prefs_dialog.time = Tid prefs_dialog.date = Dato prefs_dialog.date_separator = Separator prefs_dialog.time_12_hour = 12-timers format prefs_dialog.time_24_hour = 24-timers format prefs_dialog.show_seconds = Vis sekunder prefs_dialog.show_century = Vis århundrede prefs_dialog.check_for_updates_on_startup = Tjek for opdateringer ved opstart prefs_dialog.show_splash_screen = Vis opstartsbillede prefs_dialog.folders_tab = Mapper prefs_dialog.startup_folders = Startmappe prefs_dialog.left_folder = Venstre mappe prefs_dialog.right_folder = Højre mappe prefs_dialog.last_folder = Sidst åbne mappe prefs_dialog.custom_folder = Fast mappe prefs_dialog.show_hidden_files = Vis skjulte filer prefs_dialog.show_ds_store_files = Vis .DS_Store filer prefs_dialog.show_system_folders = Vis system mapper prefs_dialog.compact_file_size = Afrund filstørrelser prefs_dialog.follow_symlinks_when_cd = Følg symbolske henvisninger ved ændring af aktuel mappe prefs_dialog.appearance_tab = Udseende prefs_dialog.look_and_feel = Udseende og følelse prefs_dialog.icons_size = Ikonstørrelse prefs_dialog.toolbar_icons = Værktøjslinie prefs_dialog.command_bar_icons = Kommandolinie prefs_dialog.file_icons = Filtyper prefs_dialog.use_system_file_icons = Brug systemets filikoner prefs_dialog.use_system_file_icons.always = Altid prefs_dialog.use_system_file_icons.never = Aldrig prefs_dialog.use_system_file_icons.applications = Kun til programfiler prefs_dialog.edit_current_theme = Rediger aktuelt tema... prefs_dialog.themes = Temaer prefs_dialog.import_theme = Importer tema prefs_dialog.import_look_and_feel = Importer udseende (udseende og følelse) prefs_dialog.no_look_and_feel = Intet udseende blev fundet. prefs_dialog.error_in_import = Fejl ved import af temaet %1. prefs_dialog.cannot_read_theme = Kunne ikke hente temaet fra filen %1 prefs_dialog.export_theme = Eksporter %1 prefs_dialog.import = Importer prefs_dialog.export = Eksporter prefs_dialog.theme_type = Type: %1 prefs_dialog.delete_theme = Slet temaet %1 ? prefs_dialog.delete_look_and_feel = Vil du permanent slette udseenet %1 ? prefs_dialog.rename_failed = Kunne ikke omdøbe temaet %1 prefs_dialog.xml_file = XML-fil prefs_dialog.jar_file = JAR-fil prefs_dialog.mail_tab = E-mail prefs_dialog.mail_settings = Udgående e-mail indstillinger prefs_dialog.mail_name = Dit navn prefs_dialog.mail_address = Din e-mail adresse prefs_dialog.mail_server = SMTP-server prefs_dialog.misc_tab = Diverse prefs_dialog.use_brushed_metal = Anvend udseenet 'børstet metal' (kræver genstart) prefs_dialog.confirm_on_quit = Spørg efter bekræftelse ved afslutning prefs_dialog.default_shell = Anvend systemets standard kommandofortolker prefs_dialog.custom_shell = Anvend anden kommandofortolker prefs_dialog.shell_encoding = Kommandofortolker indkodning prefs_dialog.auto_detect_shell_encoding = Vælg automatisk prefs_dialog.enable_bonjour_discovery = Aktiver opdagelse af Bonjour tjenester prefs_dialog.enable_system_notifications = Aktiver system meddelelser debug_console_dialog.level = Niveau unit.byte = byte unit.bytes = bytes unit.bytes_short = B unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/sek duration.seconds = %1s duration.minutes = %1m duration.hours = %1t duration.days = %1d duration.months = %1mån duration.years = %1år theme.custom_theme = Tilpasset tema theme.custom = Tilpasset theme.built_in = Indbygget theme.add_on = Tilføjelse theme.current = Aktuel theme_could_not_be_loaded = En fejl opstod ved indlæsning af dette tema. setup.title = Velkommen til trolCommander setup.intro = Vælg venligst hvordan du vil have trolCommander til at optræde. setup.look_and_feel = Vælg udseende og følelse setup.theme = Vælg tema font_chooser.font_size = Størrelse font_chooser.font_bold = Fed font_chooser.font_italic = Kursiv color_chooser.red = Rød color_chooser.green = Grøn color_chooser.blue = Blå color_chooser.hue = Tone color_chooser.brightness = Intensitet color_chooser.swatches = Farveprøver color_chooser.saturation = Mætning color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Seneste color_chooser.alpha = Alpha-gennemsigtig color_chooser.title = Vælg en farve batch_rename_dialog.mask = Maske batch_rename_dialog.search_replace = Søg og erstat batch_rename_dialog.search_for = Søg efter batch_rename_dialog.replace_with = Erstat med batch_rename_dialog.counter = Tæller batch_rename_dialog.start_at = Start på batch_rename_dialog.step_by = Trin batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Store/små bogstaver batch_rename_dialog.no_change = Uændret batch_rename_dialog.lower_case = små bogstaver batch_rename_dialog.upper_case = STORE BOGSTAVER batch_rename_dialog.first_upper = Første bogstaver stort batch_rename_dialog.word = Første Af Hvert Ord batch_rename_dialog.old_name = Gammelt navn batch_rename_dialog.new_name = Nyt navn batch_rename_dialog.block_name = Gruppe batch_rename_dialog.range = Område batch_rename_dialog.proceed_renaming = %1 filer ud af %2 vil blive omdøbt. Ønsker du at forsætte? batch_rename_dialog.duplicate_names = Duplikerede navne! parent_folders_quick_list.empty_message = Der er ikke nogle ovenliggende mapper til denne placering recent_locations_quick_list.empty_message = Der er ingen nyeligt tilgåede placeringer recent_executed_files_quick_list.empty_message = Ingen nyeligt kørte filer #auth_dialog.error_was = Fejlen var: %1 #table.hide_column = Skjul kolonne #delete.symlink_warning_title = Symbolsk henvisning fundet #delete.symlink_warning = Filen ligner en symbolsk henvisning:\n\n  Fil: %1\n  Henviser til: %2\n\nSlet kun henvisningen eller\nfølg henvisningen og slet mappen (ADVARSEL)? #delete.delete_link_only = Slet henvisning #delete.delete_linked_folder = Slet mappe #Unpack.label = Udpak filer #Pack.label = Komprimer filer ================================================ FILE: src/main/resources/dictionary_de_DE.properties ================================================ ok = OK yes = Ja no = Nein cancel = Abbrechen edit = Bearbeiten close = Schließen reset = Zurücksetzen rename = Umbenennen apply = Auf alle anwenden change = Verändern save = Speichern dont_save = Nicht speichern replace = Ersetzen dont_replace = Nicht ersetzen delete = Löschen skip = Überspringen skip_all = Alle Überspringen overwrite_all = Alles überschreiben retry = Erneut versuchen resume = Weiter overwrite = Überschreiben overwrite_if_older = Ältere überschreiben duplicate = Duplizieren apply_to_all = Zuweisen an alle copy = Kopieren move = Verschieben pack = Packen unpack = Entpacken download = Download split = Aufteilen combine = Zusammenfügen browse = Browsen ask = Fragen stop = Stopp pause = Pause quick_search = Schnellsuche file_manager = Dateimanager create = Erstellen creating_file = Erzeugen %1 choose = Auswählen customize = Anpassen clean = Säubern search = Suchen choose_folder = Wähle ein Verzeichnis login = Login password = Passwort user = Benutzer encoding = Kodierung preferred_encodings = Bevorzugte Kodierungen license = Lizenz name = Name size = Größe date = Datum extension = Dateierweiterung permissions = Zugriffsrechte owner = Eigentümer group = Gruppe location = Ort untitled = Unbenannt source = Quelle destination = Ziel recurse_directories = Auf Unterverzeichnisse anwenden go_to = Gehe zu example = Beispiel preview = Vorschau comment = Kommentar sample_text = Beispieltext nb_files = %1 Datei(en) nb_folders = %1 Ordner loading = lade... this_operation_cannot_be_undone = Diese Operation kann nicht rückgängig gemacht werden. remove = Entfernen details = Details title = Titel warning = Warnung error = Fehler generic_error = Während der angeforderten Operation ist ein Fehler aufgetreten folder_does_not_exist = Dieser Ordner existiert nicht oder ist nicht verfügbar. this_folder_does_not_exist = Dieser Ordner existiert nicht oder ist nicht verfügbar: %1 this_file_does_not_exist = Die Datei existiert nicht oder ist nicht verfügbar: %1 invalid_path = Ungültiger Pfad: %1 directory_already_exists = Ordner %1 existiert bereits. file_exists_in_destination = Datei existiert schon am Ziel source_parent_of_destination = Versuch ein Verzeichnis in ein Unterverzeichnis zu übertragen cannot_read_file = Kann Datei %1 nicht lesen cannot_write_file = Kann Datei %1 nicht schreiben overwrite_readonly_file = Die Datei ist schreibgeschützt: %1 cannot_create_folder = Kann Ordner %1 nicht erstellen cannot_read_folder = Kann Inhalt des Ordners %1 nicht lesen cannot_delete_file = Kann Datei %1 nicht löschen cannot_delete_folder = Kann Ordner %1 nicht löschen error_while_transferring = Fehler beim Transfer von Datei %1 same_source_destination = Quell-und Zielordner sind gleich file_already_exists = %1 existiert bereits, soll sie überschrieben werden ? write_error = Schreibfehler read_error = Lesefehler integrity_check_error = Integritätstest fehlgeschlagen: Quelle und Ziel stimmen nicht überein startup_error = Ein Fehler hat trolCommander am Start gehindert. image_size = Bildgröße cannot_write_symlink = Kann Symlink nicht schreiben: %1 cannot_write_symlink_already_exists = Symlink ist schon vorhanden: %1 cannot_write_symlink_access_denied = Kann Symlink %1 nicht schreiben, Zugriff verweigert permissions.read = Lesen permissions.write = Schreiben permissions.executable = Ausführen permissions.user = Benutzer permissions.group = Gruppe permissions.other = Andere permissions.octal_notation = Oktale Schreibweise action_categories.all = Alle action_categories.navigation = Navigation action_categories.selection = Auswahl action_categories.view = Ansicht action_categories.file_operations = Dateioperationen action_categories.windows = Fenster action_categories.tabs = Tabs action_categories.commands = Benuzterbefehle action_categories.misc = Sonstiges Terminal.label = Terminal Terminal.tooltip = Terminal TerminalPanel.label = Terminal TerminalPanel.tooltip = Terminalfenster anzeigen/verbergen FindFile.label = Datei suchen FindFile.tooltip = Datei suchen AddBookmark.label = Lesezeichen hinzufügen AddBookmark.tooltip = Füge den aktuellen Ordner zur Liste der Lesezeichen hinzu BatchRename.label = Mehrfach Umbenennung EditBookmarks.label = Lesezeichen bearbeiten ExploreBookmarks.label = Untersuche Lesezeichen EditCredentials.label = Zugriffsrechte bearbeiten ChangeLocation.label = Ändere aktuelles Verzeichnis ChangeDate.label = Datum ändern ChangeDate.tooltip = Datum der ausgewählten Datei(en) bearbeiten ChangePermissions.label = Zugriffsrechte bearbeiten ChangePermissions.tooltip = Zugriffsrechte der ausgewählten Datei(en) bearbeiten CheckForUpdates.label = Nach Updates suchen CompareFolders.label = Ordner vergleichen CompareFolderFiles.label = Dateien vergleichen CompareFolderFiles.tooltip = Vergleiche Dateien und markiere geänderte im aktuellen Ordner ConnectToServer.label = Verbinde mit Server ConnectToServer.tooltip = Mit einem Remote Server verbinden View.label = Anzeigen ViewAs.label = Anzeigen als InternalView.label = Anzeigen (eingebaut) viewer_type.text = Textdatei viewer_type.hex = Binärdatei viewer_type.image = Bild viewer_type.pdf = PDF Dokument viewer_type.audio = Audiodatei viewer_type.html = HTML Dokument View.tooltip = Markierte Datei betrachten Edit.label = Bearbeiten InternalEdit.label = Editor (eingebaut) Calculator.label = Rechner CreateSymlink.label = Symlink erstellen LocateSymlink.label = Gehe zum Ziel des Symlinks ShowFoldersSize.label = Zeige Ordnergröße EditCommands.label = Bearbeite Befehle EditCommands.group.view = Betrachter EditCommands.group.edit = Editoren EditCommands.group.others = Andere CompareFiles.label = Vergleiche Dateien CompareFiles.tooltip = Vergleiche Textdateien (FileMerge) EditCommands.new = Neu EditCommands.alias = Alias EditCommands.command = Befehl EditCommands.display_name = Anzeigename EditCommands.filemask = Dateifilter symboliclinkeditor.edit = Bearbiete Symlink symboliclinkeditor.create = Symbolische Verknüpfung symboliclinkeditor.target_file_create = Bestehender Dateiname (Dateiname auf den der Symlink zeigen wird) symboliclinkeditor.target_file_edit = Symlink '%s' zeigt auf symboliclinkeditor.link_name = Dateiname für symbolische Verknüpfung Edit.tooltip = Bearbeite ausgewählte Datei Copy.label = Kopieren Copy.tooltip = Markierte Dateien kopieren LocalCopy.tooltip = Kopiert markierte Datei in den aktuellen Ordner LocalCopy.label = Lokale Kopie Move.label = Verschieben Move.tooltip = Markierte Dateien verschieben Rename.label = Umbenennen Rename.tooltip = Markierte Datei umbenennen Mkdir.label = Ordner erstellen Mkdir.tooltip = Erstellt einen Ordner im aktuellen Ordner Mkfile.label = Neue Datei anlegen Mkfile.tooltip = Neue Datei im aktuellen Verzeichnis anlegen Delete.label = Löschen Delete.tooltip = Markierte Dateien in den Papierkorb verschieben (falls möglich) PermanentDelete.label = unwiderruflich löschen PermanentDelete.tooltip = Markierte Dateien unwiderruflich löschen Refresh.label = Aktualisieren Refresh.tooltip = Aktualisiere aktuellen Ordner CloseWindow.label = Fenster schließen CloseWindow.tooltip = Schließt aktuelles Fenster CopyFileNames.label = Kopiere Name(n) CopyFileBaseNames.label = Kopiere Basisname(n) CopyFilePaths.label = Kopiere Pfad(e) CopyFilesToClipboard.label = Kopiere Datei(en) PasteClipboardFiles.label = Einfügen Datei(en) Email.label = Dateien mailen Email.tooltip = Markierte Dateien als eMail Anhang versenden GoBack.label = Zurueck gehen GoBack.tooltip = Zum vorigen Ordner gehen GoForward.label = Vorwärts gehen GoForward.tooltip = Zum nächsten Ordner gehen GoToHome.label = Gehe zum Heimat-Verzeichnis GoToParent.label = Gehe Ebene höher GoToParent.tooltip = Zum übergeordneten Ordner gehen GoToParentInOtherPanel.label = Im anderen Panel zum übergeordneten Ordner gehen GoToParentInBothPanels.label = Im beiden Paneln zum übergeordneten Ordner gehen GoToRoot.label = Gehe zum Wurzel-Verzeichnis SortByName.label = Nach Namen sortieren SortByDate.label = Nach Datum sortieren SortBySize.label = Nach Größe sortieren SortByExtension.label = Nach Erweiterung sortieren SortByPermissions.label = Nach Zugriffsrechten sortieren SortByOwner.label = Nach Eigentümer sortieren SortByGroup.label = Nach Gruppe sortieren MarkGroup.label = Dateien markieren MarkGroup.tooltip = Eine Gruppe von Dateien markieren UnmarkGroup.label = Dateien demarkieren UnmarkGroup.tooltip = Eine Gruppe von Dateien demarkieren MarkAll.label = Alle markieren UnmarkAll.label = Alle demarkieren MarkSelectedFile.label = Markieren/Demarkieren MarkSelectedFile.tooltip = Datei markieren/demarkieren MarkNextBlock.label = Nächsten Block markieren MarkPreviousBlock.label = Vorigen Block markieren MarkNextRow.label = Nächste Zeile markieren MarkPreviousRow.label = Vorige Zeile markieren MarkNextPage.label = Dateien bis zur nächsten Seite Markieren MarkPreviousPage.label = Dateien bis zur vorigen Seite Markieren MarkToFirstRow.label = Dateien bis zum Anfang Markieren MarkToLastRow.label = Dateien bis zum Ende Markieren MarkExtension.label = Erweiterung markieren MarkEmpty.label = Leere markieren InvertSelection.label = Auswahl umkehren SwapFolders.label = Fenster vertauschen SwapFolders.tooltip = Linkes und rechtes Fenster vertauschen SetSameFolder.label = Auf selben Ordner setzen SetSameFolder.tooltip = Gleicher Ordner für linkes und rechtes Fenster ToggleTableViewModeFull.label = Vollansichtsmodus ToggleTableViewModeFull.tooltip = Auf Vollansicht umschalten ToggleTableViewModeCompact.label = Kompakter Modus ToggleTableViewModeCompact.tooltip = Auf Kompaktansicht umschalten ToggleTableViewModeShort.label = Kurzansichtmodus ToggleTableViewModeShort.tooltip = Auf Kurzansichtmodus umschalten NewWindow.label = Neues Fenster NewWindow.tooltip = Ein neues Fenster öffnen Open.label = Öffnen Open.tooltip = Ordner öffnen / Archiv öffnen / Ausführen OpenNatively.label = Natürlich Öffnen OpenNatively.tooltip = Markierte Datei mit assoziiertem Programm öffnen OpenInNewTab.label = In neuem Panel öffnen OpenInOtherPanel.label = In anderem Panel öffnen OpenInBothPanels.label = In beiden Paneln öffnen OpenLeftInRightPanel.label = Linkes Panel in rechtem Panel öffnen OpenRightInLeftPanel.label = Rechtes Panel in linkem Panel öffnen RevealInDesktop.label = Öffne in %1 RunCommand.label = Befehl ausführen RunCommand.tooltip = Einen Befehl im aktuellen Ordner ausführen Pack.label = Dateien packen Pack.tooltip = Markierte Dateien in ein Archiv packen Unpack.label = Dateien entpacken Unpack.tooltip = Markierte Dateien aus dem Archiv extrahieren ShowFileProperties.label = Eigenschaften ShowFileProperties.tooltip = Eigenschaften der markierten Dateien anzeigen ShowPreferences.label = Einstellungen ShowPreferences.tooltip = trolCommander konfigurieren ShowServerConnections.label = Zeige offene Verbindungen Quit.label = Beenden ReverseSortOrder.label = Sortierung umdrehen ToggleAutoSize.label = Spaltengröße automatisch bestimmen Stop.label = Ordnerwechsel abbrechen ToggleColumn.show = Zeige Spalte %1 ToggleColumn.hide = Verberge Spalte %1 ToggleCommandBar.show = Befehlsleiste anzeigen ToggleCommandBar.hide = Befehlsleiste verbergen ToggleHiddenFiles.label = Zeige versteckte Dateien ToggleToolBar.show = Symbolleiste anzeigen ToggleToolBar.hide = Symbolleiste verbergen CustomizeCommandBar.label = Kommandozeile anpassen ToggleStatusBar.show = Statusleiste anzeigen ToggleStatusBar.hide = Statusleiste verbergen ToggleShowFoldersFirst.label = Ordner zuerst anzeigen ToggleFoldersAlwaysAlphabetical.label = Ordner immer alphabetisch ordnen ToggleTree.label = Zeige Baumansicht ToggleSinglePanel.label = Einzelnes Panel PopupLeftDriveButton.label = Linken Ordner ändern PopupRightDriveButton.label = Rechten Ordner ändern RecallPreviousWindow.label = Zum vorherigen Fenster wechseln RecallNextWindow.label = Zum nächsten Fenster wechseln RecallWindow.label = Fenster #%1 aufrufen RecallWindow1.label = Rufe Fenster #%1 auf RecallWindow2.label = Rufe Fenster #%1 auf RecallWindow3.label = Rufe Fenster #%1 auf RecallWindow4.label = Rufe Fenster #%1 auf RecallWindow5.label = Rufe Fenster #%1 auf RecallWindow6.label = Rufe Fenster #%1 auf RecallWindow7.label = Rufe Fenster #%1 auf RecallWindow8.label = Rufe Fenster #%1 auf RecallWindow9.label = Rufe Fenster #%1 auf RecallWindow10.label = Rufe Fenster #%1 auf BringAllToFront.label = Alle Fenster nach vorne bringen SwitchActiveTable.label = Zwischen linkem und rechtem Fenster wechseln SelectNextBlock.label = Zum nächsten Block springen SelectPreviousBlock.label = Zum vorigen Block springen SelectNextPage.label = Nächste Seite SelectPreviousPage.label = Vorige Seite SelectNextRow.label = Nächste Zeile SelectPreviousRow.label = Vorige Zeile SelectFirstRow.label = Erste Datei im aktuellen Ordner markieren SelectLastRow.label = Letzte Datei im aktuellen Ordner markieren SplitEqually.label = Gleichmäßig aufteilen SplitVertically.label = Vertikal aufteilen SplitHorizontally.label = Horizontal aufteilen ShowKeyboardShortcuts.label = Tastaturkürzel GoToWebsite.label = Zur Internetseite GoToForums.label = Zum Forum ReportBug.label = Einen Fehler melden Donate.label = Geld spenden ShowAbout.label = Über trolCommander OpenTrash.label = Papierkorb öffnen EmptyTrash.label = Papierkorb leeren CalculateChecksum.label = Prüfsumme berechnen MaximizeWindow.label = Maximieren MaximizeWindow.label.mac_os_x = Zoomen MinimizeWindow.label = Minimieren GoToDocumentation.label = Online Dokumentation ShowParentFoldersQL.label = Übergeordnetes Verzeichnis ShowRecentLocationsQL.label = Kürzlich besuchte Ordner ShowRecentExecutedFilesQL.label = Kürzlich ausgeführte Programme ShowRootFoldersQL.label = Wurzelverzeichnisse ShowTabsQL.label = Öffne Tabs ShowRecentViewedFilesQL.label = Kürzlich betrachtete Dateien ShowRecentEditedFilesQL.label = Kürzlich bearbeitete Dateien SplitFile.label = Teilen SplitFile.tooltip = Datei in mehrere Teile aufspalten CombineFiles.label = Zusammenfügen CombineFiles.tooltip = Dateiteile zum Original zusammenfügen ShowDebugConsole.label = Debug-Konsole FocusPrevious.label = Fokus in vorherige Komponente FocusNext.label = Fokus in nächste Komponente ShowBookmarksQL.label = Lesezeichen EjectDrive.label = Laufwerk auswerfen EjectDrive.tooltip = Laufwerk sicher entfernen file_menu = Datei file_menu.open_with = Öffne mit file_menu.open_as = Öffnen als mark_menu = Markieren view_menu = Ansicht view_menu.show_hide_columns = Spalten ein-/ausblenden view_menu.table_mode = Modus go_menu = Gehe tools_menu = Werkzeuge eject_menu = Laufwerk auswerfen bookmarks_menu = Lesezeichen bookmarks_menu.no_bookmark = Keine Lesezeichen drive_popup.network_shares = Netzwerkfreigaben quick_lists_menu = Quick Listen window_menu = Fenster help_menu = Hilfe status_bar.selected_files = %1 von %2 markiert status_bar.connecting_to_folder = Verbindungsaufbau zu Ordner, drücke ESC um Abzubrechen. status_bar.volume_free = Frei: %1 status_bar.volume_capacity = Speicherkapazität: %1 shortcuts_panel.title = Tastenkürzel shortcuts_panel.restore_defaults = Standard wiederherstellen shortcuts_panel.show = Zeigen shortcuts_panel.search = Suchen shortcuts_panel.default_message = Drücken Sie Enter oder führen Sie einen Doppelklick auf dem Tastenkürzel aus, dass sie bearbeiten wollen shortcuts_table.action_description = Beschreibung der Aktion shortcuts_table.shortcut = Tastenkürzel shortcuts_table.alternate_shortcut = Alternatives Tastenkürzel shortcuts_table.type_in_a_shortcut = Tastenkürzel eingeben command_bar_dialog.help = Verschieben Sie die Schaltflächen um die Kommandozeile anzupassen table.folder_access_error_title = Fehler beim Ordnerzugriff table.folder_access_error = Kann Ordnerinhalt nicht lesen table.download_or_browse = Wollen sie zur Datei browsen oder sie downloaden? AddTab.label = Tab hinzufügen AddTab.tooltip = Neuen Tab im aktiven Fenster hinzufügen ToggleLockTab.lock = Fixieren ToggleLockTab.unlock = Freigeben ToggleLockTab.label = Fixieren/Freigeben ToggleLockTab.tooltip = Ändere den Fixierungs-Status des Tabs DuplicateTab.label = Duplizieren DuplicateTab.tooltip = Dupliziert den aktiven Tab im gleichen Fenster CloseDuplicateTabs.label = Duplikate schließem CloseDuplicateTabs.tooltip = Schließe doppelte Tabs CloseOtherTabs.label = Schließe Andere CloseOtherTabs.tooltip = Schließe die anderen Tabs CloseTab.label = Schließen CloseTab.tooltip = Schließe diesen Tab MoveTabToOtherPanel.label = Zum anderen Fenster bewegen MoveTabToOtherPanel.tooltip = Verschiebe den Tab zum anderen Fenster CloneTabToOtherPanel.label = Zum anderen Fenster duplizieren CloneTabToOtherPanel.tooltip = Füge im anderen Fenster einen gleichen Tab hinzu NextTab.label = Nächster Tab NextTab.tooltip = Schalte zum Tab nach rechts weiter PreviousTab.label = Vorheriger Tab PreviousTab.tooltip = Schalte zum Tab nach links weiter SetTabTitle.label = Titel setzen SetTabTitle.tooltip = Setze einen festen Titel für den Tab version_dialog.no_new_version_title = Keine neue Version version_dialog.no_new_version = Gratulation, sie haben schon die aktuellste Version. version_dialog.new_version_title = Neu Version verfügbar version_dialog.new_version = Eine neue Version des trolCommander ist verfügbar. version_dialog.new_version_url = Eine neue Version des trolCommander ist bei %1 erhältlich. version_dialog.not_available_title = Server nicht erreichbar version_dialog.not_available = Kann die Versionsinformation nicht vom Server laden. version_dialog.install_and_restart = Installieren und Neustart version_dialog.preparing_for_update = Vorbereitungen für den Update... quit_dialog.title = trolCommander beenden quit_dialog.desc = Sie haben %1 offene(s) Fenster. Alle Fenster schließen und trolCommander beenden? quit_dialog.show_next_time = Nächstes Mal anzeigen destination_dialog.file_exists_action = Standard Aktion wenn Datei existiert destination_dialog.verify_integrity = Daten Integrität überprüfen destination_dialog.skip_errors = Fehler ignorieren destination_dialog.background_mode = Im Hintergrund file_collision_dialog.title = Dateikonflikt rename_dialog.new_name = Neuer Name copy_dialog.destination = Kopiere markierte Datei(en) nach copy_dialog.error_title = Kopierfehler copy_dialog.copying = Kopiere Dateien copy_dialog.copying_file = Kopiere %1 pack_dialog.packing = Packe Dateien pack_dialog.packing_file = Packe Datei %1 pack_dialog.error_title = Fehler beim Packen pack_dialog_description = Markierte Dateien hinzufügen zu pack_dialog.archive_format = Archiv Format unpack_dialog.destination = Entpacke markierte Datei(en) nach unpack_dialog.error_title = Fehler beim Entpacken unpack_dialog.unpacking = Entpacke Dateien unpack_dialog.unpacking_file = Entpacke %1 optimizing_archive = Archiv optimieren %1 error_while_optimizing_archive = Fehler bei der Optimierung des Archivs %1 move_dialog.move_description = Verschieben nach move_dialog.error_title = Fehler beim Verschieben move_dialog.moving = Verschiebe Dateien move_dialog.moving_file = Verschiebe %1 download_dialog.description = Downloaden nach download_dialog.error_title = Fehler beim Download download_dialog.downloading = Downloaden download_dialog.downloading_file = Download %1 download_dialog.downloading_file = Download von %1 mkfile_dialog.allocate_space = Speicherplatz bereitstellen mkfile_dialog.open_in_editor = Im Texteditor öffnen mkfile_dialog.make_executable = Ausführbare Datei delete_dialog.permanently_delete.confirmation = Markierte Datei(en) permanent löschen ? delete_dialog.move_to_trash.confirmation = Ausgewählte Datei(en) löschen ? delete_dialog.move_to_trash.confirmation_details = Dateien werden in den Papierkorb bewegt. delete_dialog.move_to_trash.option = In den Papierkorb verschieben delete_dialog.move_to_trash.failed = Eine oder mehrere Dateien konnten nicht in den Papierkorb verschoben werden. delete_dialog.deleting = Löschen delete_dialog.error_title = Fehler beim Löschen delete.deleting_file = Lösche %1 email_dialog.prefs_not_set_title = eMail nicht konfiguriert email_dialog.prefs_not_set = Nehmen sie erst die eMail Einstellungen vor. email_dialog.from = Von email_dialog.to = An email_dialog.subject = Betreff email_dialog.send = Senden email_dialog.error_title = Fehler beim Senden der Dateien email_dialog.read_error = Kann Dateien im Unterordner nicht lesen. email_dialog.sending = Sende Dateien email.sending_file = Sende %1 email.connecting_to_server = Verbinden mit %1 email.server_unavailable = Kann Verbindung zu Server %1 nicht herstellen, überprüfen sie die eMail Einstellungen oder versuchen sie es später. email.connection_closed = Verbindung vom Server unterbrochen, eMail nicht gesendet. email.goodbye_failed = Fehler beim Schließen der Verbindung, etvl. wurde die eMail nicht gesendet. email.send_file_error = Kann Datei %1 nicht senden, eMail nicht gesendet. split_file_dialog.error_title = Fehler beim Aufspalten einer Datei split_file_dialog.file_to_split = Datei zum Aufspalten split_file_dialog.target_directory = Zielverzeichnis split_file_dialog.part_size = Größe eines Teils split_file_dialog.parts = Anzahl der Teile split_file_dialog.generate_CRC = Erzeuge CRC-Datei split_file_dialog.max_parts = Maximale Anzehl der Teile ist %1 split_file_dialog.auto = Automatisch split_file_dialog.insert_new_media = Neues Medium einlegen combine_files_dialog.error_title = Fehler beim Zusammenfügen der Datei combine_files_job.no_crc_file = Zusammenfügen der Datei erfolgreich. Keine CRC-Datei. combine_files_job.crc_read_error = Fehler beim Lesen der CRC-Datei. combine_files_job.crc_check_failed = CRC-Fehler: erwartet %2, gefunden %1 combine_files_job.crc_ok = Zusammenfügen der Datei erfolgreich. CRC-Prüfsumme o.k. file_selection_dialog.mark = Markieren file_selection_dialog.unmark = Demarkieren file_selection_dialog.mark_description = Markiere Dateien deren Name file_selection_dialog.unmark_description = Demarkiere Dateien deren Name file_selection_dialog.case_sensitive = Groß-/Kleinschreibung beachten file_selection_dialog.include_folders = Ordner einschliessen progress_dialog.starting = Transfer startet... progress_dialog.transferred = Transferiert %1 mit %2 progress_dialog.elapsed_time = Zeitdauer progress_dialog.advanced = Erweitert progress_dialog.current_speed = Aktuelle Geschwindigkeit progress_dialog.limit_speed = Maximal Geschwindigkeit progress_dialog.close_when_finished = Nach Ende Fenster schließen progress_dialog.processing_files = Bearbeite Dateien progress_dialog.processing_file = Bearbeite %1 progress_dialog.verifying_file = Überprüfung %1 progress_dialog.job_finished = Job beendet progress_dialog.job_error = Jobfehler progress_dialog.hide = Verbergen properties_dialog.file_properties = Eigenschaften von %1 properties_dialog.contents = Inhalt properties_dialog.calculating = Kalkuliere... calculate_checksum_dialog.checksum_algorithm = Prüfsummen Algorithmus calculate_checksum_dialog.temporary_file = Temporäre Datei change_date_dialog.now = Jetzt change_date_dialog.specific_date = Spezielles Datum run_dialog.run_command_description = Ausführen im aktuellen Ordner run_dialog.run_in_home_description = In Heimatverzeichnis ausführen run_dialog.command_output = Befehlsausgabe run_dialog.run = Ausführen run_dialog.stop = Anhalten run_dialog.clear_history = Verlauf löschen server_connect_dialog.server_type = Verbindungstyp server_connect_dialog.server = Server server_connect_dialog.share = Freigabe server_connect_dialog.domain = Domäne server_connect_dialog.username = Benutzername server_connect_dialog.initial_dir = Startordner server_connect_dialog.port = Port server_connect_dialog.server_url = Server URL server_connect_dialog.http_url = URL Webseite server_connect_dialog.connect = Verbinden server_connect_dialog.protocol = Protokoll server_connect_dialog.nfs_version = NFS-Version server_connect_dialog.private_key = Privater Schlüssel server_connect_dialog.passphrase = Passwort ftp_connect.passive_mode = Passiven Modus aktivieren ftp_connect.anonymous_user = Anonymer Benutzer ftp_connect.nb_connection_retries = Maximale Anzahl Verbindungsversuche ftp_connect.retry_delay = Wartezeit zwischen Verbindungsversuchen (in Sekunden) http_connect.basic_authentication = HTTP Basic Authentication (optional) server_connections_dialog.disconnect = Verbindung Abbrechen server_connections_dialog.connection_busy = Besetzt server_connections_dialog.connection_idle = Inaktiv vsphere_connections_dialog.guest_server = Gast Server %1 vsphere_connections_dialog.guest_user = Gast Username vsphere_connections_dialog.guest_password = Gast Passwort bonjour.bonjour_services = Bonjour Dienste bonjour.no_service_discovered = Keine Dienste gefunden bonjour.bonjour_disabled = Bonjour nicht aktiv auth_dialog.title = Authentifizierung auth_dialog.desc = Bitte Login und Passwort eingeben auth_dialog.server = Server auth_dialog.store_credentials = Speichere Login und Passwort (schwach verschlüsselt) auth_dialog.connect_as = Verbinde als auth_dialog.authentication_failed = Authentifizierung fehlgeschlagen sortable_list.move_up = Nach oben sortable_list.move_down = Nach unten add_bookmark_dialog.add = Hinzufügen edit_bookmarks_dialog.location = Ort edit_bookmarks_dialog.new = Neu file_viewer.view_error_title = Fehler beim Betrachten file_viewer.view_error = Datei kann nicht betrachtet werden. file_viewer.file_menu = Datei file_viewer.close = Schließen file_viewer.large_file_warning = Diese Datei könnte für diese Operation zu groß sein. file_viewer.open_anyway = Trotzdem öffnen file_viewer.open_hex = Hex Ansicht text_viewer.edit = Bearbeiten text_viewer.copy = Kopieren text_viewer.select_all = Alles markieren text_viewer.find = Suchen text_viewer.find_next = Weitersuchen text_viewer.find_previous = Vorherigen suchen text_viewer.view = Ansicht text_viewer.line_wrap = Zeilenumbruch text_viewer.line_numbers = Zeilennummern text_viewer.binary_file_warning = Dies scheint eine Binärdatei zu sein text_viewer.goto_line = Gehe zu Zeile text_viewer.line = Zeile image_viewer.controls_menu = Steuerung image_viewer.zoom_in = Hinein zoomen image_viewer.zoom_out = Heraus zoomen file_editor.edit_error_title = Fehler beim Bearbeiten file_editor.edit_error = Kann Datei nicht bearbeiten. file_editor.save = Speichern file_editor.save_as = Speichern als... file_editor.add_to_bookmark = Lesezeichen für Datei file_editor.save_warning = Änderungen an der Datei vor dem Schließen speichern ? file_editor.cannot_write = Kann Datei nicht schreiben. file_editor.files = Dateien file_editor.file_menu = Datei file_editor.show_file_manager = Zeige Datei-Manager file_editor.close = Schließen file_editor.open_anyway = Trotzdem öffnen file_editor.save_anyway = Trotzdem speichern file_editor.overwrite_readonly = Die Datei ist schreibgeschützt text_editor.cut = Ausschneiden text_editor.paste = Einfügen text_editor.undo = Rückgänging text_editor.redo = Wiederholen text_editor.edit = Bearbeiten text_editor.copy = Kopieren text_editor.select_all = Alles auswählen text_editor.view = Ansicht text_editor.find = Suchen text_editor.find_next = Suche nächste text_editor.find_previous = Suche vorherige text_editor.search = Suchen text_editor.replace_menu = Ersetzen... text_editor.line_wrap = Zeilenumbruch text_editor.line_numbers = Zeilennummern text_editor.syntax = Syntax text_editor.format = Format text_editor.writing = Schreibe… text_editor.modified = Geändert text_editor.saved = Datei gespeichert text_editor.invisible_chars = Unsichtbare Zeichen text_editor.text_not_found = Text nicht gefunden text_editor.cant_save_file = Kann Datei nicht speichern shortcuts_dialog.quick_search = Schnellsuche shortcuts_dialog.quick_search.start_search = Geben Sie ein beliebiges Zeichen ein um die Schnellsuche zu starten shortcuts_dialog.quick_search.cancel_search = Schnellsuche abbrechen shortcuts_dialog.quick_search.remove_last_char = Letztes Zeichen aus der Schnellsuche entfernen shortcuts_dialog.quick_search.jump_to_previous = Springe zum vorigen Ergebnis der Schnellsuche shortcuts_dialog.quick_search.jump_to_next = Springe zum nächsten Ergebnis der Schnellsuche shortcuts_dialog.quick_search.mark_jump_next = Markiere/Demarkiere aktuelle Datei und springe zum nächsten Suchergebnis theme_editor.title = Themen-Editor theme_editor.folder_tab = Ordnerbereich theme_editor.shell_tab = Kommandozeile theme_editor.shell_history_tab = Shell Historie theme_editor.terminal_tab = Terminal theme_editor.statusbar_tab = Statuszeile theme_editor.free_space = Free Speicher theme_editor.free_space.ok = OK theme_editor.free_space.warning = Warnung theme_editor.free_space.critical = Kritisch theme_editor.locationbar_tab = Bereich aktueller Ordner theme_editor.editor_tab = Dateieditor theme_editor.font = Schriftart theme_editor.active_panel = Aktiv theme_editor.inactive_panel = Inaktiv theme_editor.general = Allgemein theme_editor.theme_warning_predefined = Eingebaute Themen können nicht geändert werden. Möchten Sie ein neues Thema erstellen? theme_editor.could_not_save_theme = Kann Thema %1 nicht schreiben theme_editor.border = Rand theme_editor.background = Hintergrund theme_editor.alternate_background = Alternativer Hintergrund theme_editor.unfocused_background = Hintergrund (ohne Fokus) theme_editor.copy_colors = Kopiere %1 theme_editor.quick_search = Schnellsuche theme_editor.quick_search.unmatched_file = Nichtpassende Datei theme_editor.text = Text theme_editor.progress = Fortschritt theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (ohne Fokus) theme_editor.selected = Ausgewählt theme_editor.selected_unfocused = Ausgewählt (ohne Fokus) theme_editor.color = Farbe theme_editor.colors = Farben theme_editor.plain_file = Normale Datei theme_editor.marked_file = Markierte Datei theme_editor.hidden_file = Versteckte Datei theme_editor.folder = Ordner theme_editor.archive_file = Archivdatei theme_editor.symbolic_link = Symbolische Verknüpfung theme_editor.executable_file = Executable file theme_editor.header = Überschrift theme_editor.current = Aktuelle Zeile theme_editor.file_groups = Dateigruppen theme_editor.group_ = Gruppen theme_editor.normal_color = Normale Farbe theme_editor.selected_color = Auswahlfarbe theme_editor.filemask = Dateifilter theme_editor.group_file_ = Datei der Gruppe theme_editor.item = Element command_bar_customize_dialog.available_actions = Verfügbare Aktionen command_bar_customize_dialog.modifier = Modifizierer prefs_dialog.title = Einstellungen prefs_dialog.general_tab = Allgemein prefs_dialog.day = Tag prefs_dialog.month = Monat prefs_dialog.year = Jahr prefs_dialog.language = Sprache (Neustart nötig) prefs_dialog.date_time = Datum- und Zeitformat prefs_dialog.time = Zeit prefs_dialog.date = Datum prefs_dialog.date_separator = Trennzeichen prefs_dialog.time_12_hour = 12-Stunden Format prefs_dialog.time_24_hour = 24-Stunden Format prefs_dialog.show_seconds = Sekunden anzeigen prefs_dialog.show_century = Jahrhundert anzeigen prefs_dialog.check_for_updates_on_startup = Beim Start nach Updates suchen prefs_dialog.show_splash_screen = Zeige Startbild prefs_dialog.folders_tab = Ordner prefs_dialog.startup_folders = Startordner prefs_dialog.left_folder = Linker Ordner prefs_dialog.right_folder = Rechter Ordner prefs_dialog.last_folder = Zuletzt besuchter Ordner prefs_dialog.custom_folder = Benutzerdefinierter Ordner prefs_dialog.show_hidden_files = Versteckte Dateien anzeigen prefs_dialog.show_ds_store_files = Zeige .DS_Store-Dateien prefs_dialog.show_system_folders = Zeige Systemverzeichnisse prefs_dialog.compact_file_size = Dateigrößen gerundet anzeigen prefs_dialog.follow_symlinks_when_cd = Symbolischen Links beim Wechsel des aktuellen Verzeichnis folgen prefs_dialog.show_tab_header = Tab-Überschrift immer anzeigen prefs_dialog.appearance_tab = Erscheinungsbild prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Icon-Größe prefs_dialog.toolbar_icons = Toolbar prefs_dialog.command_bar_icons = Kommandozeile prefs_dialog.file_icons = Dateiarten prefs_dialog.use_system_file_icons = Dateisystem-Icons nutzen prefs_dialog.use_system_file_icons.always = Immer prefs_dialog.use_system_file_icons.never = Nie prefs_dialog.use_system_file_icons.applications = Nur für Programme prefs_dialog.edit_current_theme = Aktuelles Thema bearbeiten... prefs_dialog.themes = Themen prefs_dialog.syntax_themes = Editor-Syntax Farbthema prefs_dialog.import_theme = Thema importieren prefs_dialog.import_look_and_feel = Look & Feel importieren prefs_dialog.no_look_and_feel = Kein Look & Feel gefunden. prefs_dialog.error_in_import = Fehler beim Import von Thema %1. prefs_dialog.cannot_read_theme = Kann Thema nicht aus Datei %1 laden prefs_dialog.export_theme = Exportiere %1 prefs_dialog.import = Import prefs_dialog.export = Export prefs_dialog.theme_type = Typ: %1 prefs_dialog.delete_theme = Thema %1 unwiderruflich löschen ? prefs_dialog.delete_look_and_feel = Look & Feel %1 dauerhaft löschen? prefs_dialog.rename_failed = Fehler beim Umbenennen von Thema %1 prefs_dialog.xml_file = XML-Datei prefs_dialog.jar_file = JAR Datei prefs_dialog.mail_tab = eMail prefs_dialog.mail_settings = Einstellungen für ausgehende eMails prefs_dialog.mail_name = Ihr Name prefs_dialog.mail_address = Ihre eMail Adresse prefs_dialog.mail_server = SMTP Server prefs_dialog.misc_tab = Verschiedenes prefs_dialog.use_brushed_metal = 'Brushed Metal' Look benutzen (Neustart nötig) prefs_dialog.confirm_on_quit = Bestätigung beim Beenden prefs_dialog.shell = Befehl ausführen prefs_dialog.default_shell = Benutze Standard-Eingabeaufforderung prefs_dialog.custom_shell = Benutze angepasste Eingabeaufforderung prefs_dialog.shell_encoding = Shell Kodierung prefs_dialog.auto_detect_shell_encoding = Automatisch feststellen prefs_dialog.external_terminal = Externes Terminal prefs_dialog.default_terminal = Standard-Terminalprogramm verwenden prefs_dialog.custom_terminal = Benutze Benutzerbefehl prefs_dialog.builtin_terminal = Eingebautes Terminal verwenden prefs_dialog.default_shell = Standard-Shell verwenden prefs_dialog.custom_shell = Benutze Benutzerbefehl prefs_dialog.enable_bonjour_discovery = Erlaube Bonjour Servicesuche prefs_dialog.enable_system_notifications = System-Nachrichten einschalten debug_console_dialog.level = Level debug_console_dialog.threads = Threads unit.byte = Byte unit.bytes = Bytes unit.bytes_short = B unit.kb = KiB unit.mb = MiB unit.gb = GiB unit.tb = TiB unit.speed = %1/s duration.seconds = %1Sek duration.minutes = %1M duration.hours = %1S duration.days = %1T duration.months = %1M duration.years = %1J duration.infinite = ∞ theme.custom_theme = Angepasstes Thema theme.custom = Angepasst theme.built_in = Eingebaut theme.add_on = Zusatz theme.current = aktuell theme_could_not_be_loaded = Beim Laden dieses Themas trat ein Fehler auf. cannot_open_cyclic_symlink = Kann den auswählten Link nicht öffnen, da er zyklisch ist setup.title = Willkommen beim trolCommander setup.intro = Bitte wählen Sie, wie sich der trolCommander verhalten soll. setup.look_and_feel = Wählen Sie Aussehen und Verhalten setup.theme = Wählen Sie Ihr Thema font_chooser.font_size = Größe font_chooser.font_bold = Fett font_chooser.font_italic = Kursiv color_chooser.red = Rot color_chooser.green = Grün color_chooser.blue = Blau color_chooser.hue = Farbton color_chooser.brightness = Helligkeit color_chooser.swatches = Muster color_chooser.saturation = Sättigung color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Bisherige color_chooser.alpha = Alpha-Transparenz color_chooser.title = Farbe wählen batch_rename_dialog.mask = Umbenennungs Muster batch_rename_dialog.search_replace = Suchen & Ersetzen batch_rename_dialog.search_for = Suche nach batch_rename_dialog.replace_with = Ersetze durch batch_rename_dialog.counter = Zähler batch_rename_dialog.start_at = Starte mit batch_rename_dialog.step_by = Schrittweite batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Groß/Kleinschreibung batch_rename_dialog.no_change = Unverändert batch_rename_dialog.lower_case = kleinschreibung batch_rename_dialog.upper_case = GROSSSCHREIBUNG batch_rename_dialog.first_upper = Erster buchstabe gross batch_rename_dialog.word = Erster Buchstabe Pro Wort Gross batch_rename_dialog.regexp = RegExp batch_rename_dialog.regexp_error = Syntaxfeler im regulären Ausdruck batch_rename_dialog.old_name = Alter Name batch_rename_dialog.new_name = Neuer Name batch_rename_dialog.block_name = Beibehalten batch_rename_dialog.range = Bereich batch_rename_dialog.proceed_renaming = %1 Dateien von %2 werden umbenannt. Wollen Sie fortfahren? batch_rename_dialog.duplicate_names = Doppelte Namen! batch_rename_dialog.names_conflict = Namens Konflikt! Gleiche Werte im alten und neuen Namen. parent_folders_quick_list.empty_message = Aktuelles Verzeichnis hat kein übergeordnetes Verzeichnis recent_locations_quick_list.empty_message = Kein jüngstes Verzeichnis recent_executed_files_quick_list.empty_message = Keine jüngst ausgeführten Programme recent_edited_files_quick_list.empty_message = Keine kürzlich bearbeitete Datei recent_viewed_files_quick_list.empty_message = Keine kürzlich betrachtete Datei roots_quick_list.empty_message = Keine Wurzelverzeichnisse verfügbar tabs_quick_list.empty_message = Nur ein Tab vorhanden #server_connect_dialog.auth_error = Ungültiger Login oder Paßwort. #move_dialog.cannot_move_to_itself = Kann Dateien nicht in Unterordner verschieben. #pack.error_on_file = Fehler beim Packen von %1 #pack_dialog.cannot_write = Kann Datei nicht im Zielordner erstellen. #unpack.unable_to_open_zip = Kann ZIP Datei %1 nicht öffnen. #image_viewer.previous_image = Vorheriges Bild #image_viewer.next_image = Nächstes Bild #mkdir_dialog.title = Ordner erstellen #mkdir_dialog.error_title = Fehler beim Erstellen des Ordners #edit_bookmarks_dialog.remove = Entfernen #mkdir_dialog.description = Ordner erstellen #mkfile_dialog.description = Erstelle neue leere Datei #done = Erledigt #move_dialog.rename_description = Umbenennen nach #theme_editor.shell_font = Kommandozeilen-Zeichensatz #theme_editor.history_font = Historien-Zeichensatz #theme_editor.shell_colors = Farbe Eingabeauforderung #theme_editor.history_colors = Historienfarbe #ToggleHiddenFiles.hide = Versteckte Dateien nicht anzeigen #auth_dialog.error_was = Fehler war: %1 #table.hide_column = Spalte ausblenden #delete.symlink_warning_title = Verknüpfung gefunden #delete.symlink_warning = Diese Datei scheint ein symbolischer Link zu sein:\n\n Datei: %1\n Verlinktt nach: %2\n\nLink löschen oder dem \nLink folgen und Ordner löschen (VORSICHT) ? #delete.delete_link_only = Link löschen #delete.delete_linked_folder = Ordner löschen #Unpack.label = Dateien entpacken #Pack.label = Dateien packen find_dialog.name = Dateiname find_dialog.contains = Enthält find_dialog.initial_directory = Starte bei find_dialog.search_subdirectories = Durchsuche Unterverzeichnisse find_dialog.search_archives = Durchsuche Archive find_dialog.case_sensitive = Groß-/Kleinschreibung beachten find_dialog.ignore_hidden = Ignoriere versteckte find_dialog.search_results = Suchergebnis find_dialog.found = Gefundene Dateien find_dialog.encoding = Textkodierung find_dialog.search_hex = Suche Hexadezimal image_viewer.next_image = Nächstes Bild image_viewer.previous_image = Vorheriges Bild hex_viewer.offset = Offset hex_viewer.ascii_dump = ASCII Ausdruck hex_viewer.view = Ansicht hex_viewer.goto = Gehe zu hex_viewer.goto.offset = Offset hex_viewer.search = Suche hex_viewer.searchNext = Suche nächstes hex_viewer.searchPrev = Suche vorheriges hex_viewer.find = Suche hex_view.text = Suche nach hex_viewer.hex = Hex hex_viewer.search_not_found = Muster nicht gefunden calculator.calculator = Rechner calculator.expression = Ausdruck calculator.error = Fehler im Ausdruck replication = Wiederholungsfaktor blocksize = Blockgröße ChangeReplication.label = Ändere Wiederholung replication.number = Wiederholungsfaktor adb.android_devices = Android adb.no_devices = Keine Geräte eject.no_mounted_devices = Keine angeschlossenen Geräte #MinimizeWindow.label.mac_os_x = Im Dock ablegen ================================================ FILE: src/main/resources/dictionary_en_GB.properties ================================================ license = Licence MaximizeWindow.label = Maximise MinimizeWindow.label = Minimise optimizing_archive = Optimising archive %1 error_while_optimizing_archive = Error while optimising archive %1 theme_editor.color = Colour theme_editor.colors = Colours prefs_dialog.confirm_on_quit = Show confirmation dialogue on quit theme.custom = Customised color_chooser.title = Pick a colour #theme_editor.shell_colors = Shell colours #theme_editor.history_colors = History colours ================================================ FILE: src/main/resources/dictionary_en_US.properties ================================================ ================================================ FILE: src/main/resources/dictionary_es_ES.properties ================================================ ok = Aceptar yes = Sí no = No cancel = Cancelar edit = Editar close = Cerrar reset = Reiniciar rename = Renombrar apply = Aplicar change = Cambiar save = Guardar dont_save = No guardar replace = Reemplazar dont_replace = No reemplazar delete = Eliminar skip = Saltar skip_all = Saltar todos retry = Reintentar resume = Continuar overwrite = Reemplazar overwrite_if_older = Reemplazar si es más antiguo duplicate = Duplicar apply_to_all = Aplicar a todo copy = Copiar move = Mover pack = Comprimir unpack = Descomprimir download = Descargar split = Dividir combine = Unir browse = Navegar ask = Preguntar stop = Parar pause = Pausa quick_search = Búsqueda rápida file_manager = Gestor de ficheros create = Crear creating_file = Creando %1 choose = Seleccionar customize = Personalizar choose_folder = Seleccionar un directorio login = Identificación password = Contraseña user = Usuario encoding = Codificación preferred_encodings = Codificaciones preferidas license = Licencia name = Nombre size = Tamaño date = Fecha extension = Extensión permissions = Permisos owner = Propietario group = Grupo location = Ubicación untitled = Sin nombre source = Origen destination = Destino recurse_directories = Procesar directorios seleccionados recursivamente go_to = Ir a example = Ejemplo preview = Previsualizar comment = Commentaire sample_text = Texto de ejemplo nb_files = %1 fichero(s) nb_folders = %1 directorio(s) loading = Cargando... this_operation_cannot_be_undone = Esta operación no puede ser deshecha. remove = Eliminar details = Detalles warning = Aviso error = Error generic_error = Ha ocurrido un error mientras se realizaba la operación folder_does_not_exist = El directoio no existe o no está disponible. this_folder_does_not_exist = El directoio no existe o no está disponible: %1 this_file_does_not_exist = Este fichero no existe o no está disponible: %1 invalid_path = Dirección inválida: %1 directory_already_exists = El directorio %1 ya existe. file_exists_in_destination = El fichero ya existe en el destino source_parent_of_destination = Intentar transferir un directorio a uno de sus subdirectorios cannot_read_file = No se puede leer el archivo %1 cannot_write_file = No se puede escribir el archivo %1 cannot_create_folder = No se puede crear el directorio %1 cannot_read_folder = No se puede leer el directorio %1 cannot_delete_file = No se puede borrar el fichero %1 cannot_delete_folder = No se puede borrar el directorio %1 error_while_transferring = Error al transferir %1 same_source_destination = El directorio de origen y destino es el mismo file_already_exists = %1 ya existe , ¿desea reemplazarlo? write_error = Error de escritura read_error = Error de lectura integrity_check_error = Comprobación de integridad fallida: fuente y destino no coinciden startup_error = Un error ha impedido iniciar trolCommander permissions.read = Lectura permissions.write = Escritura permissions.executable = Ejecución permissions.group = Grupo permissions.other = Otro permissions.octal_notation = Notación octal action_categories.all = Todos action_categories.navigation = Navigation action_categories.selection = Selección action_categories.view = Ver action_categories.file_operations = Operaciones con ficheros action_categories.windows = Ventanas Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Añadir favorito AddBookmark.tooltip = Añadir a favoritos el directorio actual BatchRename.label = Renombrado masivo EditBookmarks.label = Editar favoritos ExploreBookmarks.label = Explorar favoritos EditCredentials.label = Editar credenciales ChangeLocation.label = Cambiar ubicación actual ChangeDate.label = Cambiar fecha ChangeDate.tooltip = Cambiar fecha de fichero(s) seleccionado(s) ChangePermissions.label = Cambiar permisos ChangePermissions.tooltip = Cambiar permisos de fichero(s) seleccionado(s) CheckForUpdates.label = Comprobar actualizaciones CompareFolders.label = Comparar directorios ConnectToServer.label = Conectar a un servidor ConnectToServer.tooltip = Conectar a un servidor remoto View.label = Ver InternalView.label = Visualizar (interno) View.tooltip = Ver el fichero seleccionado InternalEdit.label = Editar (interno) Edit.tooltip = Editar el fichero seleccionado Copy.tooltip = Copiar los ficheros seleccionados LocalCopy.label = Copia local LocalCopy.tooltip = Copiar el fichero seleccionado en el directorio actual Move.tooltip = Mover los ficheros seleccionados Rename.tooltip = Renombrar el fichero seccionado Mkdir.label = Crear directorio Mkdir.tooltip = Crear subdirectorio en el directorio actual Mkfile.label = Crear fichero Mkfile.tooltip = Crear un fichero en el directorio actual Delete.tooltip = Eliminar los ficheros marcados PermanentDelete.label = Borrar permanentemente PermanentDelete.tooltip = Borrar ficheros marcados sin usar la papelera de reciclaje Refresh.label = Actualizar Refresh.tooltip = Actualizar directorio actual CloseWindow.label = Cerrar ventana CloseWindow.tooltip = Cerrar esta ventana CopyFileNames.label = Copiar nombre(s) CopyFilePaths.label = Copiar ruta(s) CopyFilesToClipboard.label = Copiar fichero(s) PasteClipboardFiles.label = Pegar fichero(s) Email.label = Enviar por email Email.tooltip = Enviar los ficheros seleccionados como adjuntos en un correo electrónico GoBack.label = Volver GoBack.tooltip = Ir al directorio anterior GoForward.label = Siguiente GoForward.tooltip = Ir al directorio siguiente GoToHome.label = Ir al directorio de inicio GoToParent.label = Ir al padre GoToParent.tooltip = Ir al directorio padre GoToParentInOtherPanel.label = Ir al directorio padre en el otro panel GoToParentInBothPanels.label = Ir al directorio padre en ambos paneles GoToRoot.label = Ir al raiz SortByName.label = Ordenar por Nombre SortByDate.label = Ordenar por Fecha SortBySize.label = Ordenar por Tamaño SortByExtension.label = Ordenar por Extensión SortByPermissions.label = Ordenar por Permisos SortByOwner.label = Ordenar por Propietario SortByGroup.label = Ordenar por Grupo MarkGroup.label = Marcar ficheros MarkGroup.tooltip = Marcar un grupo de ficheros UnmarkGroup.label = Desmarcar ficheros UnmarkGroup.tooltip = Desmarcar un grupo de ficheros MarkAll.label = Marcar todos UnmarkAll.label = Desmarcar todos MarkSelectedFile.label = Marcar/Desmarcar MarkSelectedFile.tooltip = Marcar/Desmarcar los ficheros seleccionados MarkNextBlock.label = Seleccionar el siguiente bloque MarkPreviousBlock.label = Seleccionar el bloque anterior MarkNextRow.label = Seleccionar una fila hacia abajo MarkPreviousRow.label = Seleccionar una fila hacia arriba MarkNextPage.label = Marcar página abajo MarkPreviousPage.label = Marcar página arriba MarkToFirstRow.label = Marcar ficheros hasta el comienzo MarkToLastRow.label = Marcar ficheros hasta el final MarkExtension.label = Marcar extensión InvertSelection.label = Invertir selección SwapFolders.label = Intercambiar directorios SwapFolders.tooltip = Intercambiar directorios SetSameFolder.label = Marcar el mismo directorio SetSameFolder.tooltip = Ajustar el mismo directorio en ambos lados NewWindow.label = Nueva ventana NewWindow.tooltip = Abrir una nueva ventana Open.label = Abrir Open.tooltip = Entrar en el directorio / Entrar en el fichero / Ejecutar OpenNatively.label = Abrir nativamente OpenNatively.tooltip = Ejecutar el fichero seleccionado con la asosiación del sistema OpenInNewTab.label = Abrir en pestaña nueva OpenInOtherPanel.label = Abrir en otro panel OpenInBothPanels.label = Abrir en ambos paneles RevealInDesktop.label = Mostrar en %1 RunCommand.label = Ejecutar un comando RunCommand.tooltip = Ejecutar un comando en el directorio actual Pack.tooltip = Empaquetar ficheros marcados Unpack.tooltip = Desempaquetar ficheros marcados ShowFileProperties.label = Propiedades ShowFileProperties.tooltip = Mostrar propiedades de los ficheros seleccionados ShowPreferences.label = Preferencias ShowPreferences.tooltip = Configurar trolCommander ShowServerConnections.label = Mostrar conexiones abiertas Quit.label = Salir ReverseSortOrder.label = Invertir el orden ToggleAutoSize.label = Autodimensionar las columnas Stop.label = Detener cambio de directorio ToggleColumn.show = Mostrar la columna %1 ToggleColumn.hide = Ocultar la columna %1 ToggleCommandBar.show = Mostar la barra de comandos ToggleCommandBar.hide = Ocultar la barra de comandos ToggleToolBar.show = Mostrar la barra de herramientas ToggleToolBar.hide = Ocultar la barra de herramientas CustomizeCommandBar.label = Personalizar la barra de comandos ToggleStatusBar.show = Mostrar barra de estado ToggleStatusBar.hide = Ocultar la barra de estado ToggleShowFoldersFirst.label = Mostrar directorios primero ToggleTree.label = Mostrar vista en árbol PopupLeftDriveButton.label = Cambiar el directorio izquierdo PopupRightDriveButton.label = Cambiar el directorio derecho RecallPreviousWindow.label = Cambiar a la ventana anterior RecallNextWindow.label = Cambiar a la siguiente ventana RecallWindow.label = Rellamar ventana #%1 BringAllToFront.label = Traer todo al frente SwitchActiveTable.label = Cambiar entre los paneles izquierdo y derecho SelectNextBlock.label = Saltar un bloque hacia abajo SelectPreviousBlock.label = Saltar un bloque hacia arriba SelectNextPage.label = Saltar una página hacia abajo SelectPreviousPage.label = Saltar una página hacia arriba SelectNextRow.label = Saltar una fila hacia abajo SelectPreviousRow.label = Saltar una fila hacia arriba SelectFirstRow.label = Seleccionar el primer fichero en el directorio actual SelectLastRow.label = Seleccionar el último fichero en el directorio actual SplitEqually.label = Dividir equitativamente SplitVertically.label = Dividir verticalmente SplitHorizontally.label = Dividir horizontalmente ShowKeyboardShortcuts.label = Atajos de teclado GoToWebsite.label = Ir a sitio web GoToForums.label = Foros ReportBug.label = Comunicar fallo Donate.label = Hacer una donación ShowAbout.label = Acerca de trolCommander OpenTrash.label = Abrir papelera EmptyTrash.label = Vaciar papelera CalculateChecksum.label = Calcular checksum MaximizeWindow.label = Maximizar GoToDocumentation.label = Documentación en línea ShowParentFoldersQL.label = Directorios padre ShowRecentLocationsQL.label = Ubicaciones recientes ShowRecentExecutedFilesQL.label = Ficheros ejecutados recientemente ShowRootFoldersQL.label = Directorios raíz SplitFile.tooltip = Dividir un fichero en múltiples partes CombineFiles.tooltip = Reconstruir un fichero a partir de sus partes ShowDebugConsole.label = Consola de depuración FocusPrevious.label = Posiciona el foco en el componente anterior FocusNext.label = Posiciona el foco en el siguiente componente file_menu = Archivo file_menu.open_with = Abrir con mark_menu = Marcar view_menu = Ver view_menu.show_hide_columns = Mostrar/Ocultar columnas go_menu = Ir bookmarks_menu = Favoritos bookmarks_menu.no_bookmark = No existe favorito drive_popup.network_shares = Ubicaciones de red compartidas quick_lists_menu = Listas rápidas window_menu = Ventana help_menu = Ayuda status_bar.selected_files = %1 de %2 seleccionados status_bar.connecting_to_folder = Conectando al directorio, presiona ESCAPE para cancelar status_bar.volume_free = Libre: %1 status_bar.volume_capacity = Capacidad: %1 shortcuts_panel.title = Atajos de teclado shortcuts_panel.restore_defaults = Restaurar valores por defecto shortcuts_panel.show = Mostrar shortcuts_panel.default_message = Pulsar Intro o hacer doble click sobre el atajo de teclado a editar shortcuts_table.action_description = Descripción de la acción shortcuts_table.shortcut = Atajo de teclado shortcuts_table.alternate_shortcut = Atajo de teclado alternativo shortcuts_table.type_in_a_shortcut = Escribir un atajo de teclado command_bar_dialog.help = Arrastrar botones para personalizar la barra de comandos table.folder_access_error_title = Error al acceder al directorio table.folder_access_error = Imposible leer el contenido del directorio table.download_or_browse = ¿Desea explorar o descargar este fichero? CloseDuplicateTabs.tooltip = Cerrar pestañas duplicadas CloseOtherTabs.tooltip = Cerrar las demás pestañas CloseTab.tooltip = Cerrar pestaña MoveTabToOtherPanel.tooltip = Mover pestaña al otro panel NextTab.label = Siguiente pestaña PreviousTab.label = Pestaña anterior version_dialog.no_new_version_title = No hay una versión nueva version_dialog.no_new_version = Felicitaciones, tienes la última versión. version_dialog.new_version_title = Nueva versión disponible version_dialog.new_version = Una nueva versión de trolCommander está disponible. version_dialog.new_version_url = Una nueva versión de trolCommander está disponible en %1. version_dialog.not_available_title = El servidor no está disponible version_dialog.not_available = Imposible obtener la versión desde el servidor. version_dialog.install_and_restart = Instalar y reiniciar version_dialog.preparing_for_update = Preparando actualización... quit_dialog.title = Salir de trolCommander quit_dialog.desc = Tienes %1 ventana(s) abierta(s). ¿Estás seguro de que deseas salir? quit_dialog.show_next_time = Mostrar la próxima vez destination_dialog.file_exists_action = Acción por defecto cuando el fichero existe destination_dialog.verify_integrity = Verificando integridad de datos destination_dialog.skip_errors = Ignorar los errores file_collision_dialog.title = Colisión de fichero rename_dialog.new_name = Nuevo nombre copy_dialog.destination = Copiar fichero(s) a copy_dialog.error_title = Error al copiar copy_dialog.copying = Copiando copy_dialog.copying_file = Copiando %1 pack_dialog.packing = Comprimiendo pack_dialog.packing_file = Comprimiendo %1 pack_dialog.error_title = Error de compresión pack_dialog_description = Añadir los ficheros seleccionados a pack_dialog.archive_format = Formato de fichero unpack_dialog.destination = Descomprimir fichero(s) seleccionado(s) en unpack_dialog.error_title = Error al descomprimir unpack_dialog.unpacking = Descomprimiendo unpack_dialog.unpacking_file = Descomprimiendo %1 optimizing_archive = Optimizando fichero %1 error_while_optimizing_archive = Error al comprimir el fichero %1 move_dialog.move_description = Mover a move_dialog.error_title = Error de movimiento move_dialog.moving = Moviendo ficheros move_dialog.moving_file = Moviendo %1 download_dialog.description = Descargar fichero a download_dialog.error_title = Error de descarga download_dialog.downloading = Descargando download_dialog.downloading_file = Descargando %1 mkfile_dialog.allocate_space = Tamaño delete_dialog.permanently_delete.confirmation = ¿Eliminar permanentemene el/los archivo(s) seleccionado(s)? delete_dialog.move_to_trash.confirmation = ¿Eliminar fichero(s) seleccionados? delete_dialog.move_to_trash.confirmation_details = Los ficheros serán movidos a la papelera. delete_dialog.move_to_trash.option = Mover a la papelera delete_dialog.move_to_trash.failed = Uno o más ficheros no pudieron moverse a la papelera de reciclaje delete_dialog.deleting = Eliminando delete_dialog.error_title = Error al eliminar delete.deleting_file = Eliminando %1 email_dialog.prefs_not_set_title = Correo no configurado email_dialog.prefs_not_set = Debes ajustar los parámatros de correo primero. email_dialog.from = De email_dialog.to = A email_dialog.subject = Asunto email_dialog.send = Enviar email_dialog.error_title = Error de envío email_dialog.read_error = Imposible leer ficheros en subdirectorios. email_dialog.sending = Enviando ficheros email.sending_file = Enviando %1 email.connecting_to_server = Conectando a %1 email.server_unavailable = Imposible conectar al servidor %1, comprueba tus preferencias de correo o inténtalo más tarde. email.connection_closed = Conexión cerrada por el servidor, el mensaje no ha sido enviado. email.goodbye_failed = Error al cerrar la conexión, el mensaje no ha sido enviado. email.send_file_error = Imposible enviar el fichero %1, el mensaje no ha sido enviado. split_file_dialog.error_title = Error dividiendo fichero split_file_dialog.file_to_split = Fichero a dividir split_file_dialog.target_directory = Directorio destino split_file_dialog.part_size = Tamaño de cada parte split_file_dialog.parts = Número de partes split_file_dialog.generate_CRC = Generar fichero CRC split_file_dialog.max_parts = El máximo número de partes permitido es %1 split_file_dialog.auto = Automático split_file_dialog.insert_new_media = Insertar un nuevo disco combine_files_dialog.error_title = Error uniendo ficheros combine_files_job.no_crc_file = Unión finalizada. Sin fichero CRC a verificar. combine_files_job.crc_read_error = Error leyendo fichero CRC. combine_files_job.crc_check_failed = Error de verificación de CRC: esperado %2, encontrado %1 combine_files_job.crc_ok = Unión finalizada. Verificación de CRC correcta. file_selection_dialog.mark = Marcar file_selection_dialog.unmark = Desmarcar file_selection_dialog.mark_description = Marcar ficheros cuyo nombre file_selection_dialog.unmark_description = Desmarcar ficheros cuyo nombre file_selection_dialog.case_sensitive = Sensible a las mayúsculas file_selection_dialog.include_folders = Incluir directorios progress_dialog.starting = Comenzando transferencia... progress_dialog.transferred = %1 transferidos a %2 progress_dialog.elapsed_time = Tiempo transcurrido progress_dialog.advanced = Avanzadas progress_dialog.current_speed = Velocidad actual progress_dialog.limit_speed = Limitar la velocidad progress_dialog.close_when_finished = Cerrar ventana al finalizar progress_dialog.processing_files = Procesando ficheros progress_dialog.processing_file = Procesando %1 progress_dialog.verifying_file = Comprobando %1 progress_dialog.job_finished = Trabajo finalizado progress_dialog.job_error = Error properties_dialog.file_properties = %1 Propiedades properties_dialog.contents = Contenido properties_dialog.calculating = Calculando.. calculate_checksum_dialog.checksum_algorithm = Algoritmo de checksum calculate_checksum_dialog.temporary_file = Fichero temporal change_date_dialog.now = Ahora change_date_dialog.specific_date = Fecha específica run_dialog.run_command_description = Ejecutar en el directorio actual run_dialog.run_in_home_description = Ejecutar en el directorio de inicio run_dialog.command_output = Salida del comando run_dialog.run = Ejecutar run_dialog.clear_history = Limpiar historial server_connect_dialog.server_type = Tipo de conexión server_connect_dialog.server = Servidor server_connect_dialog.share = Compartir server_connect_dialog.domain = Dominio server_connect_dialog.username = Nombre de usuario server_connect_dialog.initial_dir = Directorio inicial server_connect_dialog.port = Puerto server_connect_dialog.server_url = Dirección del servidor server_connect_dialog.http_url = Dirección del sitio web server_connect_dialog.connect = Conectar server_connect_dialog.protocol = Protocolo server_connect_dialog.nfs_version = Versión de NFS server_connect_dialog.private_key = Clave privada server_connect_dialog.passphrase = Contraseña ftp_connect.passive_mode = Activar modo pasivo ftp_connect.anonymous_user = Usuario anónimo ftp_connect.nb_connection_retries = Número de reintentos de conexión ftp_connect.retry_delay = Tiempo de espera entre reintentos (en segundos) http_connect.basic_authentication = HTTP Basic Authentication (opcional) server_connections_dialog.disconnect = Desconectar server_connections_dialog.connection_busy = Ocupado server_connections_dialog.connection_idle = Inactivo bonjour.bonjour_services = Servicios Bonjour bonjour.no_service_discovered = No se han descubierto servicios bonjour.bonjour_disabled = Bonjour deshabilitado auth_dialog.title = Autentificación auth_dialog.desc = Por favor, introduzca un nombre y una contraseña auth_dialog.server = Servidor auth_dialog.store_credentials = Almacenar usuario y contraseña (encriptación débil) auth_dialog.connect_as = Conectar como auth_dialog.authentication_failed = Error de autenticación sortable_list.move_up = Subir sortable_list.move_down = Bajar add_bookmark_dialog.add = Añadir edit_bookmarks_dialog.new = Nuevo file_viewer.view_error_title = Error de vista file_viewer.view_error = Imposible ver el fichero. file_viewer.file_menu = Archivo file_viewer.close = Cerrar file_viewer.large_file_warning = Este fichero puede que sea demasiado largo para llevar a cabo la operación. file_viewer.open_anyway = Ignorar y abrir text_viewer.edit = Editar text_viewer.copy = Copiar text_viewer.select_all = Seleccionar todo text_viewer.find = Buscar text_viewer.find_next = Buscar siguiente text_viewer.find_previous = Buscar anterior text_viewer.view = Vista text_viewer.line_wrap = Ajuste de línea text_viewer.line_numbers = Números de línea text_viewer.binary_file_warning = Parece ser un fichero binario image_viewer.controls_menu = Controles image_viewer.zoom_in = Acercar image_viewer.zoom_out = Alejar file_editor.edit_error_title = Error al editar file_editor.edit_error = Imposible editar el fichero. file_editor.save = Salvar file_editor.save_as = Salvar como... file_editor.save_warning = ¿Desea guardar los cambios realizados a este fichero antes de salir? file_editor.cannot_write = Imposible escribir el fichero. text_editor.cut = Cortar text_editor.paste = Pegar shortcuts_dialog.quick_search.start_search = Teclee cualquier carácter para comenzar una búsqueda rápida shortcuts_dialog.quick_search.cancel_search = Cancelar búsqueda rápida shortcuts_dialog.quick_search.remove_last_char = Eliminar último carácter de la cadena de búsqueda rápida shortcuts_dialog.quick_search.jump_to_previous = Ir al resultado previo de búsqueda rápida shortcuts_dialog.quick_search.jump_to_next = Ir al resultado siguiente de búsqueda rápida shortcuts_dialog.quick_search.mark_jump_next = Marcar/Desmarcar fichero actual e ir al resultado siguiente de búsqueda rápida theme_editor.title = Editor de tema theme_editor.folder_tab = Panel de directorio theme_editor.shell_tab = Consola theme_editor.shell_history_tab = Historial de terminal theme_editor.statusbar_tab = Barra de estado theme_editor.free_space = Espacio libre theme_editor.free_space.ok = OK theme_editor.free_space.warning = Aviso theme_editor.free_space.critical = Crítico theme_editor.locationbar_tab = Barra de ubicación theme_editor.editor_tab = Editor de ficheros theme_editor.font = Fuente theme_editor.active_panel = Activo theme_editor.inactive_panel = Inactivo theme_editor.general = General theme_editor.could_not_save_theme = Imposible escribir el tema %1 theme_editor.border = Borde theme_editor.background = Fondo theme_editor.alternate_background = Alternar fondo theme_editor.unfocused_background = Fondo (sin foco) theme_editor.quick_search.unmatched_file = Fichero sin encajar theme_editor.text = Texto theme_editor.progress = Progreso theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (sin foco) theme_editor.selected = Seleccionado theme_editor.selected_unfocused = Seleccionado (sin foco) theme_editor.color = Color theme_editor.colors = Colores theme_editor.plain_file = Fichero plano theme_editor.marked_file = Fichero marcado theme_editor.hidden_file = Fichero oculto theme_editor.folder = Directorio theme_editor.archive_file = Archivar fichero theme_editor.symbolic_link = Enlace simbólico theme_editor.header = Cabecera theme_editor.item = Elemento command_bar_customize_dialog.available_actions = Acciones disponibles command_bar_customize_dialog.modifier = Modificador prefs_dialog.title = Preferencias prefs_dialog.general_tab = General prefs_dialog.day = Día prefs_dialog.month = Mes prefs_dialog.year = Año prefs_dialog.language = Idioma (requiere reiniciar) prefs_dialog.date_time = Formato de fecha y hora prefs_dialog.time = Hora prefs_dialog.date = Fecha prefs_dialog.date_separator = Separador prefs_dialog.time_12_hour = Formato 12 horas prefs_dialog.time_24_hour = Formato 24 horas prefs_dialog.show_seconds = Mostrar segundos prefs_dialog.show_century = Mostrar siglo prefs_dialog.check_for_updates_on_startup = Comprobar actualizaciones al inicio prefs_dialog.show_splash_screen = Mostrar pantalla de bienvenida prefs_dialog.folders_tab = Directorios prefs_dialog.startup_folders = Directorios de inicio prefs_dialog.left_folder = Directorio de la izquierda prefs_dialog.right_folder = Directorio de la derecha prefs_dialog.last_folder = Último directorio visitado prefs_dialog.custom_folder = Directorio personalizado prefs_dialog.show_hidden_files = Mostrar ficheros ocultos prefs_dialog.show_ds_store_files = Mostrar ficheros .DS_Store prefs_dialog.show_system_folders = Mostrar directorios de sistema prefs_dialog.compact_file_size = Redondear el tamaño de los ficheros seleccionados prefs_dialog.follow_symlinks_when_cd = Seguir enlaces simbólicos al cambiar el directorio actual prefs_dialog.appearance_tab = Apariencia prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Tamaño de icono prefs_dialog.toolbar_icons = Barra de herramientas prefs_dialog.command_bar_icons = Barra de comandos prefs_dialog.file_icons = Tipos de ficheros prefs_dialog.use_system_file_icons = Usar iconos del sistema de ficheros prefs_dialog.use_system_file_icons.always = Siempre prefs_dialog.use_system_file_icons.never = Nunca prefs_dialog.use_system_file_icons.applications = Sólo para las aplicaciones prefs_dialog.edit_current_theme = Editar tema actual... prefs_dialog.themes = Temas prefs_dialog.import_theme = Importar tema prefs_dialog.import_look_and_feel = Importar look & feel prefs_dialog.no_look_and_feel = No se encontró look & feel alguno. prefs_dialog.error_in_import = Error al importar el tema %1. prefs_dialog.cannot_read_theme = No se pudo leer el tema del fichero %1 prefs_dialog.export_theme = Exportar %1 prefs_dialog.import = Importar prefs_dialog.export = Exportar prefs_dialog.theme_type = Tipo: %1 prefs_dialog.delete_theme = ¿Eliminar permanentemente el tema %1? prefs_dialog.delete_look_and_feel = Eliminar permanentemente el look & feel %1 ? prefs_dialog.rename_failed = Error al renombrar el tema %1 prefs_dialog.xml_file = Fichero XML prefs_dialog.jar_file = Fichero JAR prefs_dialog.mail_tab = Correo electrónico prefs_dialog.mail_settings = Parámetros de correo saliente prefs_dialog.mail_name = Su nombre prefs_dialog.mail_address = Su correo electrónico prefs_dialog.mail_server = Servidor SMTP prefs_dialog.misc_tab = Miscelánea prefs_dialog.use_brushed_metal = Usar la apariencia 'brocha de metal' (requiere volver a arrancar) prefs_dialog.confirm_on_quit = Mostrar diálogo de confirmación al salir prefs_dialog.default_shell = Utilizar shell predeterminada del sistema prefs_dialog.custom_shell = Utilizar shell específica prefs_dialog.shell_encoding = Codificación de terminal prefs_dialog.auto_detect_shell_encoding = Detección automática prefs_dialog.enable_bonjour_discovery = Activar servicios de descubrimiento de Bonjour prefs_dialog.enable_system_notifications = Activar las notificaciones del sistema debug_console_dialog.level = Nivel unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1me duration.years = %1a theme.custom_theme = Tema personalizado theme.custom = Personalizado theme.built_in = Preinstalado theme.add_on = Añadido theme.current = actual theme_could_not_be_loaded = Ocurrió un error mientras se cargaba éste tema. setup.title = Bienvenida/o a trolCommander setup.intro = Por favor, seleccione la forma en la que desea que trolCommander se comporte. setup.look_and_feel = Seleccione su Look & feel setup.theme = Seleccione su tema font_chooser.font_size = Tamaño font_chooser.font_bold = Negrita font_chooser.font_italic = Itálica color_chooser.red = Rojo color_chooser.green = Verde color_chooser.blue = Azul color_chooser.hue = Tonalidad color_chooser.brightness = Brillo color_chooser.swatches = Muestras color_chooser.saturation = Saturación color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Reciente color_chooser.alpha = Transparencia alpha color_chooser.title = Seleccionar un color batch_rename_dialog.mask = Patrón de renombrado batch_rename_dialog.search_replace = Buscar y reemplazar batch_rename_dialog.search_for = Buscar batch_rename_dialog.replace_with = Reemplazar con batch_rename_dialog.counter = Contador batch_rename_dialog.start_at = Comenzar en batch_rename_dialog.step_by = Incrementar en batch_rename_dialog.format = Formato batch_rename_dialog.upper_lower_case = Mayúsculas/Minúsculas batch_rename_dialog.no_change = Sin cambios batch_rename_dialog.lower_case = minúsculas batch_rename_dialog.upper_case = MAYÚSCULAS batch_rename_dialog.first_upper = Primera letra en mayúscula batch_rename_dialog.word = Primera Letra De Cada Palabra batch_rename_dialog.old_name = Nombre antiguo batch_rename_dialog.new_name = Nombre nuevo batch_rename_dialog.block_name = Preservar batch_rename_dialog.range = Intervalo batch_rename_dialog.proceed_renaming = Se renombrarán %1 ficheros de %2. ¿ Desea continuar ? batch_rename_dialog.duplicate_names = ¡Nombres duplicados! batch_rename_dialog.names_conflict = ¡Conflicto de nombres! Coinciden nombres antiguos y nuevos. parent_folders_quick_list.empty_message = La ubicación actual no tiene padre recent_locations_quick_list.empty_message = No hay ubicaciones recientes recent_executed_files_quick_list.empty_message = No hay ficheros ejecutados recientmente #server_connect_dialog.auth_error = Nombre o contraseña inválido #move_dialog.cannot_move_to_itself = Imposible mover los ficheros al subdirectorio. #pack.error_on_file = Error al comprimir %1 #pack_dialog.cannot_write = Imposible crear el fichero zip en el direcotorio de destino. #unpack.unable_to_open_zip = Error al abrir el fichero zip %1. #image_viewer.previous_image = Anterior #image_viewer.next_image = Siguiente #mkdir_dialog.title = Crear directorio #mkdir_dialog.error_title = Error al crear el directorio #edit_bookmarks_dialog.remove = Eliminar #mkdir_dialog.description = Crear directorio #done = Terminado #progress_dialog.hide = Ocultar #move_dialog.rename_description = Renombrar fichero a #theme_editor.shell_font = Fuente de consola #theme_editor.history_font = Fuente de historial #theme_editor.shell_colors = Colores de consola #theme_editor.history_colors = Colores del historial #ToggleHiddenFiles.hide = Ocultar ficheros ocultos #auth_dialog.error_was = El error era: %1 #table.hide_column = Ocultar columna #delete.symlink_warning_title = Enlace simbólico encontrado #delete.symlink_warning = El fichero parece un enlace simbólico:\n\n Fichero: %1\n Enlace a: %2\n\n¿Eliminar enlace simbólico sólo o\nseguir el enlace y elimiar el directorio (PRECAUCIÓN)? #delete.delete_link_only = Eliminar enlace #delete.delete_linked_folder = Eliminar directorio #Unpack.label = Descomprimir ficheros #Pack.label = Comprimir ficheros ================================================ FILE: src/main/resources/dictionary_fr_FR.properties ================================================ ok = OK yes = Oui no = Non cancel = Annuler edit = Editer close = Fermer reset = Réinitialiser rename = Renommer apply = Appliquer change = Changer save = Enregistrer dont_save = Ne pas enregistrer replace = Remplacer dont_replace = Ne pas remplacer delete = Supprimer skip = Ignorer skip_all = Ignorer tous retry = Réessayer resume = Reprendre overwrite = Remplacer overwrite_if_older = Remplacer si ancien duplicate = Dupliquer apply_to_all = Appliquer à tous copy = Copier move = Déplacer pack = Compresser unpack = Décompresser download = Télécharger split = Scinder combine = Aggréger browse = Explorer ask = Demander stop = Arrêter pause = Pause quick_search = Recherche rapide file_manager = Gestionnaire de fichiers create = Créer creating_file = Création de %1 en cours choose = Choisir customize = Personnaliser choose_folder = Choisir un répertoire login = Identifiant password = Mot de passe user = User encoding = Encodage preferred_encodings = Encodages préférés license = License name = Nom size = Taille date = Date extension = Extension permissions = Permissions owner = Propriétaire group = Groupe location = Emplacement untitled = Sans titre source = Source destination = Destination recurse_directories = Appliquer récursivement aux répertoires sélectionnés go_to = Aller à example = Exemple preview = Apperçu comment = Commentaire sample_text = Texte d'exemple nb_files = %1 fichier(s) nb_folders = %1 dossier(s) loading = Chargement en cours... this_operation_cannot_be_undone = Cette opération ne peut pas être annulée. remove = Retirer details = Détails warning = Avertissement error = Erreur generic_error = Une erreur est survenue pendant l'exécution de la tache demandée. folder_does_not_exist = Ce dossier n'existe pas ou n'est pas disponible. this_folder_does_not_exist = Ce dossier n'existe pas ou n'est pas disponible: %1 this_file_does_not_exist = Ce fichier n'existe pas ou n'est pas disponible: %1 invalid_path = Chemin invalide: %1 directory_already_exists = Le répertoire %1 existe déjà. file_exists_in_destination = Le fichier existe dans la destination source_parent_of_destination = Tentative de déplacement d'un répertoire vers un de ses sous-répertoires cannot_read_file = Erreur lors de la lecture de %1 cannot_write_file = Erreur lors de l'écriture de %1 cannot_create_folder = Erreur lors de la création du répertoire %1 cannot_read_folder = Erreur lors de la lecture du dossier %1 cannot_delete_file = Erreur lors de la suppression du fichier %1 cannot_delete_folder = Erreur lors de la suppression du répertoire %1 error_while_transferring = Erreur lors du transfert de %1 same_source_destination = Les répertoires source et destination sont les mêmes file_already_exists = %1 existe déjà, voulez-vous le remplacer ? write_error = Erreur d'écriture read_error = Erreur de lecture integrity_check_error = Echec du contrôle d'intégrité: les fichiers source et destination sont différents startup_error = Une erreur a empêché trolCommander de démarrer. permissions.read = Lecture permissions.write = Ecriture permissions.executable = Execution permissions.group = Groupe permissions.other = Autre permissions.octal_notation = Notation octale action_categories.all = Tous action_categories.navigation = Navigation action_categories.selection = Selection action_categories.view = Vue action_categories.file_operations = Opérations sur fichiers action_categories.windows = Fenêtres Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Ajouter aux favoris AddBookmark.tooltip = Ajouter le dossier courant à la liste de favoris BatchRename.label = Renommage multiple EditBookmarks.label = Éditer les favoris ExploreBookmarks.label = Explorer les favoris EditCredentials.label = Éditer les indentifiants ChangeLocation.label = Changer le chemin courant ChangeDate.label = Changer la date ChangeDate.tooltip = Change la date des fichiers selectionnés ChangePermissions.label = Changer les permissions ChangePermissions.tooltip = Change les permissions des fichiers selectionnés CheckForUpdates.label = Mises à jour CompareFolders.label = Comparer dossiers ConnectToServer.label = Connexion à un serveur ConnectToServer.tooltip = Se connecter à un serveur distant View.label = Voir InternalView.label = Voir (interne) View.tooltip = Voir le fichier sélectionné InternalEdit.label = Editer (interne) Edit.tooltip = Editer le fichier selectionné Copy.tooltip = Copier les fichiers marqués LocalCopy.label = Copier local LocalCopy.tooltip = Copier le fichier selectionné dans le dossier courant Move.tooltip = Déplacer les fichiers marqués Rename.tooltip = Renommer le fichier selectionné Mkdir.label = Créer rep Mkdir.tooltip = Créer un répertoire dans le dossier courant Mkfile.label = Créer fichier Mkfile.tooltip = Créer un fichier dans le dossier courant Delete.tooltip = Supprimer les fichiers marqués en utilisant la corbeille système si possible PermanentDelete.label = Supprimer définitivement PermanentDelete.tooltip = Supprimer les fichiers marqués sans utiliser la corbeille système Refresh.label = Actualiser Refresh.tooltip = Actualiser le répertoire courant CloseWindow.label = Fermer fenêtre CloseWindow.tooltip = Fermer cette fenêtre CopyFileNames.label = Copier nom(s) CopyFilePaths.label = Copier chemin(s) CopyFilesToClipboard.label = Copier fichier(s) PasteClipboardFiles.label = Coller fichier(s) Email.label = Envoyer par email Email.tooltip = Envoyer les fichiers marqués en pièces jointes à un message GoBack.label = Dossier précédent GoBack.tooltip = Aller au dossier précédent GoForward.label = Dossier suivant GoForward.tooltip = Aller au dossier suivant GoToHome.label = Aller au dossier utilisateur GoToParent.label = Dossier parent GoToParent.tooltip = Remonter au dossier parent GoToParentInOtherPanel.label = Remonter dans l'autre panneau GoToParentInBothPanels.label = Remonter dans les deux panneaux GoToRoot.label = Aller à la racine SortByName.label = Trier par Nom SortByDate.label = Trier par Date SortBySize.label = Trier par Taille SortByExtension.label = Trier par Extension SortByPermissions.label = Trier par Permissions SortByOwner.label = Trier par Propriétaire SortByGroup.label = Trier par Groupe MarkGroup.label = Marquer fichiers MarkGroup.tooltip = Marquer un groupe de fichiers UnmarkGroup.label = Dé-marquer fichiers UnmarkGroup.tooltip = Dé-marquer un groupe de fichiers MarkAll.label = Marquer tous UnmarkAll.label = Dé-marquer tous MarkSelectedFile.label = Marquer/dé-marquer MarkSelectedFile.tooltip = Marquer/Dé-marquer le fichier sélectionné MarkNextBlock.label = Marquer un bloc vers le bas MarkPreviousBlock.label = Marquer un bloc vers le haut MarkNextRow.label = Marquer une ligne vers le bas MarkPreviousRow.label = Marquer une ligne vers le haut MarkNextPage.label = Marquer une page vers le bas MarkPreviousPage.label = Marquer une page vers le haut MarkToFirstRow.label = Marquer les fichiers jusqu'au début MarkToLastRow.label = Marquer les fichiers jusqu'à la fin MarkExtension.label = Marquer l'extension InvertSelection.label = Inverser sélection SwapFolders.label = Echanger les dossiers SwapFolders.tooltip = Echanger les dossiers de gauche et de droite SetSameFolder.label = Affecter le même dossier SetSameFolder.tooltip = Affecter le même repertoire aux panneaux de gauche et de droite NewWindow.label = Nouvelle fenêtre NewWindow.tooltip = Ouvrir une nouvelle fenêtre Open.label = Ouvrir Open.tooltip = Entrer dans un dossier / Entrer dans une archive / Exécuter OpenNatively.label = Ouvrir nativement OpenNatively.tooltip = Exécuter le fichier sélectionné avec les associations de fichiers du système OpenInOtherPanel.label = Ouvrir dans l'autre panneau OpenInBothPanels.label = Ouvrir dans les deux panneaux RevealInDesktop.label = Afficher dans %1 RunCommand.label = Exécuter une commande RunCommand.tooltip = Exécuter une commande dans le dossier courant Pack.tooltip = Compresser les fichiers marqués dans une archive Unpack.tooltip = Décompresser les archives marquées ShowFileProperties.label = Propriétés ShowFileProperties.tooltip = Afficher les propriétés des fichiers marqués ShowPreferences.label = Préférences ShowPreferences.tooltip = Configurer trolCommander ShowServerConnections.label = Afficher les connexions ouvertes Quit.label = Quitter ReverseSortOrder.label = Inverser l'ordre ToggleAutoSize.label = Auto-dimensionner les colonnes Stop.label = Arrêter le changement de dossier ToggleColumn.show = Afficher la colonne %1 ToggleColumn.hide = Cacher la colonne %1 ToggleCommandBar.show = Afficher la barre de commande ToggleCommandBar.hide = Cacher la barre de commande ToggleToolBar.show = Afficher la barre d'outils ToggleToolBar.hide = Cacher la barre d'outils CustomizeCommandBar.label = Personnaliser la barre de commande ToggleStatusBar.show = Afficher la barre de status ToggleStatusBar.hide = Cacher la barre de status ToggleShowFoldersFirst.label = Afficher d'abord les répertoires ToggleTree.label = Vue arborescente PopupLeftDriveButton.label = Changer le dossier de gauche PopupRightDriveButton.label = Changer le dossier de droite RecallPreviousWindow.label = Rappeler la fenêtre précédente RecallNextWindow.label = Rappeler la fenêtre suivante RecallWindow.label = Rappeler la fenêtre #%1 BringAllToFront.label = Tout ramener au premier plan SwitchActiveTable.label = Basculer entre les dossiers de droite et de gauche SelectNextBlock.label = Descendre d'un bloc SelectPreviousBlock.label = Remonter d'un bloc SelectNextPage.label = Descendre d'une page SelectPreviousPage.label = Remonter d'une page SelectNextRow.label = Descendre d'une ligne SelectPreviousRow.label = Remonter d'une ligne SelectFirstRow.label = Sélectionner le premier fichier du dossier SelectLastRow.label = Sélectionner le dernier fichier du dossier SplitEqually.label = Scinder au milieu SplitVertically.label = Scincer verticalement SplitHorizontally.label = Scincer horizontalement ShowKeyboardShortcuts.label = Raccourcis clavier GoToWebsite.label = Site web GoToForums.label = Forums ReportBug.label = Reporter a bug Donate.label = Faire une donation ShowAbout.label = A propos de trolCommander OpenTrash.label = Ouvrir la Corbeille EmptyTrash.label = Vider la Corbeille CalculateChecksum.label = Calculer la checksum MaximizeWindow.label = Agrandir MinimizeWindow.label = Réduire GoToDocumentation.label = Documentation en ligne ShowParentFoldersQL.label = Dossiers parents ShowRecentLocationsQL.label = Dossiers récents ShowRecentExecutedFilesQL.label = Fichiers récemment exécutés SplitFile.tooltip = Scinder un fichier en plusieurs parties CombineFiles.tooltip = Aggréger des fichiers scindés pour recréer le fichier original ShowDebugConsole.label = Console de débug FocusPrevious.label = Sélectionner le précédent composant FocusNext.label = Sélectionner le prochain composant file_menu = Fichier file_menu.open_with = Ouvrir avec mark_menu = Marquer view_menu = Vue view_menu.show_hide_columns = Afficher/Cacher colonnes go_menu = Go bookmarks_menu = Favoris bookmarks_menu.no_bookmark = Pas de favori drive_popup.network_shares = Partages réseau quick_lists_menu = Listes éclair window_menu = Fenêtre help_menu = Aide status_bar.selected_files = %1 de %2 sélectionnés status_bar.connecting_to_folder = Connexion en cours, appuyer sur ESCAPE pour annuler status_bar.volume_free = Libre: %1 status_bar.volume_capacity = Capacité: %1 shortcuts_panel.title = Raccourcis shortcuts_panel.restore_defaults = Réinitialiser shortcuts_panel.show = Afficher shortcuts_panel.default_message = Appuyez sur Entrée ou double-cliquez sur le raccourci à éditer shortcuts_table.action_description = Description de l'action shortcuts_table.shortcut = Raccourci shortcuts_table.alternate_shortcut = Raccourci secondaire shortcuts_table.type_in_a_shortcut = Saisissez le raccourci command_bar_dialog.help = Déplacez les boutons pour personnaliser la barre de commande table.folder_access_error_title = Erreur d'accès au répertoire table.folder_access_error = Le contenu du dossier ne peut être lu table.download_or_browse = Souhaitez-vous explorer ou télécharger ce fichier ? version_dialog.no_new_version_title = Pas de nouvelle version version_dialog.no_new_version = Félicitations, vous avez déjà la dernière version. version_dialog.new_version_title = Nouvelle version disponible version_dialog.new_version = Une nouvelle version de trolCommander est disponible. version_dialog.new_version_url = Une nouvelle version de trolCommander est disponible à %1. version_dialog.not_available_title = Serveur indisponible version_dialog.not_available = L'information de version n'a pas pu être récupérée. version_dialog.install_and_restart = Installer et redémarrer version_dialog.preparing_for_update = Préparation de la mise à jour... quit_dialog.title = Quitter trolCommander quit_dialog.desc = Il y a %1 fenêtre(s) ouverte(s). Etes-vous sûr de vouloir quitter ? quit_dialog.show_next_time = Afficher la prochaine fois destination_dialog.file_exists_action = Action par défaut lorsqu'un fichier existe destination_dialog.verify_integrity = Vérifier l'intégrité des données destination_dialog.skip_errors = Ignorer les erreurs file_collision_dialog.title = Collision de fichiers rename_dialog.new_name = Nouveau nom copy_dialog.destination = Copier les fichiers sélectionnés dans copy_dialog.error_title = Erreur de copie copy_dialog.copying = Copie en cours copy_dialog.copying_file = Copie de %1 en cours pack_dialog.packing = Compression en cours pack_dialog.packing_file = Compression de %1 en cours pack_dialog.error_title = Erreur de compression pack_dialog_description = Ajouter les fichiers sélectionnés dans pack_dialog.archive_format = Format de l'archive unpack_dialog.destination = Décompresser les fichiers selectionnés dans unpack_dialog.error_title = Erreur de décompression unpack_dialog.unpacking = Décompression en cours unpack_dialog.unpacking_file = Décompression de %1 en cours optimizing_archive = Optimisation de l'archive %1 error_while_optimizing_archive = Erreur pendant l'optimisation de %1 move_dialog.move_description = Déplacer vers move_dialog.error_title = Erreur de déplacement move_dialog.moving = Déplacement en cours move_dialog.moving_file = Déplacement de %1 en cours download_dialog.description = Télécharger le fichier dans download_dialog.error_title = Erreur de téléchargement download_dialog.downloading = Téléchargement en cours download_dialog.downloading_file = Téléchargement de %1 en cours mkfile_dialog.allocate_space = Allouer de l'espace delete_dialog.permanently_delete.confirmation = Supprimer définitivement les fichiers sélectionnés ? delete_dialog.move_to_trash.confirmation = Supprimer les fichiers sélectionnés ? delete_dialog.move_to_trash.confirmation_details = Les fichiers seront déplacés dans la corbeille. delete_dialog.move_to_trash.option = Déplacer dans la corbeille delete_dialog.move_to_trash.failed = Un ou plusieurs fichiers n'ont pu être déplacés dans la corbeille. delete_dialog.deleting = Suppression en cours delete_dialog.error_title = Erreur de suppression delete.deleting_file = Suppression de %1 en cours email_dialog.prefs_not_set_title = Paramètres absents email_dialog.prefs_not_set = Vous devez d'abord configurer vos paramètres email. email_dialog.from = De email_dialog.to = A email_dialog.subject = Sujet email_dialog.send = Envoyer email_dialog.error_title = Erreur d'envoi email_dialog.read_error = Les fichiers ne peuvent être lus. email_dialog.sending = Envoi en cours email.sending_file = Envoi de %1 email.connecting_to_server = Connexion à %1 email.server_unavailable = Echec de la connexion au serveur %1, vérifiez vos préférences de mail ou réessayez plus tard. email.connection_closed = Connexion fermée par le serveur, le message n'a pas été envoyé. email.goodbye_failed = Erreur lors de la fermeture de connexion, le message n'a peut-être pas été envoyé. email.send_file_error = Erreur lors de l'envoi du fichier %1, le message n'a pas été envoyé. split_file_dialog.error_title = Erreur de scindage split_file_dialog.file_to_split = Fichier à scinder split_file_dialog.target_directory = Répertoire cible split_file_dialog.part_size = Taille d'une partie split_file_dialog.parts = Nombre de parties split_file_dialog.generate_CRC = Générer un fichier CRC split_file_dialog.max_parts = Le nombre maximum de parties authorisé est de %1 split_file_dialog.auto = Automatique split_file_dialog.insert_new_media = Insérer un nouveau disque combine_files_dialog.error_title = Erreur d'agrégation combine_files_job.no_crc_file = Agrégation terminée avec succès. Pas de fichier CRC. combine_files_job.crc_read_error = Erreur lors de la lecture du fichier CRC. combine_files_job.crc_check_failed = Échec vérification CRC (%1 au lieu de %2). combine_files_job.crc_ok = Agrégation terminée avec succès. Vérification CRC OK. file_selection_dialog.mark = Marquer file_selection_dialog.unmark = Dé-marquer file_selection_dialog.mark_description = Marquer les fichiers dont le nom file_selection_dialog.unmark_description = Dé-marquer les fichiers dont le nom file_selection_dialog.case_sensitive = Respecter la casse file_selection_dialog.include_folders = Inclure les dossiers progress_dialog.starting = Démarrage du transfert... progress_dialog.transferred = %1 transférés à %2 progress_dialog.elapsed_time = Temps écoulé progress_dialog.advanced = Avancé progress_dialog.current_speed = Vitesse actuelle progress_dialog.limit_speed = Limiter la vitesse progress_dialog.close_when_finished = Fermer la fenêtre lorsque terminé progress_dialog.processing_files = Traitement des fichiers en cours progress_dialog.processing_file = Traitement de %1 en cours progress_dialog.verifying_file = Vérification de %1 progress_dialog.job_finished = Tâche terminée progress_dialog.job_error = Erreur pendant tâche properties_dialog.file_properties = Propriétés de %1 properties_dialog.contents = Contenu properties_dialog.calculating = Calcul en cours... calculate_checksum_dialog.checksum_algorithm = Algorithme de checksum calculate_checksum_dialog.temporary_file = Fichier temporaire change_date_dialog.now = Maintenant change_date_dialog.specific_date = Date spécifique run_dialog.run_command_description = Exécuter dans le répertoire courant run_dialog.run_in_home_description = Exécuter dans le répertoire utilisateur run_dialog.command_output = Sortie de la commande run_dialog.run = Exécuter run_dialog.clear_history = Effacer l'historique server_connect_dialog.server_type = Type de connexion server_connect_dialog.server = Serveur server_connect_dialog.share = Partage server_connect_dialog.domain = Domaine server_connect_dialog.username = Nom d'utilisateur server_connect_dialog.initial_dir = Répertoire initial server_connect_dialog.port = Port server_connect_dialog.server_url = URL serveur server_connect_dialog.http_url = URL site web server_connect_dialog.connect = Se connecter server_connect_dialog.protocol = Protocole server_connect_dialog.nfs_version = Version de NFS server_connect_dialog.private_key = Clé privée server_connect_dialog.passphrase = Phrase secrète ftp_connect.passive_mode = Activer le mode passif ftp_connect.anonymous_user = Utilisateur anonyme ftp_connect.nb_connection_retries = Nombre de re-tentatives de connexion ftp_connect.retry_delay = Délai entre tentatives de connexion (en secondes) http_connect.basic_authentication = HTTP Basic Authentication (optionnel) server_connections_dialog.disconnect = Déconnecter server_connections_dialog.connection_busy = Occupée server_connections_dialog.connection_idle = Inactive bonjour.bonjour_services = Services Bonjour bonjour.no_service_discovered = Aucun service découvert bonjour.bonjour_disabled = Bonjour disabled auth_dialog.title = Authentification auth_dialog.desc = Veuillez entrer un identifiant et un mot de passe auth_dialog.server = Serveur auth_dialog.store_credentials = Sauver les identifiants (encryption faible) auth_dialog.connect_as = Se connecter en tant que auth_dialog.authentication_failed = L'authentification a échoué sortable_list.move_up = Monter sortable_list.move_down = Descendre add_bookmark_dialog.add = Ajouter edit_bookmarks_dialog.new = Nouveau file_viewer.view_error_title = Erreur d'affichage file_viewer.view_error = Impossible de voir le fichier. file_viewer.file_menu = Fichier file_viewer.close = Fermer file_viewer.large_file_warning = Ce fichier est peut-être trop large pour cette operation. file_viewer.open_anyway = Ignorer et ouvrir text_viewer.edit = Edition text_viewer.copy = Copier text_viewer.select_all = Tout sélectionner text_viewer.find = Rechercher text_viewer.find_next = Rechercher suivant text_viewer.find_previous = Rechercher précédent text_viewer.binary_file_warning = Ce fichier a l'air d'être un fichier binaire image_viewer.controls_menu = Contrôles image_viewer.zoom_in = Zoomer image_viewer.zoom_out = Dézoomer file_editor.edit_error_title = Erreur d'édition file_editor.edit_error = Impossible d'éditer le fichier. file_editor.save = Enregistrer file_editor.save_as = Enregistrer sous... file_editor.save_warning = Enregistrer les modifications avant de fermer ce fichier ? file_editor.cannot_write = Impossible d'écrire le fichier. text_editor.cut = Couper text_editor.paste = Coller shortcuts_dialog.quick_search.start_search = Entrez un caractère pour commencer une recherche rapide shortcuts_dialog.quick_search.cancel_search = Annuler la recherche rapide shortcuts_dialog.quick_search.remove_last_char = Supprimer le dernier caractère de recherche shortcuts_dialog.quick_search.jump_to_previous = Aller au résultat précédent shortcuts_dialog.quick_search.jump_to_next = Aller au résultat suivant shortcuts_dialog.quick_search.mark_jump_next = Marquer/Dé-marquer le fichier courant et aller au résultat suivant theme_editor.title = Editeur de thèmes theme_editor.folder_tab = Panneau de dossier theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Historique du shell theme_editor.statusbar_tab = Barre de status theme_editor.free_space = Espace libre theme_editor.free_space.ok = OK theme_editor.free_space.warning = Avertissement theme_editor.free_space.critical = Critique theme_editor.locationbar_tab = Barre d'emplacement theme_editor.editor_tab = Editeur de fichiers theme_editor.font = Fonte theme_editor.active_panel = Actif theme_editor.inactive_panel = Inactif theme_editor.general = General theme_editor.could_not_save_theme = Erreur lors de l'écriture du thème %1 theme_editor.border = Bordure theme_editor.background = Fond theme_editor.alternate_background = Fond alterné theme_editor.unfocused_background = Fond (sans focus) theme_editor.quick_search.unmatched_file = Fichier ne correspondant pas theme_editor.text = Texte theme_editor.progress = Progression theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (sans focus) theme_editor.selected = Sélectionné theme_editor.selected_unfocused = Sélectionné (without focus) theme_editor.color = Couleur theme_editor.colors = Couleurs theme_editor.plain_file = Fichier normal theme_editor.marked_file = Fichier marqué theme_editor.hidden_file = Fichier caché theme_editor.folder = Dossier theme_editor.archive_file = Archive theme_editor.symbolic_link = Lien symbolique theme_editor.header = En-tête theme_editor.item = Élément command_bar_customize_dialog.available_actions = Actions disponibles command_bar_customize_dialog.modifier = Modificateur prefs_dialog.title = Préférences prefs_dialog.general_tab = Général prefs_dialog.day = Jour prefs_dialog.month = Mois prefs_dialog.year = Année prefs_dialog.language = Langage (après redémarrage) prefs_dialog.date_time = Format de date et d'heure prefs_dialog.time = Heure prefs_dialog.date = Date prefs_dialog.date_separator = Séparateur prefs_dialog.time_12_hour = Format 12 heures prefs_dialog.time_24_hour = Format 24 heures prefs_dialog.show_seconds = Afficher les secondes prefs_dialog.show_century = Afficher le siècle prefs_dialog.check_for_updates_on_startup = Chercher les mises à jour au démarrage prefs_dialog.show_splash_screen = Afficher l'écran de chargement prefs_dialog.folders_tab = Dossiers prefs_dialog.startup_folders = Dossiers de démarrage prefs_dialog.left_folder = Dossier de gauche prefs_dialog.right_folder = Dossier de droite prefs_dialog.last_folder = Dernier dossier visité prefs_dialog.custom_folder = Dossier spécifique prefs_dialog.show_hidden_files = Afficher les fichiers cachés prefs_dialog.show_ds_store_files = Afficher les fichiers .DS_Store prefs_dialog.show_system_folders = Afficher les dossiers système prefs_dialog.compact_file_size = Arrondir les tailles de fichiers affichées prefs_dialog.follow_symlinks_when_cd = Suivre les liens symboliques en changeant de répertoire courant prefs_dialog.appearance_tab = Apparence prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Taille des icônes prefs_dialog.toolbar_icons = Barre d'outils prefs_dialog.command_bar_icons = Barre de commande prefs_dialog.file_icons = Type de fichiers prefs_dialog.use_system_file_icons = Utiliser les icônes de fichier système prefs_dialog.use_system_file_icons.always = Toujours prefs_dialog.use_system_file_icons.never = Jamais prefs_dialog.use_system_file_icons.applications = Seulement pour les applications prefs_dialog.edit_current_theme = Editer le thème courrant... prefs_dialog.themes = Thèmes prefs_dialog.import_theme = Importer un thème prefs_dialog.import_look_and_feel = Importer un look & feel prefs_dialog.no_look_and_feel = Aucun look & feel n'a été trouvé. prefs_dialog.error_in_import = Une erreur est survenue lors de l'import du thème %1. prefs_dialog.cannot_read_theme = Erreur lors de la lecture du fichier %1 prefs_dialog.export_theme = Exporter %1 prefs_dialog.import = Importer prefs_dialog.export = Exporter prefs_dialog.theme_type = Type: %1 prefs_dialog.delete_theme = Supprimer définitivement le thème %1 ? prefs_dialog.delete_look_and_feel = Supprimer définitivement le look & feel %1 ? prefs_dialog.rename_failed = Impossible de renommer le thème %1 prefs_dialog.xml_file = Fichier XML prefs_dialog.jar_file = Fichier JAR prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Paramètres d'email sortant prefs_dialog.mail_name = Votre nom prefs_dialog.mail_address = Votre adresse email prefs_dialog.mail_server = Serveur SMTP prefs_dialog.misc_tab = Autres prefs_dialog.use_brushed_metal = Utiliser le look 'brushed metal' (après redémarrage) prefs_dialog.confirm_on_quit = Afficher un dialogue de confirmation en quittant prefs_dialog.default_shell = Utiliser le shell par défaut du système prefs_dialog.custom_shell = Utiliser un shell spécifique prefs_dialog.shell_encoding = Encodage du shell prefs_dialog.auto_detect_shell_encoding = Détection automatique prefs_dialog.enable_bonjour_discovery = Activer la découverte des services Bonjour prefs_dialog.enable_system_notifications = Activer les notifications système debug_console_dialog.level = Niveau unit.byte = octet unit.bytes = octets unit.bytes_short = o unit.kb = Ko unit.mb = Mo unit.gb = Go unit.tb = To unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1j duration.months = %1mo duration.years = %1a theme.custom_theme = Thème personnalisé theme.custom = Personnalisé theme.built_in = Pré-installé theme.add_on = Ajouté theme.current = courant theme_could_not_be_loaded = Une erreur est survenue lors du chargement de ce thème. setup.title = Bienvenue dans trolCommander setup.intro = Veuillez choisir le comportement souhaité de trolCommander. setup.look_and_feel = Selectionnez votre Look & Feel setup.theme = Selectionnez votre thème font_chooser.font_size = Taille font_chooser.font_bold = Gras font_chooser.font_italic = Italique color_chooser.red = Rouge color_chooser.green = Vert color_chooser.blue = Bleu color_chooser.hue = Tonalité color_chooser.brightness = Luminosité color_chooser.swatches = Echantillons color_chooser.saturation = Saturation color_chooser.rgb = RVB color_chooser.hsb = TSL color_chooser.recent = Récentes color_chooser.alpha = Transparence alpha color_chooser.title = Choisir une couleur batch_rename_dialog.mask = Modèle de renommage batch_rename_dialog.search_replace = Chercher & Remplacer batch_rename_dialog.search_for = Chercher batch_rename_dialog.replace_with = Remplacer par batch_rename_dialog.counter = Compteur batch_rename_dialog.start_at = Commencer à batch_rename_dialog.step_by = Incrément batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Casse batch_rename_dialog.no_change = Inchangée batch_rename_dialog.lower_case = minuscule batch_rename_dialog.upper_case = MAJUSCULE batch_rename_dialog.first_upper = Première lettre majuscule batch_rename_dialog.word = Première De Chaque Mot batch_rename_dialog.old_name = Ancien nom batch_rename_dialog.new_name = Nouveau nom batch_rename_dialog.block_name = Conserver batch_rename_dialog.range = Intervalle batch_rename_dialog.proceed_renaming = %1 fichiers sur %2 vont être renommés. Etes-vous sûr? batch_rename_dialog.duplicate_names = Doublons de noms! parent_folders_quick_list.empty_message = Le dossier courant n'a pas de parent recent_locations_quick_list.empty_message = Pas de dossier récent recent_executed_files_quick_list.empty_message = Pas de fichier récemment exécuté #server_connect_dialog.auth_error = Identifiant ou mot de passe incorrect. #move_dialog.cannot_move_to_itself = Impossible de déplacer les fichiers vers un sous-répertoire. #pack.error_on_file = Erreur lors de la compression de %1 #pack_dialog.cannot_write = Impossible de créer le fichier dans le dossier de destination. #unpack.unable_to_open_zip = Erreur lors de l'ouverture du fichier zip %1. #image_viewer.previous_image = Image précédente #image_viewer.next_image = Image suivante #mkdir_dialog.title = Créer répertoire #mkdir_dialog.error_title = Erreur de création #edit_bookmarks_dialog.remove = Supprimer #mkdir_dialog.description = Créer le répertoire #mkfile_dialog.description = Créer le fichier vide #done = Terminé #progress_dialog.hide = Cacher #move_dialog.rename_description = Renommer en #theme_editor.shell_font = Fonte du shell #theme_editor.history_font = Fonte de l'historique #theme_editor.shell_colors = Couleurs du shell #theme_editor.history_colors = Couleurs de l'historique #ToggleHiddenFiles.hide = Ne pas afficher les fichiers cachés #auth_dialog.error_was = L'erreur était: %1 #table.hide_column = Cacher la colonne #delete.symlink_warning_title = Lien symbolique #delete.symlink_warning = Ce fichier a l'air d'être un lien symbolique:\n\n Fichier: %1\n Pointe vers: %2\n\nSupprimer le lien seulement ou\nSuivre le lien symbolique et supprimer le répertoire (ATTENTION) ? #delete.delete_link_only = Supprimer le lien #delete.delete_linked_folder = Supprimer le répertoire #Unpack.label = Décompresser fichiers #Pack.label = Compresser fichiers ================================================ FILE: src/main/resources/dictionary_hu_HU.properties ================================================ ok = OK yes = Igen no = Nem cancel = Mégse edit = Szerkesztés close = Bezár reset = Alaphelyzet rename = Átnevez apply = Alkalmaz change = Csere save = Mentés dont_save = Mentés nélkül replace = Csere dont_replace = Csere nélkül delete = Törlés skip = Kihagy skip_all = Mindet kihagy retry = Újra resume = Folytat overwrite = Felülír overwrite_if_older = Felülír, ha régebbi duplicate = Megkettőz apply_to_all = Mindhez alkalmaz copy = Másol move = Mozgat pack = Tömörítés unpack = Kicsomagolás download = Letölt split = Fájldarabolás combine = Fájlok összefűzése browse = Tallóz ask = Kérdez stop = Megállít pause = Szünet quick_search = Gyorskeresés file_manager = Fájlkezelő create = Létrehoz creating_file = %1 Létrehozása choose = Kiválaszt customize = Beállítás choose_folder = Könyvtár választás login = Login password = Jelszó user = Felhasználó encoding = Kódolás preferred_encodings = Ajánlott kódolás license = Licenc name = Név size = Méret date = Dátum extension = Típus permissions = Engedély owner = Tulajdonos group = Csoport location = Hely untitled = Névtelen source = Forrás destination = Cél recurse_directories = A kiválasztott könyvtárakra rekurzívan alkalmaz go_to = Ugrás example = Példa preview = Előnézet comment = Megjegyzés sample_text = Példa szöveg nb_files = %1 fájl nb_folders = %1 könyvtár loading = Betöltés... this_operation_cannot_be_undone = A művelet nem vonható vissza. remove = Mozgat details = Részletek warning = Figyelem error = Hiba generic_error = Hiba történt a művelet elvégzése alatt. folder_does_not_exist = A könyvtár nem létezik vagy nem található. this_folder_does_not_exist = A könyvtár nem létezik vagy nem elérhető: %1 this_file_does_not_exist = A fájl nem létezik vagy nem elérhető: %1 invalid_path = Érvénytelen útvonal: %1 directory_already_exists = %1 könyvtár már létezik. file_exists_in_destination = A fájl már létezik a megadott helyen source_parent_of_destination = A könyvtárat saját alkönyvtárába próbálta másolni cannot_read_file = Nem olvasható fájl %1 cannot_write_file = Nem írható fájl %1 cannot_create_folder = Nem lehet a könyvtárat létrehozni %1 cannot_read_folder = Nem lehet a könyvtár tartalmát olvasni %1 cannot_delete_file = Nem lehet a fájlt törölni %1 cannot_delete_folder = Nem lehet a könyvtárat törölni %1 error_while_transferring = Hiba fájlátvitel közben %1 same_source_destination = A forrás és a célkönyvtár azonos file_already_exists = %1 már létezik, lecseréli? write_error = Írási hiba read_error = Olvasási hiba integrity_check_error = Hiba az ellenőrzéskor: A forrás és a cél nem azonos startup_error = Váratlan hiba fordult elő a trolCommander indulásakor. permissions.read = Olvasás permissions.write = Írás permissions.executable = Végrehajtás permissions.group = Csoport permissions.other = Egyéb permissions.octal_notation = Oktális jelölés action_categories.all = Összes action_categories.navigation = Navigáció action_categories.selection = Kiválasztás action_categories.view = Megjelenítés action_categories.file_operations = Fájl műveletek action_categories.windows = Ablak Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Hozzáadás a Könyvjelzőkhöz AddBookmark.tooltip = Az aktuális könyvtár hozzáadása a Könyvjelzőkhöz BatchRename.label = Csoportos átnevezés EditBookmarks.label = Könyvjelzők rendezése ExploreBookmarks.label = Könyvjelzők átvizsgálása EditCredentials.label = Megbízólevél javítása ChangeLocation.label = Az aktuális hely módosítása ChangeDate.label = Dátum módosítása ChangeDate.tooltip = A kijelölt fájl(ok) módosítása ChangePermissions.label = Jogosultságok módosítása ChangePermissions.tooltip = A kijelölt fájl(ok) jogosultságainak módosítása CheckForUpdates.label = Frissítések keresése CompareFolders.label = Könyvtárak összehasonlítása ConnectToServer.label = Csatlakozás a szerverhez ConnectToServer.tooltip = Kapcsolodás a távoli szerverhez View.label = Megnéz InternalView.label = Megnéző (beépített) View.tooltip = A kijelölt fájl megtekintése InternalEdit.label = Szerkesztő (beépített) Edit.tooltip = A kijelölt fájl módosítása Copy.tooltip = A kijelölt fájlok másolása LocalCopy.label = Lokális másolás LocalCopy.tooltip = A kijelölt fájlok másolása az aktuális könyvtárba Move.tooltip = A kijelölt fájlok mozgatása Rename.tooltip = A kijelölt fájl átnevezése Mkdir.label = Új könyvtár Mkdir.tooltip = Új könyvtár létrehozása az aktuális könyvtárban Mkfile.label = Fájl létrehozása Mkfile.tooltip = Fájl létrehozása az aktuális könyvtárban Delete.tooltip = A kijelölt fájlok lomtárba helyezése, ha lehetséges PermanentDelete.label = Végleges törlés PermanentDelete.tooltip = A kijelölt fájlok végleges törlése a Lomtár használata nélkül Refresh.label = Frissít Refresh.tooltip = Az aktuális könyvtár frissítése CloseWindow.label = Ablak bezárása CloseWindow.tooltip = Az ablak bezárása CopyFileNames.label = Fájlnevek másolása CopyFilePaths.label = Útvonalak másolása CopyFilesToClipboard.label = Fájl(ok) másolása PasteClipboardFiles.label = Fájl(ok) beillesztése Email.label = Küldés e-mailben Email.tooltip = A kijelölt fájl(ok) küldése mellékletként GoBack.label = Ugrás vissza GoBack.tooltip = Ugrás az előző könyvtárhoz GoForward.label = Ugrás előre GoForward.tooltip = Ugrás a következő könyvtárhoz GoToHome.label = Ugrás a kezdő mappához GoToParent.label = Ugrás a kezdethez GoToParent.tooltip = Ugrás a kezdeti könyvtárhoz GoToParentInOtherPanel.label = Ugrás a másik panel elejére GoToParentInBothPanels.label = Ugrás mindkét panel elejére GoToRoot.label = Ugrás a főkönyvtárhoz SortByName.label = Megnevezés szerint rendez SortByDate.label = Dátum szerint rendez SortBySize.label = Méret szerint rendez SortByExtension.label = Típus szerint rendez SortByPermissions.label = Jogosultság szerint rendez SortByOwner.label = Rendezés tulajdonos szerint SortByGroup.label = Rendezés csoportok szerint MarkGroup.label = Fájl(ok) kijelölése MarkGroup.tooltip = Csoportos fájlkijelölés UnmarkGroup.label = Kijelölés megszüntetése UnmarkGroup.tooltip = Csoportos fájlkijelölés megszüntetése MarkAll.label = Mindet kijelöl UnmarkAll.label = Összes kijelölés megszüntetése MarkSelectedFile.label = Kijelöl/Kihagy MarkSelectedFile.tooltip = Kijelöli/Kihagyja a kiválasztott fájlt MarkNextBlock.label = Egy blokk kijelölése lefelé MarkPreviousBlock.label = Egy blokk kijelölése felfelé MarkNextRow.label = Egy sor kijelölése lefelé MarkPreviousRow.label = Egy sor kijelölése felfelé MarkNextPage.label = Előző lap kijelölése MarkPreviousPage.label = Következő lap kijelölése MarkToFirstRow.label = Fájlok kijelölése a kezdethez MarkToLastRow.label = Fájlok kijelölése a végéhez MarkExtension.label = Kiterjesztés megjelölése InvertSelection.label = A kijelölés megfordítása SwapFolders.label = Oldalak felcserélése SwapFolders.tooltip = A bal és jobb panel váltás SetSameFolder.label = Ugyanaz mindkét oldalon SetSameFolder.tooltip = A bal és jobb oldalon ugyanaz a panel legyen NewWindow.label = Új ablak NewWindow.tooltip = Megnyitás új ablakban Open.label = Megnyitás Open.tooltip = Belép a könyvtárba / Belép az archív fájlba / Végrehajt OpenNatively.label = Megnyitás közvetlenül OpenNatively.tooltip = A kijelölt fájl megnyitása a rendszer által hozzárendelt programmal OpenInOtherPanel.label = Megnyitás a másik panelen OpenInBothPanels.label = Megnyitás mindkét panelen RevealInDesktop.label = Megnyitás a következővel: %1 RunCommand.label = Parancsfuttatás RunCommand.tooltip = Parancs futtatása az aktuális könyvtárból Pack.tooltip = Kijelölt fájlok csomagolása az archívumba Unpack.tooltip = Kijelölt archívumok kicsomagolása ShowFileProperties.label = Tulajdonságok ShowFileProperties.tooltip = A kijelölt fájl(ok) tulajdonságainak megjelenítése ShowPreferences.label = Beállítások ShowPreferences.tooltip = trolCommander beállítása ShowServerConnections.label = A megnyitott kapcsolatok megjelenítése Quit.label = Kilépés ReverseSortOrder.label = Fordított sorrend ToggleAutoSize.label = Oszlopok automatikus méretezése Stop.label = Könyvtár váltás megállítása ToggleColumn.show = %1 oszlop megjelenítése ToggleColumn.hide = %1 oszlop elrejtése ToggleCommandBar.show = Parancsszalag megjelenítése ToggleCommandBar.hide = Parancsszalag elrejtése ToggleToolBar.show = Eszköztár megjelenítése ToggleToolBar.hide = Eszköztár elrejtése CustomizeCommandBar.label = Parancsszalag beállítása ToggleStatusBar.show = Állapotsor megjelenítése ToggleStatusBar.hide = Állapotsor elrejtése ToggleShowFoldersFirst.label = Mappákat a listában előre ToggleTree.label = Könyvtárfa PopupLeftDriveButton.label = A bal panel megváltoztatása PopupRightDriveButton.label = A jobb panel megváltoztatása RecallPreviousWindow.label = Váltás az előző ablakra RecallNextWindow.label = Váltás a következő ablakra RecallWindow.label = Kiválasztandó ablak #%1 BringAllToFront.label = Minden ablakot előre SwitchActiveTable.label = A bal és jobb panel váltás SelectNextBlock.label = Ugrás egy blokkal lefelé SelectPreviousBlock.label = Ugrás egy blokkal felfelé SelectNextPage.label = Ugrás a következő lapra SelectPreviousPage.label = Ugrás az előző lapra SelectNextRow.label = Ugrás a következő sorra SelectPreviousRow.label = Ugrás az előző sorra SelectFirstRow.label = Az első fájl kiválasztása az aktuális mappában SelectLastRow.label = Az utolsó fájl kiválasztása az aktuális mappában SplitEqually.label = Felosztás egyenlő részekre SplitVertically.label = Felosztás függőlegesen SplitHorizontally.label = Felosztás vízszintesen ShowKeyboardShortcuts.label = Gyorsbillentyűk GoToWebsite.label = trolCommander honlap GoToForums.label = trolCommander fórum ReportBug.label = Hibajelentés Donate.label = Adomány a fejlesztőknek ShowAbout.label = trolCommander névjegy OpenTrash.label = Lomtár megnyitása EmptyTrash.label = Lomtár ürítése CalculateChecksum.label = Ellenőrző összeg számolása MaximizeWindow.label = Nagyítás MaximizeWindow.label.mac_os_x = Nagyítás MinimizeWindow.label = Kicsinyítés MinimizeWindow.label.mac_os_x = Kicsinyítés GoToDocumentation.label = Online dokumentáció ShowParentFoldersQL.label = Felsőbb alkönyvtárak ShowRecentLocationsQL.label = Legutóbb felkeresett helyek ShowRecentExecutedFilesQL.label = Legutóbb futtatott fájlok SplitFile.tooltip = A fájl darabolása kisebb részekre CombineFiles.tooltip = Az eredeti fájl összeállítása a kisebb részekből ShowDebugConsole.label = A konzol nyomkövetése FocusPrevious.label = Az előző összetevő kijelölése FocusNext.label = A következő összetevő kijelölése file_menu = Fájl file_menu.open_with = Megnyitás másként mark_menu = Kijelölés view_menu = Nézet view_menu.show_hide_columns = Oszlopok Mutatása/Elrejtése go_menu = Ugrás bookmarks_menu = Könyvjelzők bookmarks_menu.no_bookmark = Nincsenek Könyvjelzők quick_lists_menu = Gyorslista window_menu = Ablak help_menu = Súgó status_bar.selected_files = %1 / %2 kijelölt status_bar.connecting_to_folder = Kapcsolódás a mappához... Mégse: [ESCAPE] status_bar.volume_free = Szabad: %1 status_bar.volume_capacity = Teljes méret: %1 shortcuts_panel.title = Gyorsbillentyűk shortcuts_panel.restore_defaults = Alaphelyzet visszaállítása shortcuts_panel.show = Megjelenítés shortcuts_panel.default_message = Nyomja meg az Enter billentyűt vagy kattintson duplán a Gyorsbillentyűk módosításához. shortcuts_table.action_description = A művelet leírása shortcuts_table.shortcut = Gyorsbillentyű shortcuts_table.alternate_shortcut = Másodlagos Gyorsbillentyű shortcuts_table.type_in_a_shortcut = Gyorsbillentyű típusa command_bar_dialog.help = A nyomógombok húzásával módosíthatja a Parancsszalagot. table.folder_access_error_title = Könyvtár hozzáférési hiba table.folder_access_error = A könyvtár tartalmát nem lehet olvasni table.download_or_browse = Szeretné tallózni vagy letölteni ezt a fájlt? version_dialog.no_new_version_title = Nincs új verzió version_dialog.no_new_version = Gratulálunk, Önnek már a legújabb verziója van. version_dialog.new_version_title = Az új verzió elérhető version_dialog.new_version = Egy új trolCommander verzió elérhető. version_dialog.new_version_url = Egy új trolCommander verzió elérhető itt %1. version_dialog.not_available_title = A szerver nem elérhető version_dialog.not_available = A verzióinformáció elérése nem lehetséges a szerverről. version_dialog.install_and_restart = Telepítés és újraindítás version_dialog.preparing_for_update = A frissítés előkészítése... quit_dialog.title = Kilépés a trolCommanderből quit_dialog.desc = Biztos ki akar lépni? (%1 trolCommander van megnyitva.) quit_dialog.show_next_time = Üzenet megjelenítése destination_dialog.file_exists_action = Alapértelmezett művelet, ha a fájl létezik destination_dialog.verify_integrity = Adatintegritás ellenőrzése destination_dialog.skip_errors = Hibák kihagyása file_collision_dialog.title = Fájlütközés rename_dialog.new_name = Új név copy_dialog.destination = A kiválasztott fájl(ok) másolása copy_dialog.error_title = Másolási hiba copy_dialog.copying = Fájl(ok) másolása copy_dialog.copying_file = Másolás %1 pack_dialog.packing = Fájl(ok) tömörítése pack_dialog.packing_file = Tömörítés %1 pack_dialog.error_title = Tömörítési hiba pack_dialog_description = A kiválasztott fájlok hozzáadása pack_dialog.archive_format = Archív formátum unpack_dialog.destination = A kijelölt fájl(ok) kicsomagolása unpack_dialog.error_title = Kicsomagolási hiba unpack_dialog.unpacking = Fájl(ok) kicsomagolása unpack_dialog.unpacking_file = Kicsomagolás %1 optimizing_archive = Archívum optimalizálás %1 error_while_optimizing_archive = Hiba az archívum optimalizálásakor %1 move_dialog.move_description = Áthelyezés ide move_dialog.error_title = Áthelyezési hiba move_dialog.moving = Fálj(ok) áthelyezése move_dialog.moving_file = Áthelyezés %1 download_dialog.description = Fájl(ok) letöltése ide download_dialog.error_title = Letöltési hiba download_dialog.downloading = Letöltés download_dialog.downloading_file = Letöltés %1 mkfile_dialog.allocate_space = Hely lefoglalása delete_dialog.permanently_delete.confirmation = Folyamatosan törli a kijelölt fájl(oka)t? delete_dialog.move_to_trash.confirmation = Törli a kijelölt fájl(oka)t? delete_dialog.move_to_trash.confirmation_details = A fájlok a lomtárba lesznek áthelyezve. delete_dialog.move_to_trash.option = Áthelyezés a lomtárba delete_dialog.move_to_trash.failed = A fájl(oka)t nem lehet a Lomtárba dobni. delete_dialog.deleting = Törlés delete_dialog.error_title = Törlési hiba delete.deleting_file = Törlés %1 email_dialog.prefs_not_set_title = A levelező nincs konfigurálva email_dialog.prefs_not_set = Önnek be kell állítania a levelező paramétereit. email_dialog.from = Feladó email_dialog.to = Címzett email_dialog.subject = Tárgy email_dialog.send = Küldés email_dialog.error_title = E-mail fájl hiba email_dialog.read_error = Az alkönyvtár(ak)ban lévő fájl(ok) olvasása nem lehetséges email_dialog.sending = Fájlok küldése email.sending_file = Küldés %1 email.connecting_to_server = Kapcsolódás %1 email.server_unavailable = Nem lehet kapcsolódni a levelezőszerverhez %1, kérjük, ellenőrizze a levelező beállításait és próbálja újra. email.connection_closed = A szerver megszakította a kapcsolatot, a levél nincs elküldve. email.goodbye_failed = Kapcsolatbontás közben hiba történt, így a levél nem biztos, hogy elment. email.send_file_error = A fájl küldése nem lehetséges %1, a levél nincs elküldve. split_file_dialog.error_title = Fájldarabolási hiba split_file_dialog.file_to_split = Fájldarabolás split_file_dialog.target_directory = Célkönyvtár split_file_dialog.part_size = Egy rész mérete split_file_dialog.parts = Darabok száma split_file_dialog.generate_CRC = CRC fájl készítése split_file_dialog.max_parts = A részek maximális darabszáma: %1 split_file_dialog.auto = Automatikus split_file_dialog.insert_new_media = Helyezzen be új adathordozót combine_files_dialog.error_title = Fájl összefűzési hiba combine_files_job.no_crc_file = A fájlok összefűzése sikerült. Nincs CRC fájl. combine_files_job.crc_read_error = CRC fájl olvasási hiba. combine_files_job.crc_check_failed = CRC hiba: hiányzik: %2, megtalált: %1 combine_files_job.crc_ok = A fájlok összefűzése sikerült. CRC ellenőrzőszám rendben. file_selection_dialog.mark = Kijelölés file_selection_dialog.unmark = Kijelölés visszavonása file_selection_dialog.mark_description = Kijelöli a fájlokat, amelyeknek a neve file_selection_dialog.unmark_description = Kijelölés visszavonása azon fájloknál, amelyeknek a neve file_selection_dialog.case_sensitive = Kis- és NAGYBETŰ érzékeny file_selection_dialog.include_folders = Könyvtárakkal együtt progress_dialog.starting = Átvitel indítása... progress_dialog.transferred = %1 átvitel %2 alatt megtörtént progress_dialog.elapsed_time = Eltelt idő progress_dialog.advanced = Speciális progress_dialog.current_speed = Jelenlegi sebesség progress_dialog.limit_speed = Sebesség korlát progress_dialog.close_when_finished = Ablak bezárása, ha elkészült progress_dialog.processing_files = Fájl(ok) feldolgozása progress_dialog.processing_file = Feldolgozás %1 progress_dialog.verifying_file = %1 Ellenőrzése progress_dialog.job_finished = Feladat befejezve progress_dialog.job_error = Hiba a feladatban properties_dialog.file_properties = %1 tulajdonságok properties_dialog.contents = Tartalom properties_dialog.calculating = számol... calculate_checksum_dialog.checksum_algorithm = Ellenőrző algoritmus calculate_checksum_dialog.temporary_file = Ideiglenes fájl change_date_dialog.now = Most change_date_dialog.specific_date = Speciális dátum run_dialog.run_command_description = Futtatás az aktuális könyvtárban run_dialog.run_in_home_description = Futtatás a főkönyvtárban run_dialog.command_output = Parancs kimenet run_dialog.run = Futtat run_dialog.clear_history = Előzmények törlése server_connect_dialog.server_type = Kapcsolat típusa server_connect_dialog.server = Szerver server_connect_dialog.share = Megoszt server_connect_dialog.domain = Tartomány server_connect_dialog.username = Felhasználónév server_connect_dialog.initial_dir = Kezdő könyvtár server_connect_dialog.port = Port server_connect_dialog.server_url = Szerver URL server_connect_dialog.http_url = Honlap URL server_connect_dialog.connect = Kapcsolat server_connect_dialog.protocol = Protokoll server_connect_dialog.nfs_version = NFS verzió server_connect_dialog.private_key = Privát kulcs server_connect_dialog.passphrase = Jelmondat ftp_connect.passive_mode = A passzív mód engedélyezése ftp_connect.anonymous_user = Névtelen felhasználó ftp_connect.nb_connection_retries = Próbálkozások száma ftp_connect.retry_delay = Várakozás a próbálkozások között (másodpercben) http_connect.basic_authentication = HTTP Alapszintű Hitelesítés (opcionális) server_connections_dialog.disconnect = Szétkapcsol server_connections_dialog.connection_busy = Foglalt server_connections_dialog.connection_idle = Inaktív bonjour.bonjour_services = Bonjour szolgáltatások bonjour.no_service_discovered = Nincs felderített szolgáltatás bonjour.bonjour_disabled = Bonjour letiltva auth_dialog.title = Hitelesítés auth_dialog.desc = Írja be a felhasználónevet és a jelszót auth_dialog.server = Szerver auth_dialog.store_credentials = A felhasználónév és a jelszó mentése (gyenge titkosítás) auth_dialog.connect_as = Csatlakozás mint auth_dialog.authentication_failed = A hitelesítés sikertelen sortable_list.move_up = Mozgatás fel sortable_list.move_down = Mozgatás le add_bookmark_dialog.add = Hozzáadás edit_bookmarks_dialog.new = Új file_viewer.view_error_title = Megtekintési hiba file_viewer.view_error = A fájl megtekintése nem lehetséges. file_viewer.file_menu = Fájl file_viewer.close = Bezárás file_viewer.large_file_warning = Ez a fájl túl nagy lehet ehhez a művelethez. file_viewer.open_anyway = Mindenképpen megnyit text_viewer.edit = Módosítás text_viewer.copy = Másolás text_viewer.select_all = Összes kijelölése text_viewer.find = Keresés text_viewer.find_next = Következő keresése text_viewer.find_previous = Előző keresése text_viewer.binary_file_warning = Ez a fájl valószínűleg bináris fájl image_viewer.controls_menu = Beállítás image_viewer.zoom_in = Nagyít image_viewer.zoom_out = Kicsinyít file_editor.edit_error_title = Módosítási hiba file_editor.edit_error = A fájl módosítása nem lehetséges. file_editor.save = Mentés file_editor.save_as = Mentés másként... file_editor.save_warning = Menti a változtatásokat ebben a fájlban a bezárás előtt? file_editor.cannot_write = A fájl írása nem lehetséges. text_editor.cut = Kivágás text_editor.paste = Beillesztés shortcuts_dialog.quick_search.start_search = Gyorskereséshez írjon be néhány karaktert shortcuts_dialog.quick_search.cancel_search = Gyorskeresés befejezése shortcuts_dialog.quick_search.remove_last_char = A gyorkeresési karakterlánc utolsó betűjének törlése shortcuts_dialog.quick_search.jump_to_previous = Ugrás az előző gyorskeresés eredményéhez shortcuts_dialog.quick_search.jump_to_next = Ugrás a következő gyorskeresés eredményéhez shortcuts_dialog.quick_search.mark_jump_next = Kilelöli/Visszavonja az aktuális fájlt és a következő keresési eredményhez ugrik theme_editor.title = Témaszerkesztő theme_editor.folder_tab = Könyvtár tábla theme_editor.shell_tab = Végrehajtás theme_editor.shell_history_tab = Előzmények theme_editor.statusbar_tab = Állapotsor theme_editor.free_space = Szabad hely theme_editor.free_space.ok = OK theme_editor.free_space.warning = Figyelem theme_editor.free_space.critical = Kritikus theme_editor.locationbar_tab = Helyzetsor theme_editor.editor_tab = Fájlszerkesztő theme_editor.font = Betű theme_editor.active_panel = Aktív theme_editor.inactive_panel = Inaktív theme_editor.general = Általános theme_editor.could_not_save_theme = Nem lehet írni a témát %1 theme_editor.border = Keret theme_editor.background = Háttér theme_editor.alternate_background = Alternatív háttér theme_editor.unfocused_background = Háttér (kijelölve) theme_editor.quick_search.unmatched_file = Kívülálló fájlok theme_editor.text = Szöveg theme_editor.progress = Folyamat theme_editor.normal = Normál theme_editor.normal_unfocused = Normál (kijelöletlen) theme_editor.selected = Kiválasztott theme_editor.selected_unfocused = Kiválasztott (kijelöletlen) theme_editor.color = Szín theme_editor.colors = Színek theme_editor.plain_file = Sima fájl theme_editor.marked_file = Kijelölt fájl theme_editor.hidden_file = Rejtett fájl theme_editor.folder = Könyvtár theme_editor.archive_file = Archívum theme_editor.symbolic_link = Jelképes hivatkozás theme_editor.header = Fejléc theme_editor.item = Elem command_bar_customize_dialog.available_actions = Lehetséges műveletek command_bar_customize_dialog.modifier = Módosítók prefs_dialog.title = Tulajdonságok prefs_dialog.general_tab = Általános prefs_dialog.day = Nap prefs_dialog.month = Hónap prefs_dialog.year = Év prefs_dialog.language = Nyelv (újraindítást igényel) prefs_dialog.date_time = Dátum és idő formátum prefs_dialog.time = Idő prefs_dialog.date = Dátum prefs_dialog.date_separator = Elválasztó prefs_dialog.time_12_hour = 12 órás formátum prefs_dialog.time_24_hour = 24 órás formátum prefs_dialog.show_seconds = Másodpercek megjelenítése prefs_dialog.show_century = Évszázad megjelenítése prefs_dialog.check_for_updates_on_startup = Ellenőrizze a frissítéseket induláskor prefs_dialog.show_splash_screen = Indítóképernyő megjelenítése prefs_dialog.folders_tab = Könyvtárak prefs_dialog.startup_folders = Indítási panelek prefs_dialog.left_folder = Bal panel prefs_dialog.right_folder = Jobb panel prefs_dialog.last_folder = Utoljára megtekintett könyvtár prefs_dialog.custom_folder = Egyedi könyvtár prefs_dialog.show_hidden_files = Rejtett fájlok megjelenítése prefs_dialog.show_ds_store_files = A .DS_Store fájlok megjelenítése prefs_dialog.show_system_folders = Rendszer könyvtárak megjelenítése prefs_dialog.compact_file_size = Fájlméretek kijelzése kerekítve prefs_dialog.follow_symlinks_when_cd = Könyvtár váltáskor ugrás a hivatkozásához prefs_dialog.appearance_tab = Megjelenés prefs_dialog.look_and_feel = Kinézet & Hatások prefs_dialog.icons_size = Ikon méret prefs_dialog.toolbar_icons = Eszköztár prefs_dialog.command_bar_icons = Parancssor prefs_dialog.file_icons = Fájl típusok prefs_dialog.use_system_file_icons = Rendszerikonok használata prefs_dialog.use_system_file_icons.always = Mindig prefs_dialog.use_system_file_icons.never = Soha prefs_dialog.use_system_file_icons.applications = Csak alkalmazásokhoz prefs_dialog.edit_current_theme = Jelenlegi téma javítása... prefs_dialog.themes = Témák prefs_dialog.import_theme = Téma importálás prefs_dialog.import_look_and_feel = Kinézet importálása prefs_dialog.no_look_and_feel = Nem található Kinézet. prefs_dialog.error_in_import = Hiba a téma importálásakor %1 prefs_dialog.cannot_read_theme = Nem lehet betölteni a témát a fájlból %1 prefs_dialog.export_theme = Exportálás %1 prefs_dialog.import = Importálás prefs_dialog.export = Exportálás prefs_dialog.theme_type = Típus: %1 prefs_dialog.delete_theme = Folyamatosan törli a(z) %1 témát? prefs_dialog.delete_look_and_feel = Folyamatosan töröljem a(z) %1 Kinézetet? prefs_dialog.rename_failed = Hiba a téma átnevezéskor %1 prefs_dialog.xml_file = XML fájl prefs_dialog.jar_file = JAR fájl prefs_dialog.mail_tab = E-mail prefs_dialog.mail_settings = Kimenő levelezési beállítások prefs_dialog.mail_name = Az Ön neve prefs_dialog.mail_address = Az Ön e-mail címe prefs_dialog.mail_server = Kimenő (SMTP) szerver prefs_dialog.misc_tab = Egyebek prefs_dialog.use_brushed_metal = A 'brushed metal' felületet használata (újraindítást igényel) prefs_dialog.confirm_on_quit = Kérjen megerősítést kilépéskor prefs_dialog.default_shell = Az alapértelmezett végrehajtás használata prefs_dialog.custom_shell = Egyedi végrehajtás használata prefs_dialog.shell_encoding = Karakter kódolás prefs_dialog.auto_detect_shell_encoding = Automatikus felismerés prefs_dialog.enable_bonjour_discovery = A Bonjour szolgáltatások felderítésének engedélyezése prefs_dialog.enable_system_notifications = Rendszerértesítések engedélyezése debug_console_dialog.level = Szint unit.byte = bájt unit.bytes = bájt unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1m duration.minutes = %1p duration.hours = %1ó duration.days = %1n duration.months = %1h duration.years = %1é theme.custom_theme = Egyedi téma theme.custom = Módosított theme.built_in = Beépített theme.add_on = Bővítmény theme.current = jelenlegi theme_could_not_be_loaded = Hiba a téma betöltésekor. setup.title = Üdvözöljük a trolCommanderben setup.intro = Válassza ki a trolCommander működési módját setup.look_and_feel = Válassza ki a Megjelenést setup.theme = Válassza ki a témát font_chooser.font_size = Méret font_chooser.font_bold = Félkövér font_chooser.font_italic = Dőlt color_chooser.red = Piros color_chooser.green = Zöld color_chooser.blue = Kék color_chooser.hue = Színárnyalat color_chooser.brightness = Világosság color_chooser.swatches = Minta color_chooser.saturation = Telítettség color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Legutóbbi color_chooser.alpha = Alfa átlátszóság color_chooser.title = Válasszon színt batch_rename_dialog.mask = Átnevezési minta batch_rename_dialog.search_replace = Keresés és Csere batch_rename_dialog.search_for = Keresett szöveg batch_rename_dialog.replace_with = Csere erre batch_rename_dialog.counter = Számláló batch_rename_dialog.start_at = Kezdés ennél batch_rename_dialog.step_by = Növekmény batch_rename_dialog.format = Formátum batch_rename_dialog.upper_lower_case = Kisbetű - Nagybetű batch_rename_dialog.no_change = Változatlan batch_rename_dialog.lower_case = kisbetű batch_rename_dialog.upper_case = NAGYBETŰ batch_rename_dialog.first_upper = Mondatkezdő nagybetű batch_rename_dialog.word = Szókezdő nagybetű batch_rename_dialog.old_name = Régi név batch_rename_dialog.new_name = Új név batch_rename_dialog.block_name = Megtart batch_rename_dialog.range = Tartomány batch_rename_dialog.proceed_renaming = %1 fájl átnevezve (összesen: %2). Folytassam? batch_rename_dialog.duplicate_names = Azonos megnevezések! parent_folders_quick_list.empty_message = A jelenlegi helynek nincs felsőbb alkönyvtára recent_locations_quick_list.empty_message = Nincsenek legutóbbi helyek recent_executed_files_quick_list.empty_message = Nincsenek legutóbb futtatott fájlok #pack.error_on_file = Hiba a tömörítés közben %1 #pack_dialog.cannot_write = A célkönyvtárba nem lehetséges tömörített fájlt készíteni. #unpack.unable_to_open_zip = A Zip fájl megnyitása nem lehetséges %1. #mkdir_dialog.title = új könyvtár #mkdir_dialog.error_title = könyvtár létrehozási hiba #mkdir_dialog.description = új könyvtár létrehozása #progress_dialog.hide = rejt #move_dialog.rename_description = átnevezés ide #theme_editor.shell_font = Végrehajtás betűtípus #theme_editor.history_font = Előzmények betűtípus #theme_editor.shell_colors = Végrehajtás színek #theme_editor.history_colors = Előzmények színei #ToggleHiddenFiles.hide = Rejtett fájlok elrejtése #auth_dialog.error_was = Hiba történt: %1 #table.hide_column = Oszlop elrejtése #delete.symlink_warning_title = Parancsikont találtam #delete.symlink_warning = Ez a fájl egy parancsikonnak néz ki:\n\n Fájl: %1\n Link: %2\n\nCsak a parancsikon törlése vagy\nkövesse a parancsikont és törölje a könyvtárat (VIGYÁZAT)? #delete.delete_link_only = Hivatkozás törlése #delete.delete_linked_folder = Könyvtár törlése #Unpack.label = Kicsomagolás #Pack.label = Tömörítés ================================================ FILE: src/main/resources/dictionary_it_IT.properties ================================================ ok = OK yes = Si no = No cancel = Annulla edit = Modifica close = Chiudi reset = Reset rename = Rinomina apply = Applica change = Cambia save = Salva dont_save = Non salvare replace = Sostituisci dont_replace = Non sostituire delete = Elimina skip = Salta retry = Riprova resume = Riprendi overwrite = Sovrascrivi overwrite_if_older = Sovrascrivi vecchio duplicate = Duplica apply_to_all = Applica a tutti copy = Copia move = Sposta pack = Compressione unpack = Decomprimi download = Scarica browse = Naviga ask = Chiedi stop = Ferma pause = Pausa quick_search = Ricerca rapida file_manager = Gestione File create = Crea creating_file = Creazione di %1 in corso choose = Seleziona choose_folder = Seleziona una cartella login = Login password = Password user = Utente encoding = Codifica license = Licenza name = Nome size = Dimensione date = Data extension = Estensione permissions = Permessi owner = Proprietario group = Gruppo location = Location untitled = SenzaTitolo source = Sorgente destination = Destinazione recurse_directories = Elabora anche le sotto-directory go_to = Vai a example = Esempio preview = Anteprima comment = Commento sample_text = Testo di esempio nb_files = %1 file nb_folders = %1 cartella(e) loading = Caricamento in corso... this_operation_cannot_be_undone = Questa operazione non puo' essere annullata. remove = Elimina warning = Attenzione error = Errore generic_error = Errore durante l'esecuzione dell'operazione richiesta. folder_does_not_exist = Directory inesistente o non disponibile. this_folder_does_not_exist = Directory inesistente o non disponibile: %1 this_file_does_not_exist = Questo file non esiste o non e' disponibile: %1 invalid_path = Percorso non valido: %1 directory_already_exists = Directory %1 gia' esistente. file_exists_in_destination = File gia' esistente nella destinazione source_parent_of_destination = Tentativo di trasferire una cartella in una sua sottocartella cannot_read_file = Impossibile leggere il file %1 cannot_write_file = Impossibile scrivere file %1 cannot_create_folder = Impossibile creare directory %1 cannot_read_folder = Impossibile leggere il contenuto della directory %1 cannot_delete_file = Impossibile eliminare il file %1 cannot_delete_folder = Impossibile eliminare directory %1 error_while_transferring = Errore trasferimento file %1 same_source_destination = Directory sorgente e destinazione uguali. file_already_exists = %1 esiste gia', vuoi sostituirlo ? write_error = Errore in scrittura read_error = Errore in lettura integrity_check_error = Verifica integrità fallita: sorgente e destinazione non corrispondono permissions.read = Lettura permissions.write = Scrittura permissions.executable = Esegui permissions.group = Gruppo permissions.other = Altro permissions.octal_notation = Notazione ottale action_categories.navigation = Navigare action_categories.selection = Selezionare action_categories.view = Visualizzare action_categories.file_operations = Operazioni su file action_categories.windows = Finestre Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Aggiungi a favoriti AddBookmark.tooltip = Aggiungi directory a favoriti BatchRename.label = Multi rinomina EditBookmarks.label = Modifica favoriti ExploreBookmarks.label = Esplora i bookmark EditCredentials.label = Modifica credenziali ChangeLocation.label = Cambia percorso corrente ChangeDate.label = Cambia data ChangeDate.tooltip = Cambia data dei file selezionati ChangePermissions.label = Cambia permessi ChangePermissions.tooltip = Cambia permessi dei file selezionati CheckForUpdates.label = Controlla aggiornamenti CompareFolders.label = Confronta directory ConnectToServer.label = Connetti al server ConnectToServer.tooltip = Connetti ad un server remoto View.label = Vedi View.tooltip = Visualizza file selezionati Edit.tooltip = Modifica file selezionato Copy.tooltip = Copia file selezionati LocalCopy.label = Copia locale LocalCopy.tooltip = Copia i file selezionati nella corrente directory Move.tooltip = Sposta i file selezionati Rename.tooltip = Rinomina i file selezionati Mkdir.label = Crea Mkdir.tooltip = Crea una nuova directory nella cartella corrente Mkfile.label = Crea nuovo file Mkfile.tooltip = Crea un file nella cartella corrente Delete.tooltip = Elimina file selezionati usando il cestino quando possibile PermanentDelete.label = Elimina definitivamente PermanentDelete.tooltip = Elimina file selezionati senza usare il cestino Refresh.label = Rileggi Refresh.tooltip = Rigenera cartella corrente CloseWindow.label = Chiudi CloseWindow.tooltip = Chiudi questa finestra CopyFileNames.label = Copia nome(i) CopyFilePaths.label = Copia percorso(i) CopyFilesToClipboard.label = Copia il(i) file PasteClipboardFiles.label = Incolla il(i) file Email.label = Invia i file via eMail Email.tooltip = Invia i file selezionati come allegati ad una eMail GoBack.label = Indietro GoBack.tooltip = Torna alla cartella precedente GoForward.label = Vai avanti GoForward.tooltip = Vai alla cartella seguente GoToHome.label = Torna nella cartella Home GoToParent.label = Vai alla cartella superiore GoToParent.tooltip = Vai alla cartella superiore GoToParentInOtherPanel.label = Vai su superiore nell'altro pannello GoToParentInBothPanels.label = Vai su superiore in entrambi i pannelli GoToRoot.label = Vai sulla directory Root SortByName.label = Ordina per Nome SortByDate.label = Ordina per Data SortBySize.label = Ordina per Dimensione SortByExtension.label = Ordina per Estensione SortByPermissions.label = Ordina per Permessi SortByOwner.label = Ordina per Propietario SortByGroup.label = Ordina per Gruppo MarkGroup.label = Seleziona file MarkGroup.tooltip = Seleziona un gruppo di file UnmarkGroup.label = Deseleziona file UnmarkGroup.tooltip = Deseleziona un gruppo di file MarkAll.label = Seleziona tutto UnmarkAll.label = Deseleziona tutto MarkSelectedFile.label = Seleziona/Deseleziona MarkSelectedFile.tooltip = Seleziona/Deseleziona file MarkNextPage.label = Seleziona pagina giu' MarkPreviousPage.label = Seleziona pagina su MarkToFirstRow.label = Seleziona file dall'inizio MarkToLastRow.label = Seleziona file fino alla fine MarkExtension.label = Marca estensione InvertSelection.label = Inverti selezione SwapFolders.label = Scambia cartelle SwapFolders.tooltip = Scambia finestre SetSameFolder.label = Allinea destinazione e sorgente SetSameFolder.tooltip = Setta la stessa directory su cartella di destra e sinistra NewWindow.label = Nuova finestra NewWindow.tooltip = Apri una nuova finestra Open.label = Apri Open.tooltip = Inserisci directory / Inserisci archivio / Esegui OpenNatively.label = Apri nativamente OpenNatively.tooltip = Esegui i file selezionati con le associazioni di sistema OpenInOtherPanel.label = Apri nell'altro pannello OpenInBothPanels.label = Apri in entrambi i pannelli RevealInDesktop.label = Mostra in %1 RunCommand.label = Esegui comando RunCommand.tooltip = Esegui un comando nella cartella corrente Pack.tooltip = Comprimi file selezionati Unpack.tooltip = Decomprimi gli archivi selezionati ShowFileProperties.label = Proprieta' ShowFileProperties.tooltip = Mostra proprieta' dei file selezionati ShowPreferences.label = Preferenze ShowPreferences.tooltip = Configura trolCommander ShowServerConnections.label = Mostra le connessioni aperte Quit.label = Esci ReverseSortOrder.label = Inverti ordine ToggleAutoSize.label = Auto-dimensiona colonne Stop.label = Ferma cambio cartella ToggleCommandBar.show = Mostra barra di comando ToggleCommandBar.hide = Nascondi barra di comando ToggleToolBar.show = Mostra toolbar ToggleToolBar.hide = Nascondi toolbar ToggleStatusBar.show = Mostra barra di stato ToggleStatusBar.hide = Nascondi barra di stato ToggleShowFoldersFirst.label = Mostra le cartelle per prime ToggleTree.label = Visualizza struttura PopupLeftDriveButton.label = Cambia cartella sinistra PopupRightDriveButton.label = Cambia cartella destra RecallPreviousWindow.label = Richiama finestra precedente RecallNextWindow.label = Richiama prossima finestra RecallWindow.label = Richiama finestra #%1 BringAllToFront.label = Porta tutte le finestre in primo piano SwitchActiveTable.label = Scambia tra pannello sinistro e destro SelectFirstRow.label = Seleziona primo file nella cartella corrente SelectLastRow.label = Seleziona ultimo file nella cartella corrente SplitEqually.label = Dividi egualmente SplitVertically.label = Dividi verticalmente SplitHorizontally.label = Dividi orizzontalmente ShowKeyboardShortcuts.label = Scorciatoie da tastiera GoToWebsite.label = Visualizza sito Web GoToForums.label = Visualizza forum ReportBug.label = Segnala un bug Donate.label = Fai una donazione ShowAbout.label = Info su trolCommander OpenTrash.label = Apri il cestino EmptyTrash.label = Vuota il cestino CalculateChecksum.label = Calcola checksum MaximizeWindow.label = Massimizza MaximizeWindow.label.mac_os_x = Zoom MinimizeWindow.label = Minimizza GoToDocumentation.label = Documentazione in linea ShowParentFoldersQL.label = Cartelle superiori ShowRecentLocationsQL.label = Percorsi recenti ShowRecentExecutedFilesQL.label = File eseguibili recenti file_menu = File file_menu.open_with = Apri con mark_menu = Selezione view_menu = Visualizza view_menu.show_hide_columns = Mostra/Nascondi colonne go_menu = Vai bookmarks_menu = Favoriti bookmarks_menu.no_bookmark = Nessun favorito quick_lists_menu = Liste rapide window_menu = Finestra help_menu = Aiuto status_bar.selected_files = %1 di %2 selezionati status_bar.connecting_to_folder = Connessione in corso, premere ESC per annullare status_bar.volume_free = Libero: %1 status_bar.volume_capacity = Capacita': %1 table.folder_access_error_title = Errore nell'accesso alla cartella table.folder_access_error = Impossibile leggere il contenuto della cartella table.download_or_browse = Vuoi navigare o scaricare questo file ? version_dialog.no_new_version_title = Nessuna nuova versione version_dialog.no_new_version = Congratulazioni, hai gia' l'ultima versione. version_dialog.new_version_title = Nessuna versione disponibile version_dialog.new_version = E' disponibile una nuova versione di trolCommander. version_dialog.new_version_url = E' disponibile una nuova versione di trolCommander: %1. version_dialog.not_available_title = Server non disponibile version_dialog.not_available = Impossibile prendere le informazioni sulla versione dal server. version_dialog.install_and_restart = Installa e riparti version_dialog.preparing_for_update = Preparazione per aggiornamento... quit_dialog.title = Esci da trolCommander quit_dialog.desc = Hai %1 finestra(e) aperta(e). Sei sicuro di voler uscire ? quit_dialog.show_next_time = Mostra la prossima volta destination_dialog.file_exists_action = Azione di default quando il file esiste destination_dialog.verify_integrity = Verifica integrità dei dati file_collision_dialog.title = Collisione di file rename_dialog.new_name = Nuovo nome copy_dialog.destination = Copia file selezionato(i) su copy_dialog.error_title = Errore durante la copia copy_dialog.copying = Copiando file copy_dialog.copying_file = Copiando %1 pack_dialog.packing = Comprimendo file pack_dialog.packing_file = Comprimendo %1 pack_dialog.error_title = Errore nella compressione pack_dialog_description = Aggiungi i file selezionati a pack_dialog.archive_format = Formato archivio unpack_dialog.destination = Decomprimi i file selezionati in unpack_dialog.error_title = Errore decompressione unpack_dialog.unpacking = Decomprimendo file unpack_dialog.unpacking_file = Decomprimendo %1 optimizing_archive = Ottimizzazione archivio %1 error_while_optimizing_archive = Errore ottimizzando l'archivio %1 move_dialog.move_description = Sposta verso move_dialog.error_title = Errore spostando move_dialog.moving = Spostando file move_dialog.moving_file = Spostando %1 download_dialog.description = Scaricando file in download_dialog.error_title = Errore in download download_dialog.downloading = In corso di scaricamento download_dialog.downloading_file = Scaricando %1 mkfile_dialog.allocate_space = Alloca spazio delete_dialog.permanently_delete.confirmation = Elimino definitivamente i file selezionati ? delete_dialog.move_to_trash.confirmation = Cancellare file selezionato(i) ? delete_dialog.move_to_trash.confirmation_details = I file saranno spostati nel cestino. delete_dialog.move_to_trash.option = Sposta nel cestino delete_dialog.deleting = Eliminazione in corso delete_dialog.error_title = Errore durante la cancellazione delete.deleting_file = In corso di cancellazione %1 email_dialog.prefs_not_set_title = Mail non configurato email_dialog.prefs_not_set = Devi prima impostare i tuoi parametri di Mail. email_dialog.from = Da email_dialog.to = A email_dialog.subject = Soggetto email_dialog.send = Invia email_dialog.error_title = Errore Email file email_dialog.read_error = Impossibile leggere i file nelle sottodirectory. email_dialog.sending = Inviando file email.sending_file = Inviando %1 email.connecting_to_server = Contattando %1 email.server_unavailable = Impossibile contattare mail server %1, verifica le tue preferenze di mail o prova piu' tardi. email.connection_closed = Connessione chiusa dal server, mail non inviata. email.goodbye_failed = Errore chiudendo la connessione, la meil puo' non essere stata inviata. email.send_file_error = Impossibile inviare il file %1, mail non inviata. file_selection_dialog.mark = Selezione file_selection_dialog.unmark = Deselezione file_selection_dialog.mark_description = Selezione file il cui nome file_selection_dialog.unmark_description = Deseleziona i file il cui nome file_selection_dialog.case_sensitive = MAIUSCOLE/minuscole file_selection_dialog.include_folders = Includi cartelle progress_dialog.starting = Trasferimento iniziato... progress_dialog.transferred = Trasferendo %1 in %2 progress_dialog.elapsed_time = Durata progress_dialog.advanced = Avanzate progress_dialog.current_speed = Velocita' attuale progress_dialog.limit_speed = Limite di velocita' progress_dialog.close_when_finished = Chiudi finestra quando finito progress_dialog.processing_files = Elaborazione file progress_dialog.processing_file = Elaborando %1 progress_dialog.verifying_file = Verificando %1 progress_dialog.job_finished = Lavoro terminato progress_dialog.job_error = Errore durante il lavoro properties_dialog.file_properties = %1 Proprieta' properties_dialog.contents = Contenuto properties_dialog.calculating = calcolando... calculate_checksum_dialog.checksum_algorithm = Algoritmo di Checksum calculate_checksum_dialog.temporary_file = File Temporaneo change_date_dialog.now = Adesso change_date_dialog.specific_date = Specifica data run_dialog.run_command_description = Esegui nella cartella corrente run_dialog.run_in_home_description = Esegui nella cartella Home run_dialog.command_output = Command output run_dialog.run = Esegui run_dialog.clear_history = Ripulisci l'history server_connect_dialog.server_type = Tipo connessione server_connect_dialog.server = Server server_connect_dialog.share = Condividi server_connect_dialog.username = Username server_connect_dialog.initial_dir = Directory iniziale server_connect_dialog.port = Porta server_connect_dialog.server_url = Server URL server_connect_dialog.http_url = Web site URL server_connect_dialog.connect = Connetti server_connect_dialog.protocol = Protocollo server_connect_dialog.nfs_version = Versione-NFS server_connect_dialog.private_key = Chiave privata server_connect_dialog.passphrase = Password ftp_connect.passive_mode = Abilita modalita' passiva ftp_connect.anonymous_user = Utente anonimo ftp_connect.nb_connection_retries = Numero di tentativi di connessione ftp_connect.retry_delay = Ritardo tra tentativi (in secondi) http_connect.basic_authentication = HTTP Autentica Base (opzionale) server_connections_dialog.disconnect = Disconnetti server_connections_dialog.connection_busy = Occupato server_connections_dialog.connection_idle = Inattivo bonjour.bonjour_services = Servizi Bonjour bonjour.no_service_discovered = Nessun servizio scoperto bonjour.bonjour_disabled = Bonjour disabilitato auth_dialog.title = Autenticazione auth_dialog.desc = Prego inserire login e password auth_dialog.server = Server auth_dialog.store_credentials = Memorizza login e password (weak encryption) auth_dialog.connect_as = Connetti come auth_dialog.authentication_failed = Autenticazione fallita sortable_list.move_up = Muovi sopra sortable_list.move_down = Muovi sotto add_bookmark_dialog.add = Aggiungi edit_bookmarks_dialog.new = Nuovo file_viewer.view_error_title = Errore vista file_viewer.view_error = Impossibile visualizzare il file. file_viewer.file_menu = File file_viewer.close = Chiudi file_viewer.large_file_warning = Questo file e' troppo grande per questa operazione. file_viewer.open_anyway = Apri comunque text_viewer.edit = Modifica text_viewer.copy = Copia text_viewer.select_all = Seleziona tutto text_viewer.find = Cerca text_viewer.find_next = Trova il successivo text_viewer.find_previous = Trova il precedente text_viewer.binary_file_warning = Questo sembra essere un file binario image_viewer.controls_menu = Controlli image_viewer.zoom_in = Ingrandisci image_viewer.zoom_out = Rimpiccolisci file_editor.edit_error_title = Errore in modifica file_editor.edit_error = Impossibile modificare il file. file_editor.save = Salva file_editor.save_as = Salva come... file_editor.save_warning = Salvare le modifiche a questo file prima di chiudere ? file_editor.cannot_write = Impossibile salvare il file. text_editor.cut = Taglia text_editor.paste = Incolla shortcuts_dialog.quick_search.start_search = Batti un tasto per iniziare una ricerca rapida shortcuts_dialog.quick_search.cancel_search = Annullare ricerca rapida shortcuts_dialog.quick_search.remove_last_char = Togli ultimo carattere dalla stringa di ricerca shortcuts_dialog.quick_search.jump_to_previous = Salta sulla ricerca rapida precedente shortcuts_dialog.quick_search.jump_to_next = Salta sulla ricerca rapida seguente shortcuts_dialog.quick_search.mark_jump_next = Seleziona/deseleziona il file corrente e salta sulla ricerca rapida seguente theme_editor.title = Modifica tema theme_editor.folder_tab = Pannello Cartella theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Shell history theme_editor.statusbar_tab = Barra di stato theme_editor.free_space = Spazio libero theme_editor.free_space.ok = OK theme_editor.free_space.warning = Attenzione theme_editor.free_space.critical = Critico theme_editor.locationbar_tab = Barra delle Localita' theme_editor.editor_tab = Editor dei file theme_editor.font = Font theme_editor.active_panel = Attivo theme_editor.inactive_panel = Disattivo theme_editor.general = Generale theme_editor.could_not_save_theme = Impossibile scrivere il tema %1 theme_editor.border = Bordo theme_editor.background = Sfondo theme_editor.alternate_background = Sfondo alternativo theme_editor.unfocused_background = Sfondo (non selezionato) theme_editor.quick_search.unmatched_file = File non corrispondenti theme_editor.text = Testo theme_editor.progress = Progresso theme_editor.normal = Normale theme_editor.normal_unfocused = Normale (senza selezione) theme_editor.selected = Seleziona theme_editor.selected_unfocused = Selezionato (senza focus) theme_editor.color = Colore theme_editor.colors = Colori theme_editor.plain_file = File normale theme_editor.marked_file = File selezionato theme_editor.hidden_file = File nascosto theme_editor.folder = Cartella theme_editor.archive_file = Archivio theme_editor.symbolic_link = Link simbolico prefs_dialog.title = Preferenze prefs_dialog.general_tab = Generale prefs_dialog.day = Giorno prefs_dialog.month = Mese prefs_dialog.year = Anno prefs_dialog.language = Italiano (E' necessario il restart) prefs_dialog.date_time = Formato data e ora prefs_dialog.time = Ora prefs_dialog.date = Data prefs_dialog.date_separator = Separatore prefs_dialog.time_12_hour = Formato 12 ore prefs_dialog.time_24_hour = Formato 24 ore prefs_dialog.show_seconds = Mostra secondi prefs_dialog.show_century = Mostra secolo prefs_dialog.check_for_updates_on_startup = Controlla aggiornamenti alla partenza prefs_dialog.show_splash_screen = Mostra pannello di apertura prefs_dialog.folders_tab = Cartelle prefs_dialog.startup_folders = Cartelle iniziali prefs_dialog.left_folder = Cartella di sinistra prefs_dialog.right_folder = Cartella di destra prefs_dialog.last_folder = Ultima cartella visitata prefs_dialog.custom_folder = Cartella definita dall'utilizzatore prefs_dialog.show_hidden_files = Mostra file nascosti prefs_dialog.show_ds_store_files = Mostra file .DS_Store prefs_dialog.show_system_folders = Mostra cartelle di sistema prefs_dialog.compact_file_size = Arrotonda le grandezze dei file mostrate prefs_dialog.follow_symlinks_when_cd = Segui link simbolici quando si cambia la directory corrente prefs_dialog.appearance_tab = Aspetto prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Grandezza icone prefs_dialog.toolbar_icons = Toolbar prefs_dialog.command_bar_icons = Barra comandi prefs_dialog.file_icons = Tipi file prefs_dialog.use_system_file_icons = Usa per i file le icone di sistema prefs_dialog.use_system_file_icons.always = Sempre prefs_dialog.use_system_file_icons.never = Mai prefs_dialog.use_system_file_icons.applications = Solo per le applicazioni prefs_dialog.edit_current_theme = Modifica il tema corrente... prefs_dialog.themes = Temi prefs_dialog.import_theme = Importa il tema prefs_dialog.import_look_and_feel = Importa look & feel prefs_dialog.no_look_and_feel = Nessun look & feel trovato. prefs_dialog.error_in_import = Errore durante import del tema %1. prefs_dialog.cannot_read_theme = Impossibile caricare il tema dal file %1 prefs_dialog.export_theme = Esportare %1 prefs_dialog.import = Import prefs_dialog.export = Esportare prefs_dialog.theme_type = Tipo: %1 prefs_dialog.delete_theme = Cancellazione definitiva del tema %1 ? prefs_dialog.delete_look_and_feel = Cancello definitivamente il look & feel %1 ? prefs_dialog.rename_failed = Impossibile rinominare il tema %1 prefs_dialog.xml_file = File XML prefs_dialog.jar_file = File JAR prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Parametri mail in uscita prefs_dialog.mail_name = Il Vostro Nome prefs_dialog.mail_address = Il Vostro indirizzo email prefs_dialog.mail_server = SMTP server prefs_dialog.misc_tab = Altro prefs_dialog.use_brushed_metal = Usa 'brushed metal' look (richiede restart) prefs_dialog.confirm_on_quit = Mostra pannello di conferma in uscita prefs_dialog.default_shell = Usa il terminale di default del sistema prefs_dialog.custom_shell = Usa il terminale utente prefs_dialog.shell_encoding = Codifica Shell prefs_dialog.auto_detect_shell_encoding = Auto-detect prefs_dialog.enable_bonjour_discovery = Abilita Bonjour services discovery prefs_dialog.enable_system_notifications = Abilita notifiche di sistema unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1g duration.months = %1me duration.years = %1a theme.custom_theme = Tema utente theme.custom = Personalizzato theme.built_in = Pre-istallato theme.add_on = Aggiuntivo theme.current = corrente theme_could_not_be_loaded = Errore durante il caricamento del tema. setup.title = Benvenuti in trolCommander setup.intro = Prego, scegliere il modo di comportarsi di trolCommander. setup.look_and_feel = Scegliete il Vostro Look & Feel setup.theme = Selezionate il Vostro tema font_chooser.font_size = Dimensione font_chooser.font_bold = Grassetto font_chooser.font_italic = Corsivo color_chooser.red = Rosso color_chooser.green = Verde color_chooser.blue = Blu color_chooser.hue = Tonalita' color_chooser.brightness = Luminosita' color_chooser.swatches = Palette dei colori color_chooser.saturation = Saturazione color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Recente color_chooser.alpha = Alpha transparenza color_chooser.title = Scegli un colore batch_rename_dialog.mask = Maschera di rinomina batch_rename_dialog.search_replace = Cerca & Sostituisci batch_rename_dialog.search_for = Cerca batch_rename_dialog.replace_with = Sostituisci con batch_rename_dialog.counter = Contatore batch_rename_dialog.start_at = Inizia da batch_rename_dialog.step_by = Incremento batch_rename_dialog.format = Formato batch_rename_dialog.upper_lower_case = Maiuscolo/Minuscolo batch_rename_dialog.no_change = Invariato batch_rename_dialog.lower_case = minuscolo batch_rename_dialog.upper_case = MAIUSCOLO batch_rename_dialog.first_upper = Prima lettera maiuscola batch_rename_dialog.word = Prima Lettera Di Ogni Parola batch_rename_dialog.old_name = Vecchio nome batch_rename_dialog.new_name = Nuovo nome batch_rename_dialog.block_name = Conserva batch_rename_dialog.range = Intervallo batch_rename_dialog.proceed_renaming = %1 file di %2 verranno rinominati. Vuoi continuare? batch_rename_dialog.duplicate_names = Nomi duplicati! parent_folders_quick_list.empty_message = Il percorso corrente non ha un livello superiore recent_locations_quick_list.empty_message = Nessun percorso recente recent_executed_files_quick_list.empty_message = Nessun file eseguibile recente #mkdir_dialog.description = Crea directory #mkfile_dialog.description = Crea un nuovo file vuoto #done = Fatto #move_dialog.rename_description = Rinomina file in #theme_editor.shell_colors = Colori del terminale #ToggleHiddenFiles.hide = Non mostrare i file nascosti #auth_dialog.error_was = Errore: %1 #table.hide_column = Nascondi colonna #delete.symlink_warning_title = Trovato link simbolico #delete.symlink_warning = Questo file sembra un link simbolico:\n\n File: %1\n Link a: %2\n\nElimina solo link oppure\nSegio link e cancella la cartella (ATTENZIONE) ? #delete.delete_link_only = Elimina link #delete.delete_linked_folder = Elimina directory #Unpack.label = Decomprimi file #Pack.label = Comprimi file ================================================ FILE: src/main/resources/dictionary_ja_JP.properties ================================================ ok = OK yes = はい no = いいえ cancel = キャンセル edit = 編集 close = 閉じる reset = リセット rename = 名前の変更 apply = 適用 change = 変更 save = 保存 dont_save = 保存しない replace = 置換 dont_replace = 置換しない delete = 削除 skip = スキップ retry = 再試行 resume = 再開 overwrite = 上書き overwrite_if_older = より古ければ上書き duplicate = 複製 apply_to_all = すべて適用 copy = コピー move = 移動 pack = パック unpack = パック解除 download = ダウンロード browse = 参照 ask = 質問 stop = 停止 pause = 一時停止 quick_search = クイック検索 file_manager = ファイル マネージャー create = 作成 creating_file = %1 を作成しています choose = 選択 choose_folder = フォルダーを選択します login = ログイン password = パスワード user = ユーザー encoding = エンコード license = ライセンス name = 名前 size = サイズ date = 日付 extension = 拡張子 permissions = 権限 owner = 所有者 group = グループ location = 場所 untitled = 無題 source = ソース destination = 先 recurse_directories = 再帰的に選択されたディレクトリを処理する go_to = 移動 example = 見本 preview = プレビュー comment = コメント sample_text = サンプル テキスト nb_files = %1 個のファイル nb_folders = %1 個のフォルダー loading = 読み込んでいます... this_operation_cannot_be_undone = この操作は元に戻せません。 remove = 削除 warning = 警告 error = エラー generic_error = 要求された操作を行っている間にエラーが発生しています。 folder_does_not_exist = このフォルダーは存在しないか利用できません。 this_folder_does_not_exist = このフォルダーは存在しないか利用できません: %1 this_file_does_not_exist = このファイルは存在しないか利用できません: %1 invalid_path = 不正なパスです: %1 directory_already_exists = ディレクトリ %1 はすでに存在します。 file_exists_in_destination = ファイルはすでに先にあります source_parent_of_destination = フォルダーのそのサブフォルダーの 1 つへの転送を試みています cannot_read_file = ファイル %1 を読み取れません cannot_write_file = ファイル %1 を書き込めません cannot_create_folder = ディレクトリ %1 を作成することができません cannot_read_folder = フォルダー %1 の内容を読み取ることができません cannot_delete_file = ファイル %1 を削除することができません cannot_delete_folder = フォルダー %1 を削除することができません error_while_transferring = ファイル %1 の転送中のエラーです same_source_destination = ソースと先のフォルダーは同じです file_already_exists = %1 はすでに存在します、置換しますか ? write_error = 書き込みエラー read_error = 読み取りエラー integrity_check_error = 整合性チェックが失敗しました: ソースと先が一致しません permissions.read = 読み取り permissions.write = 書き込み permissions.executable = 実行ファイル permissions.group = グループ permissions.other = その他 permissions.octal_notation = 8 進法 action_categories.navigation = 操作 action_categories.selection = 選択範囲 action_categories.view = 表示 action_categories.file_operations = ファイルの操作 action_categories.windows = ウィンドウ Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = ブックマークの追加 AddBookmark.tooltip = 現在のフォルダーをブックマークの一覧に追加します BatchRename.label = 名前の一括変更 EditBookmarks.label = ブックマークの編集 ExploreBookmarks.label = ブックマークの検索 EditCredentials.label = 信任状の編集 ChangeLocation.label = 現在の位置を変更 ChangeDate.label = 日付の変更 ChangeDate.tooltip = 選択されたファイルの日付を変更します ChangePermissions.label = 権限の変更 ChangePermissions.tooltip = 選択されたファイルの権限を変更します CheckForUpdates.label = 更新のチェック CompareFolders.label = フォルダーの比較 ConnectToServer.label = サーバーへ接続 ConnectToServer.tooltip = リモート サーバーへ接続します View.label = 表示 View.tooltip = 選択されたファイルを表示します Edit.tooltip = 選択されたファイルを編集します Copy.tooltip = マークされたファイルをコピーします LocalCopy.label = ローカル コピー LocalCopy.tooltip = 現在のフォルダーに選択されたファイルをコピーします Move.tooltip = マークされたファイルを移動します Rename.tooltip = 選択されたファイルの名前を変更します Mkdir.label = ディレクトリの作成 Mkdir.tooltip = 現在のフォルダーにディレクトリを作成します Mkfile.label = ファイルの作成 Mkfile.tooltip = 現在のフォルダーにファイルを作成します Delete.tooltip = マークされたファイルを削除します PermanentDelete.label = 永久に削除 PermanentDelete.tooltip = システムのごみ箱を使用せずにマークされたファイルを削除します Refresh.label = 更新 Refresh.tooltip = 現在のフォルダーを更新します CloseWindow.label = ウィンドウを閉じる CloseWindow.tooltip = このウィンドウを閉じます CopyFileNames.label = 名前のコピー CopyFilePaths.label = パスのコピー CopyFilesToClipboard.label = ファイルのコピー PasteClipboardFiles.label = ファイルの貼り付け Email.label = ファイルの電子メール Email.tooltip = 電子メールの添付としてマークされたファイルを送信します GoBack.label = 戻る GoBack.tooltip = 前のフォルダーへ移動します GoForward.label = 進む GoForward.tooltip = 次のフォルダーへ移動します GoToHome.label = ホーム フォルダーへ移動 GoToParent.label = 親へ移動 GoToParent.tooltip = 親フォルダーへ移動します GoToParentInOtherPanel.label = 他のパネルで親へ移動 GoToParentInBothPanels.label = 両方のパネルで親へ移動 GoToRoot.label = ルートへ移動 SortByName.label = 名前で整列 SortByDate.label = 日付で整列 SortBySize.label = サイズで整列 SortByExtension.label = 拡張子で整列 SortByPermissions.label = 権限で整列 SortByOwner.label = 所有者で整列 SortByGroup.label = グループで整列 MarkGroup.label = ファイルのマーク MarkGroup.tooltip = ファイルのグループをマークします UnmarkGroup.label = ファイルのマーク解除 UnmarkGroup.tooltip = ファイルのグループをマーク解除します MarkAll.label = すべてマーク UnmarkAll.label = すべてマーク解除 MarkSelectedFile.label = マーク/解除 MarkSelectedFile.tooltip = 選択されたフォルダーをマーク/解除します MarkNextPage.label = 下へページをマーク MarkPreviousPage.label = 上へページをマーク MarkToFirstRow.label = 始めまでのファイルをマーク MarkToLastRow.label = 終わりまでのファイルをマーク MarkExtension.label = 拡張子のマーク InvertSelection.label = 選択範囲の反転 SwapFolders.label = フォルダーの交換 SwapFolders.tooltip = 左右のフォルダーを交換します SetSameFolder.label = 同じフォルダーに設定 SetSameFolder.tooltip = 左右のフォルダーを同じフォルダーに設定します NewWindow.label = 新しいウィンドウ NewWindow.tooltip = 新しいウィンドウを開きます Open.label = 開く Open.tooltip = フォルダーの入力 / アーカイブの入力 / 実行 OpenNatively.label = 自然に開く OpenNatively.tooltip = システムのファイルの関連付けで選択されたファイルを実行します OpenInOtherPanel.label = 他のパネルで開く OpenInBothPanels.label = 両方のパネルで開く RevealInDesktop.label = %1 で実行 RunCommand.label = コマンドの実行 RunCommand.tooltip = 現在のフォルダーでコマンドを実行します Pack.tooltip = アーカイブにマークされたファイルをパックします Unpack.tooltip = マークされたアーカイブ ファイルをパック解除します ShowFileProperties.label = プロパティ ShowFileProperties.tooltip = マークされたファイルのプロパティを表示します ShowPreferences.label = 環境設定 ShowPreferences.tooltip = trolCommander を構成します ShowServerConnections.label = 開かれている接続の表示 Quit.label = 終了 ReverseSortOrder.label = 順序の反転 ToggleAutoSize.label = 列のサイズの自動変更 Stop.label = フォルダーの変更を中止 ToggleCommandBar.show = コマンド バーの表示 ToggleCommandBar.hide = コマンド バーを非表示にする ToggleToolBar.show = ツール バーの表示 ToggleToolBar.hide = ツール バーを非表示にする ToggleStatusBar.show = ステータス バーの表示 ToggleStatusBar.hide = ステータス バーを非表示にする ToggleShowFoldersFirst.label = 最初にフォルダーを表示 ToggleTree.label = ツリー ビューの表示 PopupLeftDriveButton.label = 左のフォルダーを変更 PopupRightDriveButton.label = 右のフォルダーを変更 RecallPreviousWindow.label = 前のウィンドウを呼び戻す RecallNextWindow.label = 次のウィンドウを呼び戻す RecallWindow.label = ウィンドウ #%1 の呼び戻し BringAllToFront.label = すべて最前面へ移動 SwitchActiveTable.label = 左右のパネル間を切り替え SelectFirstRow.label = 現在のフォルダーの最初のファイルを選択 SelectLastRow.label = 現在のフォルダーの最後のファイルを選択 SplitEqually.label = 均等に分割 SplitVertically.label = 垂直に分割 SplitHorizontally.label = 水平に分割 ShowKeyboardShortcuts.label = キーボード ショートカット GoToWebsite.label = ウェブサイトへ移動 GoToForums.label = フォーラムへ移動 ReportBug.label = バグの報告 Donate.label = 寄付 ShowAbout.label = trolCommander のバージョン情報 OpenTrash.label = ごみ箱を開く EmptyTrash.label = ごみ箱を空にする CalculateChecksum.label = チェックサムの計算 MaximizeWindow.label = 最大化 MaximizeWindow.label.mac_os_x = ズーム MinimizeWindow.label = 最小化 GoToDocumentation.label = オンライン ドキュメント ShowParentFoldersQL.label = 親フォルダー ShowRecentLocationsQL.label = 最近の場所 ShowRecentExecutedFilesQL.label = 最近実行したファイル file_menu = ファイル file_menu.open_with = 開く mark_menu = マーク view_menu = 表示 view_menu.show_hide_columns = 列の表示/非表示を切り替え go_menu = 移動 bookmarks_menu = ブックマーク bookmarks_menu.no_bookmark = ブックマークがありません quick_lists_menu = クイック リスト window_menu = ウィンドウ help_menu = ヘルプ status_bar.selected_files = %2 個中 %1 個選択されています status_bar.connecting_to_folder = フォルダーへ接続しています、取り消すには ESCAPE を押します。 status_bar.volume_free = 空き: %1 status_bar.volume_capacity = 容量: %1 table.folder_access_error_title = フォルダー アクセス エラー table.folder_access_error = フォルダーの内容を読み取ることができません table.download_or_browse = このファイルを参照またはダウンロードしますか ? version_dialog.no_new_version_title = 新しいバージョンがありません version_dialog.no_new_version = おめでとうございます、すでに最新のバージョンです。 version_dialog.new_version_title = 新しいバージョンが利用可能です version_dialog.new_version = trolCommander の新しいバージョンは利用可能です。 version_dialog.new_version_url = trolCommander の新しいバージョンは %1 で 利用可能です。 version_dialog.not_available_title = サーバーが到達できません version_dialog.not_available = サーバーからバージョン情報を取得することができません。 version_dialog.install_and_restart = インストールと再起動 version_dialog.preparing_for_update = 更新を準備しています... quit_dialog.title = trolCommander の終了 quit_dialog.desc = %1 個のウィンドウを開いています。終了してもよろしいですか ? quit_dialog.show_next_time = 次回に表示する destination_dialog.file_exists_action = ファイルが存在するときの既定の動作 destination_dialog.verify_integrity = データの統合性を検証する file_collision_dialog.title = ファイルの衝突 rename_dialog.new_name = 新しい名前 copy_dialog.destination = 選択されたファイルのコピー先 copy_dialog.error_title = コピー エラー copy_dialog.copying = ファイルのコピー中 copy_dialog.copying_file = %1 のコピー中 pack_dialog.packing = ファイルのパック中 pack_dialog.packing_file = %1 の圧縮中 pack_dialog.error_title = パック エラー pack_dialog_description = 選択されたファイルの追加先 pack_dialog.archive_format = アーカイブ フォーマット unpack_dialog.destination = 選択されたファイルのパック解除先 unpack_dialog.error_title = パック解除 エラー unpack_dialog.unpacking = ファイルのパック解除中 unpack_dialog.unpacking_file = %1 のパック解除中 optimizing_archive = アーカイブ %1 の最適化中 error_while_optimizing_archive = アーカイブ %1 の最適化中のエラーです move_dialog.move_description = 移動先 move_dialog.error_title = 移動エラー move_dialog.moving = ファイルの移動中 move_dialog.moving_file = %1 の移動中 download_dialog.description = ファイルのダウンロード先 download_dialog.error_title = ダウンロード エラー download_dialog.downloading = ダウンロード中 download_dialog.downloading_file = %1 のダウンロード中 mkfile_dialog.allocate_space = 割り当てる領域 delete_dialog.permanently_delete.confirmation = 選択されたファイルを永久に削除しますか ? delete_dialog.move_to_trash.confirmation = 選択されたファイルを削除しますか ? delete_dialog.move_to_trash.confirmation_details = ファイルはごみ箱へ移動されます。 delete_dialog.move_to_trash.option = ごみ箱へ移動 delete_dialog.deleting = 削除中 delete_dialog.error_title = 削除エラー delete.deleting_file = %1 の削除中 email_dialog.prefs_not_set_title = メールが構成されていません email_dialog.prefs_not_set = まずメール パラメーターを設定する必要があります。 email_dialog.from = 送信者 email_dialog.to = 宛先 email_dialog.subject = 件名 email_dialog.send = 送信 email_dialog.error_title = ファイルの電子メール エラー email_dialog.read_error = サブフォルダーのファイルを読み取ることができません。 email_dialog.sending = ファイルの送信中 email.sending_file = %1 の送信中 email.connecting_to_server = %1 へ接続中 email.server_unavailable = メール サーバー %1 に連絡することができません、メールの環境設定をチェックするか後で再び試行します。 email.connection_closed = 接続がサーバーによって閉じられました、メールが送信されませんでした。 email.goodbye_failed = 接続を閉じている間のエラーです、メールは送信されていない可能性があります。 email.send_file_error = ファイル %1 を送信することができません、メールが送信されませんでした。 file_selection_dialog.mark = マーク file_selection_dialog.unmark = マーク解除 file_selection_dialog.mark_description = マークするファイルのファイル名 file_selection_dialog.unmark_description = マーク解除するファイルのファイル名 file_selection_dialog.case_sensitive = 大文字と小文字を区別する file_selection_dialog.include_folders = フォルダーを含める progress_dialog.starting = 転送が開始されています... progress_dialog.transferred = %2 で %1 を転送しました progress_dialog.elapsed_time = 経過時間 progress_dialog.advanced = 詳細 progress_dialog.current_speed = 現在の速度 progress_dialog.limit_speed = 速度の制限 progress_dialog.close_when_finished = 完了時にウィンドウを閉じる progress_dialog.processing_files = ファイルの処理中 progress_dialog.processing_file = %1 の処理中 progress_dialog.verifying_file = %1 の検証中 progress_dialog.job_finished = ジョブが完了しました progress_dialog.job_error = ジョブ エラー properties_dialog.file_properties = %1 のプロパティ properties_dialog.contents = 内容 properties_dialog.calculating = 計算しています... calculate_checksum_dialog.checksum_algorithm = チェックサムのアルゴリズム calculate_checksum_dialog.temporary_file = 一時ファイル change_date_dialog.now = 現在 change_date_dialog.specific_date = 特定の日付 run_dialog.run_command_description = 現在のフォルダーで実行します run_dialog.run_in_home_description = ホーム フォルダーで実行します run_dialog.command_output = コマンド出力 run_dialog.run = 実行 run_dialog.clear_history = 履歴のクリア server_connect_dialog.server_type = 接続の種類 server_connect_dialog.server = サーバー server_connect_dialog.share = 共有 server_connect_dialog.username = ユーザー名 server_connect_dialog.initial_dir = 初期ディレクトリ server_connect_dialog.port = ポート server_connect_dialog.server_url = サーバーの URL server_connect_dialog.http_url = Web サイトの URL server_connect_dialog.connect = 接続 server_connect_dialog.protocol = プロトコル server_connect_dialog.nfs_version = NFS のバージョン server_connect_dialog.private_key = プライベート キー server_connect_dialog.passphrase = パスフレーズ ftp_connect.passive_mode = パッシブ モードを有効にする ftp_connect.anonymous_user = 匿名ユーザー ftp_connect.nb_connection_retries = 接続の再試行の数 ftp_connect.retry_delay = 再試行間の遅延 (秒) http_connect.basic_authentication = HTTP ベーシック認証 (オプション) server_connections_dialog.disconnect = 切断 server_connections_dialog.connection_busy = ビジー server_connections_dialog.connection_idle = アイドル bonjour.bonjour_services = Bonjour サービス bonjour.no_service_discovered = サービスが発見されませんでした bonjour.bonjour_disabled = Bonjour が無効 auth_dialog.title = 認証 auth_dialog.desc = ログインとパスワードを入力してください auth_dialog.server = サーバー auth_dialog.store_credentials = ログインとパスワードを格納する (弱い暗号化) auth_dialog.connect_as = 名義 auth_dialog.authentication_failed = 認証が失敗しました sortable_list.move_up = 上へ移動 sortable_list.move_down = 下へ移動 add_bookmark_dialog.add = 追加 edit_bookmarks_dialog.new = 新規 file_viewer.view_error_title = 表示エラー file_viewer.view_error = ファイルを表示することができません。 file_viewer.file_menu = ファイル file_viewer.close = 閉じる file_viewer.large_file_warning = このファイルはこの操作には大きすぎる可能性があります。 file_viewer.open_anyway = そのまま開く text_viewer.edit = 編集 text_viewer.copy = コピー text_viewer.select_all = すべて選択 text_viewer.find = 検索 text_viewer.find_next = 次を検索 text_viewer.find_previous = 前を検索 text_viewer.binary_file_warning = これはバイナリ ファイルにみえます image_viewer.controls_menu = 制御 image_viewer.zoom_in = 拡大 image_viewer.zoom_out = 縮小 file_editor.edit_error_title = 編集エラー file_editor.edit_error = ファイルを編集することができません。 file_editor.save = 上書き保存 file_editor.save_as = 名前を付けて保存... file_editor.save_warning = 閉じる前にこのファイルへの変更を保存しますか ? file_editor.cannot_write = ファイルを書き込むことができません。 text_editor.cut = 切り取り text_editor.paste = 貼り付け shortcuts_dialog.quick_search.start_search = クイック検索を開始するには何か文字を入力します shortcuts_dialog.quick_search.cancel_search = クイック検索の取り消し shortcuts_dialog.quick_search.remove_last_char = クイック検索の文字列から最後の文字を削除 shortcuts_dialog.quick_search.jump_to_previous = 前のクイック検索の結果へジャンプ shortcuts_dialog.quick_search.jump_to_next = 次のクイック検索の結果へジャンプ shortcuts_dialog.quick_search.mark_jump_next = 現在のファイルをマーク/解除して次の検索結果へジャンプ theme_editor.title = テーマ エディター theme_editor.folder_tab = フォルダー ペイン theme_editor.shell_tab = シェル theme_editor.shell_history_tab = シェルの履歴 theme_editor.statusbar_tab = ステータス バー theme_editor.free_space = 空き領域 theme_editor.free_space.ok = OK theme_editor.free_space.warning = 警告 theme_editor.free_space.critical = 致命的 theme_editor.locationbar_tab = ロケーション バー theme_editor.editor_tab = ファイル エディター theme_editor.font = フォント theme_editor.active_panel = アクティブ theme_editor.inactive_panel = 非アクティブ theme_editor.general = 全般 theme_editor.could_not_save_theme = テーマ %1 へ書き込むことができません theme_editor.border = 枠 theme_editor.background = 背景 theme_editor.alternate_background = 代替の背景 theme_editor.unfocused_background = 背景 (フォーカスなし) theme_editor.quick_search.unmatched_file = 不一致のファイル theme_editor.text = テキスト theme_editor.progress = 進行状況 theme_editor.normal = 通常 theme_editor.normal_unfocused = 通常 (フォーカスなし) theme_editor.selected = 選択済み theme_editor.selected_unfocused = 選択済み (フォーカスなし) theme_editor.color = 色 theme_editor.colors = 色 theme_editor.plain_file = プレーン ファイル theme_editor.marked_file = マーク済みファイル theme_editor.hidden_file = 隠しファイル theme_editor.folder = フォルダー theme_editor.archive_file = アーカイブ theme_editor.symbolic_link = シンボリック リンク prefs_dialog.title = 環境設定 prefs_dialog.general_tab = 全般 prefs_dialog.day = 日 prefs_dialog.month = 月 prefs_dialog.year = 年 prefs_dialog.language = 言語 (再起動を必要とします) prefs_dialog.date_time = 日時のフォーマット prefs_dialog.time = 時間 prefs_dialog.date = 日付 prefs_dialog.date_separator = 区切り prefs_dialog.time_12_hour = 12 時間制 prefs_dialog.time_24_hour = 24 時間制 prefs_dialog.show_seconds = 秒を表示する prefs_dialog.show_century = 世紀を表示する prefs_dialog.check_for_updates_on_startup = 起動時に更新をチェックする prefs_dialog.show_splash_screen = スプラッシュ画面を表示する prefs_dialog.folders_tab = フォルダー prefs_dialog.startup_folders = スタートアップ フォルダー prefs_dialog.left_folder = 左のフォルダー prefs_dialog.right_folder = 右のフォルダー prefs_dialog.last_folder = 最後に訪問したフォルダー prefs_dialog.custom_folder = カスタム フォルダー prefs_dialog.show_hidden_files = 隠しファイルを表示する prefs_dialog.show_ds_store_files = .DS_Store ファイルを表示する prefs_dialog.show_system_folders = システム フォルダーを表示する prefs_dialog.compact_file_size = 表示されるファイル サイズを四捨五入する prefs_dialog.follow_symlinks_when_cd = 現在のフォルダーの変更時にシンボリック リンクに追従する prefs_dialog.appearance_tab = 外観 prefs_dialog.look_and_feel = 外観 prefs_dialog.icons_size = アイコンのサイズ prefs_dialog.toolbar_icons = ツール バー prefs_dialog.command_bar_icons = コマンド バー prefs_dialog.file_icons = ファイルの種類 prefs_dialog.use_system_file_icons = システムのファイル アイコンを使用する prefs_dialog.use_system_file_icons.always = 常に prefs_dialog.use_system_file_icons.never = しない prefs_dialog.use_system_file_icons.applications = アプリケーションのみ prefs_dialog.edit_current_theme = 現在のテーマを編集... prefs_dialog.themes = テーマ prefs_dialog.import_theme = テーマのインポート prefs_dialog.import_look_and_feel = 外観のインポート prefs_dialog.no_look_and_feel = 外観は見つかりませんでした。 prefs_dialog.error_in_import = テーマ %1 のインポート中のエラーです。 prefs_dialog.cannot_read_theme = ファイル %1 からテーマを読み取れません prefs_dialog.export_theme = %1 のエクスポート prefs_dialog.import = インポート prefs_dialog.export = エクスポート prefs_dialog.theme_type = 種類: %1 prefs_dialog.delete_theme = テーマ %1 を永久に削除しますか ? prefs_dialog.delete_look_and_feel = 外観 %1 を永久に削除しますか? prefs_dialog.rename_failed = テーマ %1 の名前の変更に失敗しました prefs_dialog.xml_file = XML ファイル prefs_dialog.jar_file = JAR ファイル prefs_dialog.mail_tab = メール prefs_dialog.mail_settings = 発信メールの設定 prefs_dialog.mail_name = 名前 prefs_dialog.mail_address = 電子メール アドレス prefs_dialog.mail_server = SMTP サーバー prefs_dialog.misc_tab = さまざま prefs_dialog.use_brushed_metal = 'ブラシ メタル' ルックを使用する (再起動を必要とします) prefs_dialog.confirm_on_quit = 終了時に確認ダイアログを表示する prefs_dialog.default_shell = 既定のシステム シェルを使用する prefs_dialog.custom_shell = カスタム シェルを使用する prefs_dialog.shell_encoding = シェルのエンコード prefs_dialog.auto_detect_shell_encoding = 自動検出 prefs_dialog.enable_bonjour_discovery = Bonjour サービス ディスカバリを有効にする prefs_dialog.enable_system_notifications = システム通知を有効にする unit.byte = バイト unit.bytes = バイト unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mo duration.years = %1y theme.custom_theme = カスタム テーマ theme.custom = カスタマイズ済み theme.built_in = ビルトイン theme.add_on = アドオン theme.current = 現在 theme_could_not_be_loaded = このテーマの読み込み中にエラーが発生しました。 setup.title = trolCommander へようこそ setup.intro = trolCommander の挙動を選択してください。 setup.look_and_feel = 外観を選択します setup.theme = テーマを選択します font_chooser.font_size = サイズ font_chooser.font_bold = 太字 font_chooser.font_italic = 斜体 color_chooser.red = 赤 color_chooser.green = 緑 color_chooser.blue = 青 color_chooser.hue = 色合い color_chooser.brightness = 明るさ color_chooser.swatches = 色見本 color_chooser.saturation = 鮮やかさ color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = 最近 color_chooser.alpha = Alpha 透明度 color_chooser.title = 色の抽出 batch_rename_dialog.mask = 名前の変更パターン batch_rename_dialog.search_replace = 検索と置換 batch_rename_dialog.search_for = 検索する文字列 batch_rename_dialog.replace_with = 置換する文字列 batch_rename_dialog.counter = カウンター batch_rename_dialog.start_at = 開始 batch_rename_dialog.step_by = 増加 batch_rename_dialog.format = フォーマット batch_rename_dialog.upper_lower_case = 大文字と小文字 batch_rename_dialog.no_change = 変更しない batch_rename_dialog.lower_case = 小文字にする batch_rename_dialog.upper_case = 大文字にする batch_rename_dialog.first_upper = 最初の文字を大文字にする batch_rename_dialog.word = 単語の最初の文字を大文字にする batch_rename_dialog.old_name = 古い名前 batch_rename_dialog.new_name = 新しい名前 batch_rename_dialog.block_name = 除外 batch_rename_dialog.range = 範囲 batch_rename_dialog.proceed_renaming = %2 中 %1 個のファイルの名前が変更されます。続行しますか? batch_rename_dialog.duplicate_names = 重複する名前です! parent_folders_quick_list.empty_message = 現在の場所には親がありません recent_locations_quick_list.empty_message = 最近の場所がありません recent_executed_files_quick_list.empty_message = 最近実行したファイルがありません #auth_dialog.error_was = エラー: %1 #table.hide_column = 列を非表示にする #delete.symlink_warning_title = シンボリック リンクが見つかりました #delete.symlink_warning = このファイルはシンボリック リンクのようにみえます:\n\n ファイル: %1\n リンク先: %2\n\nシンボリック リンクを削除するか\nシンボリック リンクに追従してフォルダーを削除しますか (注意) ? #delete.delete_link_only = リンクの削除 #delete.delete_linked_folder = フォルダーの削除 #Unpack.label = ファイルのパック解除 #Pack.label = ファイルのパック ================================================ FILE: src/main/resources/dictionary_ko_KR.properties ================================================ ok = 승인 yes = 네 no = 아니오 cancel = 취소 edit = 편집 close = 닫기 rename = 이름변경 apply = 적용 save = 저장 dont_save = 저장하지 않음 replace = 바꾸기 dont_replace = 바꾸지않음 delete = 삭제 skip = 지나가기 retry = 재시도 resume = 이어쓰기 overwrite = 겹쳐쓰기 overwrite_if_older = 오래된 경우 겹쳐쓰기 duplicate = 중복 apply_to_all = 모두적용 copy = 복사 move = 이동 pack = 압축 unpack = 압축해제 download = 다운로드 browse = 탐색 ask = 질문 stop = 중지 pause = 일시중지 create = 작성 login = 로그인 password = 비밀번호 user = 사용자 name = 이름 size = 크기 date = 날짜 extension = 확장자 permissions = 권한 location = 위치 untitled = 이름없음 source = 원본 destination = 목적지 recurse_directories = 선택된 하위 디렉토리까지 작업 go_to = 이동 example = 예제 comment = 설명추가 nb_files = %1 파일 nb_folders = %1 폴더 warning = 주의 error = 오류 folder_does_not_exist = 폴더가 존재하지 않거나 사용할수 없습니다 this_folder_does_not_exist = 폴더가 존재하지 않거나 사용할수 없습니다: %1 invalid_path = 알수없는 경로: %1 directory_already_exists = 디렉토리 %1은 이미 존재합니다. file_exists_in_destination = 이미 파일이 존재 합니다 source_parent_of_destination = 자신의 하위 폴더로 이동하려고 합니다 cannot_read_file = 파일을 읽을수 없음 %1 cannot_write_file = 파일을 쓸수 없음 %1 cannot_create_folder = 디렉토리를 생성할수 없음 %1 cannot_read_folder = 폴더의 내용을 읽을수 없음 %1 cannot_delete_file = 파일을 삭제할수 없음 %1 cannot_delete_folder = 폴더를 삭제할수 없음 %1 error_while_transferring = 파일이동중에 오류가 발생하였습니다 %1 same_source_destination = 원본과 복사하려는 위치가 같습니다 file_already_exists = %1가 이미 존재합니다. 덮어쓰시겠습니까? write_error = 쓰기 오류 permissions.read = 읽기 permissions.write = 쓰기 permissions.executable = 실행가능 permissions.group = 그룹 permissions.other = 기타 permissions.octal_notation = 8진법 표시 action_categories.navigation = 탐색 action_categories.selection = 셀렉션 action_categories.view = 보기 action_categories.file_operations = 파일 작업 action_categories.windows = 창 Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = 북마크 추가 AddBookmark.tooltip = 현재 폴더를 북마크 목록에 추가 EditBookmarks.label = 북마크 편집 EditCredentials.label = 암호목록 편집 ChangeLocation.label = 현재위치 변경 ChangeDate.label = 날짜 변경 ChangeDate.tooltip = 선택된 파일의 날짜를 변경 ChangePermissions.label = 권한 정보 변경 ChangePermissions.tooltip = 선택된 파일에 대한 권한 정보 변경 CheckForUpdates.label = 최신버전 검사 CompareFolders.label = 폴더 비교 ConnectToServer.label = 서버에 접속 ConnectToServer.tooltip = 원격 서버에 접속 View.label = 보기 View.tooltip = 선택된 파일 보기 Edit.tooltip = 선택된 파일 편집 Copy.tooltip = 선택된 파일 복사 LocalCopy.label = 로컬 복사 LocalCopy.tooltip = 선택된 파일을 현재 폴더로 복사 Move.tooltip = 선택된 파일을 이동 Rename.tooltip = 선택된 파일을 이름변경 Mkdir.label = 디렉토리작성 Mkdir.tooltip = 현재 폴더에 디렉토리 작성 Mkfile.label = 파일작성 Mkfile.tooltip = 현재 폴더에 파일 작성 Delete.tooltip = 선택된 파일 삭제 Refresh.label = 새로고침 Refresh.tooltip = 현재 폴더를 새로고침 CloseWindow.label = 창닫기 CloseWindow.tooltip = 현재 창을 닫기 CopyFileNames.label = 파일 이름 복사 CopyFilePaths.label = 경로 복사 CopyFilesToClipboard.label = 파일 복사 PasteClipboardFiles.label = 파일 붙이기 Email.label = 메일로 보내기 Email.tooltip = 선택된 파일을 이메일 첨부로 보냄 GoBack.label = 이전으로 GoBack.tooltip = 이전 폴더로 이동 GoForward.label = 다음으로 GoForward.tooltip = 다음 폴더로 이동 GoToHome.label = 홈폴더로 이동 GoToParent.label = 상위폴더로 이동 GoToParent.tooltip = 상위폴더로 이동 GoToRoot.label = 최상위 폴더로 SortByName.label = 이름으로 정렬 SortByDate.label = 날짜로 정렬 SortBySize.label = 크기로 정렬 SortByExtension.label = 확장자로 정렬 SortByPermissions.label = 권한별로 정렬 MarkGroup.label = 파일 선택 MarkGroup.tooltip = 여러 파일 선택 UnmarkGroup.label = 파일 선택 해제 UnmarkGroup.tooltip = 여러 파일 선택 해제 MarkAll.label = 모두 선택 UnmarkAll.label = 모두 선택 해제 MarkSelectedFile.label = 선택/해제 MarkSelectedFile.tooltip = 선택된 파일에 대해 선택/해제 MarkNextPage.label = 페이지 아래로 선택 MarkPreviousPage.label = 페이지 위로 선택 MarkToFirstRow.label = 시작부터 선택 MarkToLastRow.label = 끝까지 선택 InvertSelection.label = 선택 반전 SwapFolders.label = 폴더를 서로 바꾸기 SwapFolders.tooltip = 좌우 폴더를 서로 바꾸기 SetSameFolder.label = 같은 폴더로 설정 SetSameFolder.tooltip = 좌우를 같은 폴더로 설정 NewWindow.label = 새창 NewWindow.tooltip = 새로운 창을 열기 Open.label = 열기 Open.tooltip = 폴더 열기 / 압축파일 열기 / 실행 OpenNatively.label = 시스템에서 실행 OpenNatively.tooltip = 선택된 파일을 시스템에 연결된 프로그램으로 실행 RevealInDesktop.label = %1에서 보기 RunCommand.label = 명령어 실행 RunCommand.tooltip = 현재 폴더에서 명령어 실행 Pack.tooltip = 선택된 파일을 압축 Unpack.tooltip = 선택된 압축파일 해제 ShowFileProperties.label = 환경설정 ShowFileProperties.tooltip = 선택된 파일의 환경설정 표시 ShowPreferences.label = 환경설정 ShowPreferences.tooltip = trolCommander 설정 ShowServerConnections.label = 열린 접속 보이기 Quit.label = 종료 ReverseSortOrder.label = 순서 변경 ToggleAutoSize.label = 컬럼 자동 크기 조정 Stop.label = 폴더 변경 중단 ToggleCommandBar.show = 커맨드바 보이기 ToggleCommandBar.hide = 커맨드바 숨기기 ToggleToolBar.show = 툴바 보이기 ToggleToolBar.hide = 툴바 숨기기 ToggleStatusBar.show = 상태바 보이기 ToggleStatusBar.hide = 상태바 숨기기 ToggleShowFoldersFirst.label = 폴더를 먼저 보이기 PopupLeftDriveButton.label = 왼쪽 폴더 변경 PopupRightDriveButton.label = 오른쪽 폴더 변경 RecallPreviousWindow.label = 이전창 불러오기 RecallNextWindow.label = 다음창 불러오기 RecallWindow.label = 창 불러오기 #%1 SwitchActiveTable.label = 좌우 판넬을 바꿈 SelectFirstRow.label = 현재 폴더에 처음 파일 셀렉트 SelectLastRow.label = 현재 폴더에 마지막 파일 셀렉트 SplitEqually.label = 똑같은 크기로 분할 SplitVertically.label = 수직으로 분할 SplitHorizontally.label = 수평으로 분할 ShowKeyboardShortcuts.label = 키보드 단축키 GoToWebsite.label = 웹사이트 이동 GoToForums.label = 포럼으로 이동 ReportBug.label = 버그 보고 Donate.label = 기부 하기 ShowAbout.label = trolCommander에 대해서 file_menu = 파일 file_menu.open_with = 연결프로그램 mark_menu = 선택 view_menu = 보기 view_menu.show_hide_columns = 컬럼 표시/숨김 go_menu = 이동 bookmarks_menu = 북마크 bookmarks_menu.no_bookmark = 북마크 없음 window_menu = 윈도우 help_menu = 도움말 status_bar.selected_files = %1 / %2 선택됨 status_bar.connecting_to_folder = 폴더 접속중, 취소하려면 ESCAPE를 눌러주세요. status_bar.volume_free = 빈공간: %1 status_bar.volume_capacity = 용량: %1 table.folder_access_error_title = 폴더 접근 오류 table.folder_access_error = 폴더의 내용을 읽을수 없음 table.download_or_browse = 탐색을 하거나 이 파일을 다운로드 받겠습니까? version_dialog.no_new_version_title = 새로운 버전이 없습니다 version_dialog.no_new_version = 축하합니다. 이미 최신버전 입니다 version_dialog.new_version_title = 최신버전 알림 version_dialog.new_version = 새로운 버전의 trolCommander가 사용 가능합니다 version_dialog.new_version_url = 새로운 버전의 trolCommander를 다음 주소에서 받아 주십시오 %1. version_dialog.not_available_title = 서버 접근 오류 version_dialog.not_available = 서버에서 버전정보를 얻을수 없습니다 quit_dialog.title = trolCommander 종료 quit_dialog.desc = 모든 창들과 trolCommander를 종료 하시겠습니까? quit_dialog.show_next_time = 다음에도 보입니다 destination_dialog.file_exists_action = 파일이 존재할때 기본설정으로 file_collision_dialog.title = 파일 충돌 copy_dialog.destination = 선택된 파일을 복사 copy_dialog.error_title = 복사 오류 copy_dialog.copying = 파일 복사중 copy_dialog.copying_file = 복사중 %1 pack_dialog.packing = 파일 압축 pack_dialog.packing_file = 압축중 %1 pack_dialog.error_title = 압축오류 pack_dialog_description = 선택된 파일을 추가 pack_dialog.archive_format = 압축파일 형식 unpack_dialog.destination = 압축파일을 선택한 곳으로 해제합니다 unpack_dialog.error_title = 압축해제 오류 unpack_dialog.unpacking = 압축해제중 unpack_dialog.unpacking_file = 압축해제중 %1 move_dialog.move_description = 이동 위치 move_dialog.error_title = 이동 오류 move_dialog.moving = 파일 이동중 move_dialog.moving_file = 이동중 %1 download_dialog.description = 다운로드 파일 위치 download_dialog.error_title = 다운로드 오류 download_dialog.downloading = 다운로드중 download_dialog.downloading_file = 다운로드중 %1 delete_dialog.permanently_delete.confirmation = 선택한 파일을 영구히 삭제 하겠습니까? delete_dialog.deleting = 삭제중 delete_dialog.error_title = 삭제 오류 delete.deleting_file = 삭제중 %1 email_dialog.prefs_not_set_title = 메일이 설정되지 않음 email_dialog.prefs_not_set = 메일 파라미터 설정을 해야 합니다 email_dialog.from = 보내는 사람 email_dialog.to = 받는사람 email_dialog.subject = 제목 email_dialog.send = 보내기 email_dialog.error_title = 이메일 보내기 오류 email_dialog.read_error = 하위 폴더에서 파일을 읽을수 없습니다 email.sending_file = 파일 보내는중 %1 email.connecting_to_server = 연결중 %1 email.server_unavailable = 메일 서버에 연결할수 없습니다 %1, 메일 설정을 확인하거나 나중에 재시도 해주세요 email.connection_closed = 서버에서 접속이 종료되었습니다. 메일이 발송되지 않았습니다 email.goodbye_failed = 접속을 닫는중에 오류가 발생하였습니다. 메일이 발송되지 않을수 있습니다. email.send_file_error = 파일 %1을 보내지 못했습니다, 메일을 보낼수 없습니다. file_selection_dialog.mark = 선택 file_selection_dialog.unmark = 선택 해제 file_selection_dialog.mark_description = 파일 이름으로 선택 file_selection_dialog.unmark_description = 파일 이름으로 선택 해제 file_selection_dialog.case_sensitive = 대소문자 구분 file_selection_dialog.include_folders = 폴더포함 progress_dialog.starting = 이동시작... progress_dialog.transferred = 이동됨 %1 속도 %2 progress_dialog.elapsed_time = 경과시간 progress_dialog.advanced = 고급설정 progress_dialog.current_speed = 현재속도 progress_dialog.limit_speed = 속도제한 progress_dialog.close_when_finished = 종료후 윈도우 닫기 progress_dialog.processing_files = 파일 처리중 progress_dialog.processing_file = 처리중 %1 progress_dialog.job_finished = 작업 종료 properties_dialog.file_properties = %1 속성 properties_dialog.contents = 내용 properties_dialog.calculating = 계산중... change_date_dialog.now = 현재 change_date_dialog.specific_date = 지정날짜 run_dialog.run_command_description = 현재 폴더에서 실행 run_dialog.run_in_home_description = 홈 폴더에서 실행 run_dialog.command_output = 커맨드 출력 run_dialog.run = 실행 run_dialog.clear_history = 기록지우기 server_connect_dialog.server_type = 접속 방식 server_connect_dialog.server = 서버 server_connect_dialog.username = 유저이름 server_connect_dialog.initial_dir = 시작 디렉토리 server_connect_dialog.port = 포트 server_connect_dialog.server_url = 서버 URL server_connect_dialog.http_url = 웹사이트 URL server_connect_dialog.connect = 연결 ftp_connect.passive_mode = passive 모드 설정 ftp_connect.anonymous_user = 익명연결 http_connect.basic_authentication = HTTP 기본 인증 (선택사항) server_connections_dialog.disconnect = 접속종료 server_connections_dialog.connection_busy = 바쁨 server_connections_dialog.connection_idle = 쉬는중 bonjour.bonjour_services = Bonjour 서비스 bonjour.no_service_discovered = 서비스가 발견되지 않음 bonjour.bonjour_disabled = Bonjour 사용불가 auth_dialog.title = 인증 auth_dialog.desc = login, password를 입력해주세요 auth_dialog.server = 서버 auth_dialog.store_credentials = login, password 저장 (약한 보안) sortable_list.move_up = 위로 sortable_list.move_down = 아래로 add_bookmark_dialog.add = 추가 edit_bookmarks_dialog.new = 신규 file_viewer.view_error_title = 보기 오류 file_viewer.view_error = 파일을 볼수 없음 file_viewer.file_menu = 파일 file_viewer.close = 닫기 file_viewer.large_file_warning = 이 작업을 하기에 파일이 너무 큽니다. file_viewer.open_anyway = 계속열기 text_viewer.edit = 편집 text_viewer.copy = 복사 text_viewer.select_all = 모두선택 image_viewer.controls_menu = 조정 image_viewer.zoom_in = 확대 image_viewer.zoom_out = 축소 file_editor.edit_error_title = 편집 오류 file_editor.edit_error = 파일을 편집할수 없음 file_editor.save = 저장 file_editor.save_as = 새이름으로 저장... file_editor.save_warning = 파일을 닫기전 저장하시겠습니까? file_editor.cannot_write = 파일을 기록할수 없음 text_editor.cut = 잘라내기 text_editor.paste = 붙여넣기 shortcuts_dialog.quick_search.start_search = 빠른 검색을 위하여, 적절한 단어를 입력해보세요. shortcuts_dialog.quick_search.cancel_search = 빠른 검색 취소 shortcuts_dialog.quick_search.remove_last_char = 빠른 검색 단어에서 마지막 글자를 지웁니다 shortcuts_dialog.quick_search.jump_to_previous = 이전 빠른 검색 결과로 이동 shortcuts_dialog.quick_search.jump_to_next = 다음 빠른 검색 결과로 이동 shortcuts_dialog.quick_search.mark_jump_next = 현재 파일을 선택/선택해제하고 다음 검색결과로 이동 theme_editor.font = 글꼴 theme_editor.background = 바탕 theme_editor.plain_file = 일반 파일 theme_editor.marked_file = 선택된 파일 theme_editor.hidden_file = 숨김파일 theme_editor.folder = 폴더 theme_editor.archive_file = 아카이브 파일 theme_editor.symbolic_link = 심볼릭 링크 prefs_dialog.title = 환경설정 prefs_dialog.general_tab = 일반 prefs_dialog.day = 일 prefs_dialog.month = 월 prefs_dialog.year = 년 prefs_dialog.language = 언어 (재시작 필요) prefs_dialog.date_time = 날짜 시간 형식 prefs_dialog.time = 시간 prefs_dialog.date = 날짜 prefs_dialog.date_separator = 분리기호 prefs_dialog.time_12_hour = 12시간제 prefs_dialog.time_24_hour = 24시간제 prefs_dialog.show_seconds = 초 보임 prefs_dialog.show_century = 세기(100년)보임 prefs_dialog.check_for_updates_on_startup = 시작시에 최신버전 확인 prefs_dialog.folders_tab = 폴더 prefs_dialog.startup_folders = 시작 폴더 prefs_dialog.left_folder = 왼쪽 폴더 prefs_dialog.right_folder = 오른쪽 폴더 prefs_dialog.last_folder = 최종 방문 폴더 prefs_dialog.custom_folder = 사용자 정의 폴더 prefs_dialog.show_hidden_files = 숨김 파일 표시 prefs_dialog.show_ds_store_files = .DS_Store 파일 표시 prefs_dialog.show_system_folders = 시스템 폴더 표시 prefs_dialog.compact_file_size = 파일 크기(용량) 올림 prefs_dialog.appearance_tab = 모양새 prefs_dialog.look_and_feel = 룩엔필 prefs_dialog.icons_size = 아이콘 크기 prefs_dialog.toolbar_icons = 도구 바 prefs_dialog.command_bar_icons = 명령 바 prefs_dialog.file_icons = 파일 타입 prefs_dialog.use_system_file_icons = 시스템파일 아이콘 사용 prefs_dialog.use_system_file_icons.always = 항상사용 prefs_dialog.use_system_file_icons.never = 사용안함 prefs_dialog.use_system_file_icons.applications = 어플리케이션만 prefs_dialog.themes = 테마 prefs_dialog.mail_tab = 메일 prefs_dialog.mail_settings = 보내는 메일 설정 prefs_dialog.mail_name = 이름 prefs_dialog.mail_address = 메일 주소 prefs_dialog.mail_server = SMTP 서버 prefs_dialog.misc_tab = 기타 prefs_dialog.use_brushed_metal = '브러쉬드 메탈' 룩을 사용합니다 (재시작 필요) prefs_dialog.confirm_on_quit = 종료시에 확인창을 띄웁니다 prefs_dialog.default_shell = 기본 시스템 쉘을 사용합니다 prefs_dialog.custom_shell = 사용자 정의 쉘을 사용합니다 prefs_dialog.enable_bonjour_discovery = Bonjour서비스 탐색을 켭니다 unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mo duration.years = %1y theme.custom_theme = 사용자 테마 theme_could_not_be_loaded = 테마를 읽는중에 오류가 발생하였습니다 setup.title = trolCommander에 환영합니다 setup.intro = trolCommander가 작동하는 방식을 선택하세요 setup.look_and_feel = 룩엔필을 선택하세요 setup.theme = 테마선택 font_chooser.font_size = 크기 font_chooser.font_bold = 볼드 font_chooser.font_italic = 이탤릭 color_chooser.title = 색상 선택 #move_dialog.rename_description = 이름변경 위치 #theme_editor.shell_colors = 쉘 색상 #auth_dialog.error_was = 오류: %1 #table.hide_column = 컬럼 숨김 #delete.symlink_warning_title = 심볼릭링크 발견됨 #delete.symlink_warning = 이 파일은 심볼릭 링크로 보입니다:\n\n 파일: %1\n 연결됨: %2\n\n심볼릭 링크만 삭제하겠습니까\n심볼릭 링크를 따라가서 폴더를 지우겠습니까 (주의) ? #delete.delete_link_only = 링크삭제 #delete.delete_linked_folder = 폴더삭제 #Unpack.label = 파일 압축해제 #Pack.label = 파일압축 ================================================ FILE: src/main/resources/dictionary_nl_NL.properties ================================================ ok = OK yes = Ja no = Nee cancel = Annuleren edit = Bewerken close = Sluiten reset = Resetten rename = Hernoemen apply = Toepassen change = Veranderen save = Opslaan dont_save = Verwerpen replace = Vervangen dont_replace = Niet vervangen delete = Verwijderen skip = Overslaan skip_all = Alles overslaan retry = Opnieuw proberen resume = Doorgaan overwrite = Overschrijven overwrite_if_older = Oudere overschrijven duplicate = Dupliceren apply_to_all = Toepassen op alle copy = Kopiëren move = Verplaatsen pack = Inpakken unpack = Uitpakken download = Downloaden split = Splitsen combine = Samenvoegen browse = Bladeren ask = Vragen stop = Afbreken pause = Onderbreken quick_search = Snelzoeken file_manager = Bestandsbeheerder create = Maken creating_file = %1 wordt gemaakt choose = Uitkiezen customize = Aanpassen choose_folder = Kies map login = Login password = Wachtwoord user = Gebruiker encoding = Tekenset preferred_encodings = Voorkeurs tekenset license = Licentie name = Naam size = Grootte date = Datum en tijd extension = Extensie permissions = Rechten owner = Eigenaar group = Groep location = Plaats untitled = Zonder naam source = Bron destination = Doellocatie recurse_directories = Toepassen op onderliggende mappen go_to = Ga naar example = Voorbeeld preview = Voorbeeld comment = Commentaar sample_text = Voorbeeldtekst nb_files = %1 Bestand(en) nb_folders = %1 map(pen) loading = Laden... this_operation_cannot_be_undone = Deze bewerking kan niet ongedaan gemaakt worden. remove = Verwijder details = Details warning = Waarschuwing error = Fout generic_error = Er heeft zich een fout voorgedaan bij het uitvoeren van de gevraagde taak. folder_does_not_exist = Map bestaat niet of is niet beschikbaar this_folder_does_not_exist = Deze map bestaat niet of is niet beschikbaar: %1 this_file_does_not_exist = Dit bestand bestaat niet of is niet beschikbaar: %1 invalid_path = Ongeldige padaanduiding: %1 directory_already_exists = Er is al een map %1 file_exists_in_destination = Bestand bestaat al in het doel source_parent_of_destination = Poging om een map in een onderliggende map te verplaatsen cannot_read_file = Kan bestand niet lezen %1 cannot_write_file = Kan bestand niet schrijven %1 cannot_create_folder = Kan map niet maken %1 cannot_read_folder = Kan inhoud van de map niet lezen %1 cannot_delete_file = Kan bestand niet wissen %1 cannot_delete_folder = Kan map niet wissen %1 error_while_transferring = Fout bij het verplaatsen van bestand %1 same_source_destination = Oorspronkelijk bestand is hetzelfde als doel file_already_exists = %1 bestaat al, wilt u het vervangen ? write_error = Fout bij het schrijven read_error = Leesfout integrity_check_error = Integriteitscheck negatief: bron en doel komen niet overeen startup_error = Een fout verhinderde het opstarten van trolCommander. permissions.read = Lezen permissions.write = Schrijven permissions.executable = Uitvoerbaar permissions.group = Groep permissions.other = Anderen permissions.octal_notation = Octale schrijfwijze action_categories.all = Alle action_categories.navigation = Navigatie action_categories.selection = Selectie action_categories.view = Beeld action_categories.file_operations = Bestandsbewerkingen action_categories.windows = Vensters Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Toevoegen aan bladwijzers AddBookmark.tooltip = Voeg huidige map aan bladwijzers toe BatchRename.label = Multi hernoemen EditBookmarks.label = Bladwijzers bewerken ExploreBookmarks.label = Bladwijzers verkennen EditCredentials.label = Toegangsgegevens bewerken ChangeLocation.label = Huidige locatie veranderen ChangeDate.label = Verander datum ChangeDate.tooltip = Verander datum van gemarkeerde bestanden ChangePermissions.label = Rechten veranderen ChangePermissions.tooltip = Rechten van gemarkeerde bestanden wijzigen CheckForUpdates.label = Zoeken naar updates CompareFolders.label = Vergelijk mappen ConnectToServer.label = Met server verbinden ConnectToServer.tooltip = Met server op afstand verbinden View.label = Bekijken InternalView.label = Bekijken (intern) View.tooltip = Geselecteerd bestand bekijken InternalEdit.label = Bewerken (intern) Edit.tooltip = Gemarkeerd bestand bewerken Copy.tooltip = Kopieer gemarkeerde bestanden LocalCopy.label = Locale kopie LocalCopy.tooltip = Kopieer gemarkeerde bestanden naar huidige directory Move.tooltip = Gemarkeerde bestanden verplaatsen Rename.tooltip = Geselecteerd bestand hernoemen Mkdir.label = Maak map Mkdir.tooltip = Maak map in huidige map Mkfile.label = Maak bestand Mkfile.tooltip = Maak bestand in huidig directory Delete.tooltip = Gemarkeerde bestanden verwijderen met gebruik van de prullenbak indien mogelijk PermanentDelete.label = Definitief verwijderen PermanentDelete.tooltip = Verwijder gemarkeerde bestanden zonder de prullenbak te gebruiken Refresh.label = Vernieuwen Refresh.tooltip = Vernieuw huidige map CloseWindow.label = Venster sluiten CloseWindow.tooltip = Dit venster sluiten CopyFileNames.label = Naam kopiëren CopyFilePaths.label = Kopieer pad CopyFilesToClipboard.label = Bestand(en) kopiëren PasteClipboardFiles.label = Bestand(en) plakken Email.label = Bestanden emailen Email.tooltip = Verstuur gemarkeerde bestanden als bijlagen in een email GoBack.label = Ga terug GoBack.tooltip = Teruggaan naar vorige map GoForward.label = Naar volgende GoForward.tooltip = Naar volgende map GoToHome.label = Ga naar home-map GoToParent.label = Naar bovenliggende GoToParent.tooltip = Naar bovenliggende map GoToParentInOtherPanel.label = Ga naar bovenliggende map in het andere paneel GoToParentInBothPanels.label = Ga naar bovenliggende map in beide panelen GoToRoot.label = Ga naar root SortByName.label = Sorteren op naam SortByDate.label = Sorteren op datum SortBySize.label = Sorteren op grootte SortByExtension.label = Sorteren op extensie SortByPermissions.label = Sorteren op rechten SortByOwner.label = Sorteren op eigenaar SortByGroup.label = Sorteren op groep MarkGroup.label = Bestanden markeren MarkGroup.tooltip = Een groep bestanden markeren UnmarkGroup.label = Bestanden demarkeren UnmarkGroup.tooltip = Demarkeer groep bestanden MarkAll.label = Alles markeren UnmarkAll.label = Demarkeer alles MarkSelectedFile.label = Markeer/Demarkeer MarkSelectedFile.tooltip = Markeer/demarkeer geselecteerd bestand MarkNextBlock.label = Markeer een blok naar beneden MarkPreviousBlock.label = Markeer een blok omhoog MarkNextRow.label = Markeer een regel naar beneden MarkPreviousRow.label = Markeer een regel omhoog MarkNextPage.label = Markeer bestanden tot volgende bladzijde MarkPreviousPage.label = Markeer bestanden tot vorige pagina MarkToFirstRow.label = Markeer bestanden tot begin MarkToLastRow.label = Markeer bestanden tot eind MarkExtension.label = Markeer extensie InvertSelection.label = Selectie omkeren SwapFolders.label = Verwissel vensters SwapFolders.tooltip = Linker en rechter mappen wisselen SetSameFolder.label = Op zelfde map zetten SetSameFolder.tooltip = Dezelfde map in linker en rechter venster NewWindow.label = Nieuw venster NewWindow.tooltip = Nieuw venster openen Open.label = Openen Open.tooltip = Open map/ Open archief / Uitvoeren OpenNatively.label = Bestand met eigen toepassing openen OpenNatively.tooltip = Uitvoeren met systeem-geassocieerd programma OpenInOtherPanel.label = Openen in nieuw scherm OpenInBothPanels.label = Openen in beide schermen RevealInDesktop.label = Toon in %1 RunCommand.label = Commando uitvoeren RunCommand.tooltip = Commando uitvoeren in huidige map Pack.tooltip = Gemarkeerde bestanden inpakken in een archief Unpack.tooltip = Gemarkeerde archefbestanden uitpakken ShowFileProperties.label = Eigenschappen ShowFileProperties.tooltip = Toon eigenschappen van gemarkeerde bestanden ShowPreferences.label = Voorkeuren ShowPreferences.tooltip = trolCommander configureren ShowServerConnections.label = Toon open verbindingen Quit.label = Beëindigen ReverseSortOrder.label = Volgorde omkeren ToggleAutoSize.label = Kolombreedte automatisch bepalen Stop.label = Verandering aan mappen afbreken ToggleColumn.show = Toon de kolom %1 ToggleColumn.hide = Verberg de kolom %1 ToggleCommandBar.show = Commandobalk tonen ToggleCommandBar.hide = Commandobalk verbergen ToggleToolBar.show = Toon gereedschappenbalk ToggleToolBar.hide = Gereedschappenbalk verbergen CustomizeCommandBar.label = Commandobalk aanpassen ToggleStatusBar.show = Toon statusbalk ToggleStatusBar.hide = Statusbalk verbergen ToggleShowFoldersFirst.label = Toon mappen eerst ToggleTree.label = Toon boomweergave PopupLeftDriveButton.label = Linker map veranderen PopupRightDriveButton.label = Rechter map veranderen RecallPreviousWindow.label = Terug naar vorige venster RecallNextWindow.label = Terug naar volgende venster RecallWindow.label = Terug naar venster #%1 BringAllToFront.label = Alles naar voren halen SwitchActiveTable.label = Wissel tussen linker en rechter venster SelectNextBlock.label = Een blok naar beneden SelectPreviousBlock.label = Een blok omhoog SelectNextPage.label = Een bladzijde omlaag SelectPreviousPage.label = Een bladzijde omhoog SelectNextRow.label = Een regel omlaag SelectPreviousRow.label = Een regel omhoog SelectFirstRow.label = Selecteer eerste bestand in huidige map SelectLastRow.label = Selecteer laatste bestand in huidig directory SplitEqually.label = Splits in gelijke delen SplitVertically.label = Splits verticaal SplitHorizontally.label = Splits horizontaal ShowKeyboardShortcuts.label = Sneltoetsen GoToWebsite.label = Naar website GoToForums.label = Naar forum ReportBug.label = Een bug melden Donate.label = Doe een donatie ShowAbout.label = Over trolCommander OpenTrash.label = Open prullenbak EmptyTrash.label = Prullenbak leeg maken CalculateChecksum.label = Checksum berekenen MaximizeWindow.label = Maximaliseren MaximizeWindow.label.mac_os_x = Zoom MinimizeWindow.label = Minimaliseren GoToDocumentation.label = Online dokumentatie ShowParentFoldersQL.label = Bovenliggende mappen ShowRecentLocationsQL.label = Recente locaties ShowRecentExecutedFilesQL.label = Recent uitgevoerde bestanden SplitFile.tooltip = Bestand opsplitsen in verschillende gedeeltes CombineFiles.tooltip = Gesplitse bestandsdelen samenvoegen om het orrigineel terug te krijgen ShowDebugConsole.label = Debug console FocusPrevious.label = Focus op vorige component FocusNext.label = Focus op vorige component file_menu = Bestand file_menu.open_with = Openen met mark_menu = Markeren view_menu = Beeld view_menu.show_hide_columns = Kolommen tonen/verbergen go_menu = Toepassen bookmarks_menu = Bladwijzers bookmarks_menu.no_bookmark = Geen bladwijzer quick_lists_menu = Snellijsten window_menu = Venster help_menu = Help status_bar.selected_files = %1 van %2 geselecteerd status_bar.connecting_to_folder = Verbinden met map, druk ESC om af te breken status_bar.volume_free = Vrij: %1 status_bar.volume_capacity = Capaciteit: %1 shortcuts_panel.title = Sneltoetsen shortcuts_panel.restore_defaults = Standaard instellingen shortcuts_panel.show = Toon shortcuts_panel.default_message = Druk op Enter of dubbelklik op de sneltoets die u wilt bewerken shortcuts_table.action_description = Beschrijving van de actie shortcuts_table.shortcut = Sneltoets shortcuts_table.alternate_shortcut = Alternatieve sneltoets shortcuts_table.type_in_a_shortcut = Voer een sneltoets in command_bar_dialog.help = Sleep knoppen om de commandobalk aan te passen table.folder_access_error_title = Fout bij toegang tot map table.folder_access_error = Kan mapinhoud niet lezen table.download_or_browse = Wilt u dit bestand verkennen of wilt u het downloaden? version_dialog.no_new_version_title = Geen nieuwe versie version_dialog.no_new_version = Gefeliciteerd, u hebt al de nieuwste versie. version_dialog.new_version_title = Nieuwe versie beschikbaar version_dialog.new_version = Een nieuwe versie van trolCommander is beschikbaar. version_dialog.new_version_url = Een nieuwe versie van trolCommander is beschikbaar op %1. version_dialog.not_available_title = Server niet berijkbaar version_dialog.not_available = Kan geen versie-informatie van server krijgen version_dialog.install_and_restart = Installeren en opnieuw opstarten version_dialog.preparing_for_update = Update voorbereiden... quit_dialog.title = trolCommander afsluiten quit_dialog.desc = Er zijn %1 venster(s) geopend. Alle vensters sluiten en trolCommander afsluiten? quit_dialog.show_next_time = Toon volgende keer destination_dialog.file_exists_action = Voorkeursactie wanneer bestand er is destination_dialog.verify_integrity = Verifieer data-integriteit destination_dialog.skip_errors = Negeer fouten file_collision_dialog.title = Dataconflict rename_dialog.new_name = Nieuwe naam copy_dialog.destination = Kopieer gemarkeerde bestand(en) naar copy_dialog.error_title = Kopieerfout copy_dialog.copying = Kopieer bestanden copy_dialog.copying_file = Kopiëren %1 pack_dialog.packing = Bezig bestanden in te pakken pack_dialog.packing_file = Inpakken %1 pack_dialog.error_title = Fout bij inpakken pack_dialog_description = Voeg gemarkeerde bestanden toe aan pack_dialog.archive_format = Archiefformaat unpack_dialog.destination = Pak gemarkeerde bestand(en) uit naar unpack_dialog.error_title = Fout bij uitpakken unpack_dialog.unpacking = Bezig bestanden uit te pakken unpack_dialog.unpacking_file = Uitpakken %1 optimizing_archive = Optimaliseren van archief %1 error_while_optimizing_archive = Fout bij het optimaliseren van archief %1 move_dialog.move_description = Verplaats naar move_dialog.error_title = Fout bij verplaatsen move_dialog.moving = Bezig te verplaatsen move_dialog.moving_file = Verplaatsen %1 download_dialog.description = Download bestand naar download_dialog.error_title = Fout bij downloaden download_dialog.downloading = Downloaden download_dialog.downloading_file = Downloaden %1 mkfile_dialog.allocate_space = Bestandsruimte toewijzen delete_dialog.permanently_delete.confirmation = Gemarkeerde bestand(en) voorgoed verwijderen ? delete_dialog.move_to_trash.confirmation = Wis gemarkeerd(e) bestand(en) ? delete_dialog.move_to_trash.confirmation_details = Bestanden worden naar de prullenbak verplaatst. delete_dialog.move_to_trash.option = Verplaats naar prullenbak delete_dialog.move_to_trash.failed = Een of meer bestanden konden niet naar de prullenbak verplaatst worden. delete_dialog.deleting = Verwijderen delete_dialog.error_title = Fout bij verwijderen delete.deleting_file = Verwijderen %1 email_dialog.prefs_not_set_title = Email niet geconfigureerd email_dialog.prefs_not_set = U moet eerst uw emailvoorkeuren instellen email_dialog.from = Van email_dialog.to = Aan email_dialog.subject = Onderwerp email_dialog.send = Verzenden email_dialog.error_title = Fout bij versturen van email email_dialog.read_error = Kan bestanen in onderliggende mappen niet lezen email_dialog.sending = Verzenden bestanden email.sending_file = Versturen %1 email.connecting_to_server = Verbinden met %1 email.server_unavailable = Kan niet verbinden met server %1, kontroleer uw emailvoorkeuren of probeer het later nog eens. email.connection_closed = Verbinding door server verbroken, email niet verzonden email.goodbye_failed = Fout bij het beeinigen van de verbinding, email eventueel niet verzonden email.send_file_error = Kan bestand %1 niet verzenden, emial niet verstuurd. split_file_dialog.error_title = Fout bij bestanden opsplitsen split_file_dialog.file_to_split = Te splitsen bestand split_file_dialog.target_directory = Doelmap split_file_dialog.part_size = Grootte van de stukken split_file_dialog.parts = Hoeveelheid stukken split_file_dialog.generate_CRC = Creëer CRC bestand split_file_dialog.max_parts = Maximum hoeveelheid delen is %1 split_file_dialog.auto = Automatisch split_file_dialog.insert_new_media = Nieuw medium invoeren combine_files_dialog.error_title = Fout bij bestand samenvoegen combine_files_job.no_crc_file = Samenvoegen geslaagd. Geen CRC bestand. combine_files_job.crc_read_error = Fout bij lezen CRC bestand. combine_files_job.crc_check_failed = CRC past niet: verwacht %2, gevonden %1 combine_files_job.crc_ok = Samenvoegen geslaagd. CRC controlsom ok. file_selection_dialog.mark = Markeren file_selection_dialog.unmark = Demarkeren file_selection_dialog.mark_description = Markeer bestanden met de naam file_selection_dialog.unmark_description = Demarkeer bestanden met de naam file_selection_dialog.case_sensitive = Hoofdlettergevoelig file_selection_dialog.include_folders = Mappen erbij nemen progress_dialog.starting = Start overdracht... progress_dialog.transferred = Overgedragen %1 met %2 progress_dialog.elapsed_time = Tijdsduur progress_dialog.advanced = Geavanceerd progress_dialog.current_speed = Huidige snelheid progress_dialog.limit_speed = Maximale snelheid progress_dialog.close_when_finished = Na beëinigen venster sluiten progress_dialog.processing_files = Bestanden bewerken progress_dialog.processing_file = Bewerken %1 progress_dialog.verifying_file = %1 verifiëren progress_dialog.job_finished = Taak beeindigd progress_dialog.job_error = Fout bij uitvoeren taak properties_dialog.file_properties = Eigenschappen van %1 properties_dialog.contents = Inhoud properties_dialog.calculating = Berekenen... calculate_checksum_dialog.checksum_algorithm = Checksum algoritme calculate_checksum_dialog.temporary_file = Tijdelijk bestand change_date_dialog.now = Nu change_date_dialog.specific_date = Bepaalde datum run_dialog.run_command_description = Voer uit in huidige map run_dialog.run_in_home_description = Voer uit in home-map run_dialog.command_output = Commando output run_dialog.run = Uitvoeren run_dialog.clear_history = Geschiedenis wissen server_connect_dialog.server_type = Verbindingstype server_connect_dialog.server = Server server_connect_dialog.share = Delen server_connect_dialog.domain = Domein server_connect_dialog.username = Gebruikersnaam server_connect_dialog.initial_dir = Begindirectory server_connect_dialog.port = Poort server_connect_dialog.server_url = Server URL server_connect_dialog.http_url = Website URL server_connect_dialog.connect = Verbinden server_connect_dialog.protocol = Protocol server_connect_dialog.nfs_version = NFS versie server_connect_dialog.private_key = Persoonlijke sleutel server_connect_dialog.passphrase = Wachtwoord ftp_connect.passive_mode = Passive modus inschakelen ftp_connect.anonymous_user = Annonieme gebruiker ftp_connect.nb_connection_retries = Hoeveelheid pogingen tot verbinden ftp_connect.retry_delay = Tijd tussen pogingen (in seconden) http_connect.basic_authentication = HTTP Basic Authentication (optioneel) server_connections_dialog.disconnect = Verbreken server_connections_dialog.connection_busy = Bezet server_connections_dialog.connection_idle = Niet actief bonjour.bonjour_services = Bonjour services bonjour.no_service_discovered = Geen service gevonden bonjour.bonjour_disabled = Bonjour uitgeschakeld auth_dialog.title = Autentificatie auth_dialog.desc = Geef a.u.b. naam en wachtwoord auth_dialog.server = Server auth_dialog.store_credentials = Sla naam en wachtwoord op (matige encryptie) auth_dialog.connect_as = Verbinden als auth_dialog.authentication_failed = Autentificatie mislukt sortable_list.move_up = Ga omhoog sortable_list.move_down = Ga omlaag add_bookmark_dialog.add = Toevoegen edit_bookmarks_dialog.new = Nieuw file_viewer.view_error_title = Fout bij het bekijken file_viewer.view_error = Kan bestand niet bekijken file_viewer.file_menu = Bestand file_viewer.close = Sluiten file_viewer.large_file_warning = Dit bestand is misschien te groot voor deze bewerking. file_viewer.open_anyway = Toch openen text_viewer.edit = Bewerken text_viewer.copy = Kopiëren text_viewer.select_all = Selecteer alles text_viewer.find = Zoeken text_viewer.find_next = Zoek volgende text_viewer.find_previous = Zoek vorige text_viewer.binary_file_warning = Dit bestand lijkt een binair bestand te zijn image_viewer.controls_menu = Bediening image_viewer.zoom_in = Zoom in image_viewer.zoom_out = Zoom uit file_editor.edit_error_title = Fout bij het bewerken file_editor.edit_error = Kan bestand niet bewerken file_editor.save = Opslaan file_editor.save_as = Opslaan als... file_editor.save_warning = Voor het afsluiten veranderingen opslaan? file_editor.cannot_write = Kan het bestand niet schrijven text_editor.cut = Knippen text_editor.paste = Plakken shortcuts_dialog.quick_search.start_search = Voer een teken in om het snelzoeken te starten shortcuts_dialog.quick_search.cancel_search = Snelzoeken afbreken shortcuts_dialog.quick_search.remove_last_char = Verwijder laatste teken uit snelzoek-opdracht shortcuts_dialog.quick_search.jump_to_previous = Ga naar vorige snelzoekresultaat shortcuts_dialog.quick_search.jump_to_next = Ga naar volgende snelzoekresultaat shortcuts_dialog.quick_search.mark_jump_next = Markeer/Demarkeer huidige bestand en ga naar volgend zoekresultaat theme_editor.title = Themabewerker theme_editor.folder_tab = Bestandsvenster theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Shell geschiedenis theme_editor.statusbar_tab = Statusbalk theme_editor.free_space = Ruimte vrij theme_editor.free_space.ok = OK theme_editor.free_space.warning = Waarschuwing theme_editor.free_space.critical = Kritiek theme_editor.locationbar_tab = Locatiebalk theme_editor.editor_tab = Bestandsbewerker theme_editor.font = Font theme_editor.active_panel = Actief theme_editor.inactive_panel = Niet actief theme_editor.general = Algemeen theme_editor.could_not_save_theme = Problemen bij opslaan van thema %1 theme_editor.border = Rand theme_editor.background = Achtergrond theme_editor.alternate_background = Afwisselende achtergrond theme_editor.unfocused_background = Achtergrond (zonder focus) theme_editor.quick_search.unmatched_file = Bestanden zijn niet hetzelfde theme_editor.text = Tekst theme_editor.progress = Voortgang theme_editor.normal = Normaal theme_editor.normal_unfocused = Normaal (zonder focus) theme_editor.selected = Gemarkeerd theme_editor.selected_unfocused = Geselecteerd (zonder focus) theme_editor.color = Kleur theme_editor.colors = Kleuren theme_editor.plain_file = Normaal bestand theme_editor.marked_file = Gemarkeerd bestand theme_editor.hidden_file = Verborgen bestand theme_editor.folder = Map theme_editor.archive_file = Ingepakt bestand theme_editor.symbolic_link = Symbolische link theme_editor.header = Kop theme_editor.item = Inhoud command_bar_customize_dialog.available_actions = Beschikbare acties command_bar_customize_dialog.modifier = Veranderen prefs_dialog.title = Voorkeuren prefs_dialog.general_tab = Algemeen prefs_dialog.day = Dag prefs_dialog.month = Maand prefs_dialog.year = Jaar prefs_dialog.language = Taal (herstarten nodig) prefs_dialog.date_time = Formaat van datum en tijd prefs_dialog.time = Tijd prefs_dialog.date = Datum prefs_dialog.date_separator = Scheidingsteken prefs_dialog.time_12_hour = 12-uurs formaat prefs_dialog.time_24_hour = 24-uurs formaat prefs_dialog.show_seconds = Toon seconden prefs_dialog.show_century = Toon eeuw prefs_dialog.check_for_updates_on_startup = Controleer op updates bij het opstarten prefs_dialog.show_splash_screen = Toon opstartscherm prefs_dialog.folders_tab = Mappen prefs_dialog.startup_folders = Opstartmappen prefs_dialog.left_folder = Linker map prefs_dialog.right_folder = Rechter map prefs_dialog.last_folder = Laatst bezochte map prefs_dialog.custom_folder = Zelfgekozen map prefs_dialog.show_hidden_files = Toon verborgen bestanden prefs_dialog.show_ds_store_files = Toon .DS_Store bestanden prefs_dialog.show_system_folders = Toon systeem-mappen prefs_dialog.compact_file_size = Getoonde bestandsgroottes afronden prefs_dialog.follow_symlinks_when_cd = Volg symbolische koppelingen bij wijzigen huidige directory prefs_dialog.appearance_tab = Weergave prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Grootte van iconen prefs_dialog.toolbar_icons = Gereedschappenbalk prefs_dialog.command_bar_icons = Commandobalk prefs_dialog.file_icons = Bestandstypen prefs_dialog.use_system_file_icons = Gebruik systeemeigen bestandsiconen prefs_dialog.use_system_file_icons.always = Altijd prefs_dialog.use_system_file_icons.never = Nooit prefs_dialog.use_system_file_icons.applications = Alleen voor toepassingen prefs_dialog.edit_current_theme = Huidige thema bewerken... prefs_dialog.themes = Thema's prefs_dialog.import_theme = Thema importeren prefs_dialog.import_look_and_feel = Importeer look and feel prefs_dialog.no_look_and_feel = Geen look en feel gevonden. prefs_dialog.error_in_import = Fout bij importeren van thema prefs_dialog.cannot_read_theme = Kan thema niet importeren uit bestand %1 prefs_dialog.export_theme = Exporteer %1 prefs_dialog.import = Importeren prefs_dialog.export = Exporteren prefs_dialog.theme_type = Type: %1 prefs_dialog.delete_theme = Voorgoed verwijderen thema %1 ? prefs_dialog.delete_look_and_feel = Voorgoed verwijderen van look and feel %1 ? prefs_dialog.rename_failed = Fout bij hernoemen thema %1 prefs_dialog.xml_file = XML bestand prefs_dialog.jar_file = JAR bestand prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Instellingen voor uitgaande mail prefs_dialog.mail_name = Uw naam prefs_dialog.mail_address = Uw email adres prefs_dialog.mail_server = SMTP server prefs_dialog.misc_tab = Diversen prefs_dialog.use_brushed_metal = Gebruik 'brushet metal' uiterlijk (herstarten vereist) prefs_dialog.confirm_on_quit = Toon bevestigingsdialoog bij afsluiten prefs_dialog.default_shell = Gebruik voorkeursshell van het systeem prefs_dialog.custom_shell = Gebruik zelfgekozen shell prefs_dialog.shell_encoding = Tekenset shell prefs_dialog.auto_detect_shell_encoding = Auto-detect prefs_dialog.enable_bonjour_discovery = Schakel Bonjour services discovery in prefs_dialog.enable_system_notifications = Systeemnotificaties inschakelen debug_console_dialog.level = Niveau unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mnd duration.years = %1jr theme.custom_theme = Zelfgemaakt thema theme.custom = Aangepast theme.built_in = Ingebouwd theme.add_on = Toegevoegd theme.current = huidig theme_could_not_be_loaded = Er deed zich een fout voor bij het laden van dit thema. setup.title = Welkom bij trolCommander setup.intro = Kiest u a.u.b. de manier waarop trolCommander zich moet gedragen. setup.look_and_feel = Kies uw Look & feel setup.theme = Kies uw thema font_chooser.font_size = Grootte font_chooser.font_bold = Vet font_chooser.font_italic = Schuin color_chooser.red = Rood color_chooser.green = Groen color_chooser.blue = Blauw color_chooser.hue = Tint color_chooser.brightness = Helderheid color_chooser.swatches = Actieve kleuren color_chooser.saturation = Verzadiging color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Recent color_chooser.alpha = Alpha transparantie color_chooser.title = Kies een kleur batch_rename_dialog.mask = Patroon nieuwe naam batch_rename_dialog.search_replace = Zoek en vervang batch_rename_dialog.search_for = Zoeken batch_rename_dialog.replace_with = Vervang door batch_rename_dialog.counter = Teller batch_rename_dialog.start_at = Start bij batch_rename_dialog.step_by = Verhoog met batch_rename_dialog.format = Voorloopnullen batch_rename_dialog.upper_lower_case = Hoofdletters/kleine letters batch_rename_dialog.no_change = onveranderd batch_rename_dialog.lower_case = kleine letters batch_rename_dialog.upper_case = HOOFDLETTERS batch_rename_dialog.first_upper = Eerste letter hoofdletter batch_rename_dialog.word = Eerste Van Ieder Woord batch_rename_dialog.old_name = Oude naam batch_rename_dialog.new_name = Nieuwe naam batch_rename_dialog.block_name = Groep batch_rename_dialog.range = Gebied batch_rename_dialog.proceed_renaming = %1 bestanden van %2 zullen hernoemd worden. Doorgaan? batch_rename_dialog.duplicate_names = Dubbele Naam! parent_folders_quick_list.empty_message = Er is geen bovenliggende map bij huidige locatie recent_locations_quick_list.empty_message = Geen recente locaties recent_executed_files_quick_list.empty_message = Geen recent uitgevoerde bestanden #move_dialog.rename_description = Hernoem bestand naar #theme_editor.shell_font = Shell font #theme_editor.history_font = Font van Geschiedenis #theme_editor.shell_colors = Kleuren van de shell #theme_editor.history_colors = Kleuren van geschiedenis #ToggleHiddenFiles.hide = verborgen bestanden niet tonen #auth_dialog.error_was = Fout was: %1 #table.hide_column = Kolom verbergen #delete.symlink_warning_title = Symbolische koppeling #delete.symlink_warning = Dit bestand lijkt op een symbolische koppeling:\n\n Bestand: %1\n Verwijst naar: %2\n\nAlleen koppeling verwijderen of \nKoppeling volgen en map verwijderen (VOORZICHTIG)? #delete.delete_link_only = Verwijder koppeling #delete.delete_linked_folder = Map verwijderen #Unpack.label = Bestanden uitpakken #Pack.label = Bestanden inpakken ================================================ FILE: src/main/resources/dictionary_no_NO.properties ================================================ ok = OK yes = Ja no = Nei cancel = Avbryt edit = Rediger close = Lukk reset = Nullstill rename = Gi nytt navn apply = Bruk change = Endre save = Lagre dont_save = Ikke lagre replace = Erstatt dont_replace = Ikke erstatt delete = Slett skip = Hopp over skip_all = Hopp over alle retry = Prøv igjen resume = Fortsett overwrite = Overskriv overwrite_if_older = Overskriv hvis eldre duplicate = Lag kopi apply_to_all = Bruk på alle copy = Kopier move = Flytt pack = Komprimer unpack = Pakk ut download = Last ned split = Del opp combine = Sammenflett browse = Utforsk ask = Spør stop = Stopp pause = Pause quick_search = Hurtigsøk file_manager = Filhåndterer create = Opprett choose = Velg customize = Tilpass choose_folder = Velg mappe login = Brukernavn password = Passord user = Bruker encoding = Tegnsett preferred_encodings = Foretrukket tegnsett license = Lisens name = Navn size = Størrelse date = Dato extension = Filtype permissions = Rettigheter owner = Eier group = Gruppe location = Plassering untitled = Uten navn source = Kilde destination = Mål recurse_directories = Behandle valgte mapper rekursivt go_to = Gå til example = Eksempel preview = Forhåndsvisning comment = Kommentar sample_text = Eksempel på tekst nb_files = %1 fil(er) nb_folders = %1 mappe(r) loading = Laster... this_operation_cannot_be_undone = Denne handlingen kan ikke angres. remove = Fjern details = Detaljer warning = Advarsel error = Feilmelding generic_error = En feil oppstod under utførelsen av valgte handling. folder_does_not_exist = Mappen finnes ikke eller er utilgjengelig. this_folder_does_not_exist = Mappen finnes ikke eller er utilgjengelig: %1 this_file_does_not_exist = Filen finnes ikke eller er utilgjengelig: %1 invalid_path = Ugyldig sti: %1 directory_already_exists = Mappen %1 finnes allerede. file_exists_in_destination = Filen finnes allerede i målmappen. source_parent_of_destination = Forsøker å flytte mappen til en av dens undermapper cannot_read_file = Kan ikke lese filen %1 cannot_write_file = Kan ikke skrive til filen %1 cannot_create_folder = Kan ikke opprette mappen %1 cannot_read_folder = Kan ikke lese innholdet i mappen %1 cannot_delete_file = Kan ikke slette filen %1 cannot_delete_folder = Kan ikke slette mappen %1 error_while_transferring = Feil oppstod ved flyttingen av filen %1 same_source_destination = Kilde- og målmappen er den samme file_already_exists = %1 finnes allerede. Vil du erstatte den? write_error = Feil ved skriving read_error = Feil ved lesing integrity_check_error = Integritetssjekk feilet: kilde og målet stemmer ikke med hverandre startup_error = En feil forhindret trolCommander fra å starte. permissions.read = Lese permissions.write = Skrive permissions.executable = Kjøre permissions.group = Gruppe permissions.other = Andre permissions.octal_notation = Oktal notasjon action_categories.all = Alle action_categories.navigation = Navigasjon action_categories.selection = Markering action_categories.view = Vis action_categories.file_operations = Filhandlinger action_categories.windows = Vinduer Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Nytt bokmerke AddBookmark.tooltip = Legg gjeldende mappe til listen over bokmerker BatchRename.label = Gi nytt navn til filer EditBookmarks.label = Rediger bokmerker ExploreBookmarks.label = Vis bokmerker EditCredentials.label = Endre fullmakt ChangeLocation.label = Endre gjeldene sti ChangeDate.label = Endre dato ChangeDate.tooltip = Endre dato for gjeldene fil(er) ChangePermissions.label = Endre rettigheter ChangePermissions.tooltip = Endre rettigheter for markert(e) fil(er) CheckForUpdates.label = Søk etter oppdateringer CompareFolders.label = Sammenlign mapper ConnectToServer.label = Koble til tjener ConnectToServer.tooltip = Koble til fjerntjener View.label = Vis InternalView.label = Vis (intern) View.tooltip = Vis markert fil InternalEdit.label = Rediger (intern) Edit.tooltip = Rediger markert fil Copy.tooltip = Kopier markert fil LocalCopy.label = Lokal kopi LocalCopy.tooltip = Kopier markert fil til gjeldene mappe Move.tooltip = Flytt markert fil Rename.tooltip = Gi nytt navn til markert fil Mkdir.label = Ny mappe Mkdir.tooltip = Opprett en ny mappe i gjeldende mappe Mkfile.label = Ny fil Mkfile.tooltip = Opprett en ny fil i gjeldene mappe Delete.tooltip = Slett markerte filer via papirkurven hvis mulig PermanentDelete.label = Slett permanent PermanentDelete.tooltip = Slett markerte filer uten å bruke papirkurven Refresh.label = Oppdater Refresh.tooltip = Oppdater gjeldene mappe CloseWindow.label = Lukk vindu CloseWindow.tooltip = Lukk dette vinduet CopyFileNames.label = Kopier navn(ene) CopyFilePaths.label = Kopier sti(ene) CopyFilesToClipboard.label = Kopier fil(er) PasteClipboardFiles.label = Lim inn fil(er) Email.label = Send via epost Email.tooltip = Send markerte filer som epost vedlegg GoBack.label = Tilbake GoBack.tooltip = Gå tilbake til forrige mappe GoForward.label = Frem GoForward.tooltip = Gå til neste mappe GoToHome.label = Hjemmappe GoToParent.label = Gå opp GoToParent.tooltip = Gå opp en mappe GoToParentInOtherPanel.label = Opp en mappe, andre panel GoToParentInBothPanels.label = Opp en mappe, begge paneler GoToRoot.label = Gå til rot SortByName.label = Sorter etter navn SortByDate.label = Sorter etter dato SortBySize.label = Sorter etter størrelse SortByExtension.label = Sorter etter filtype SortByPermissions.label = Sorter etter rettigheter SortByOwner.label = Sorter etter eier SortByGroup.label = Sorter etter gruppe MarkGroup.label = Marker filer MarkGroup.tooltip = Marker en gruppe filer UnmarkGroup.label = Fjern markering av filer UnmarkGroup.tooltip = Fjern markeringen av en gruppe filer MarkAll.label = Marker alt UnmarkAll.label = Fjern markering av alt MarkSelectedFile.label = Marker/fjern markering MarkSelectedFile.tooltip = Marker/fjern markering av valgt fil MarkNextBlock.label = Marker en blokk nedover MarkPreviousBlock.label = Marker en blokk oppover MarkNextRow.label = Marker en rad nedover MarkPreviousRow.label = Marker en rad oppover MarkNextPage.label = Marker en side nedover MarkPreviousPage.label = Marker en side oppover MarkToFirstRow.label = Marker filer til begynnelsen MarkToLastRow.label = Marker filer til slutten MarkExtension.label = Marker filendelse InvertSelection.label = Omvend markering SwapFolders.label = Bytt mapper SwapFolders.tooltip = Bytt om høyre og venstre panel SetSameFolder.label = Vis samme mappe SetSameFolder.tooltip = Vis samme mappe i høyre og venstre panel NewWindow.label = Nytt vindu NewWindow.tooltip = Åpne et nytt vindu Open.label = Åpne Open.tooltip = Åpne mappe / Åpne arkiv / Kjør OpenNatively.label = Åpne normalt OpenNatively.tooltip = Åpne markert fil med systemets filassosiasjoner OpenInOtherPanel.label = Åpne i det andre panelet OpenInBothPanels.label = Åpne i begge panelene RevealInDesktop.label = Vis i %1 RunCommand.label = Kjør kommando RunCommand.tooltip = Kjør kommando i gjeldene mappe Pack.tooltip = Pakk markerte filer til et arkiv Unpack.tooltip = Pakk ut markerte arkivfiler ShowFileProperties.label = Egenskaper ShowFileProperties.tooltip = Vis egenskaper til markerte filer ShowPreferences.label = Innstillinger ShowPreferences.tooltip = Konfigurer trolCommander ShowServerConnections.label = Vis åpne tilkoblinger Quit.label = Avslutt ReverseSortOrder.label = Snu sortering ToggleAutoSize.label = Automatisk kolonnebredde Stop.label = Avbryt mappebytte ToggleColumn.show = Vis %1 kolonne ToggleColumn.hide = Skjul %1 kolonne ToggleCommandBar.show = Vis kommandolinjen ToggleCommandBar.hide = Skjul kommandolinjen ToggleToolBar.show = Vis verktøylinje ToggleToolBar.hide = Skjul verktøylinje CustomizeCommandBar.label = Tilpass kommandolinjen ToggleStatusBar.show = Vis statuslinjen ToggleStatusBar.hide = Skjul statuslinjen ToggleShowFoldersFirst.label = Vis mapper først ToggleTree.label = Trevisning PopupLeftDriveButton.label = Endre venstre mappe PopupRightDriveButton.label = Endre høyre mappe RecallPreviousWindow.label = Gjennkall forrige vindu RecallNextWindow.label = Gjennkall neste vindu RecallWindow.label = Gjennkall vindu #%1 BringAllToFront.label = Legg alle øverst SwitchActiveTable.label = Bytt høyre og venstre panel SelectNextBlock.label = Hopp en blokk ned SelectPreviousBlock.label = Hopp en blokk opp SelectNextPage.label = Hopp ned en side SelectPreviousPage.label = Hopp opp en side SelectNextRow.label = Hopp ned en rad SelectPreviousRow.label = Hopp opp en rad SelectFirstRow.label = Merk første fil (her,) i gjeldende mappe SelectLastRow.label = Merk siste fil (her,) i gjeldende mappe SplitEqually.label = Del likt SplitVertically.label = Del vertikalt SplitHorizontally.label = Del horisontalt ShowKeyboardShortcuts.label = Tastatursnarveier GoToWebsite.label = Gå til nettside GoToForums.label = Gå til forum ReportBug.label = Rapporter en feil Donate.label = Doner ShowAbout.label = Om trolCommander OpenTrash.label = Åpne papirkurv EmptyTrash.label = Tøm papirkurv CalculateChecksum.label = Regn ut kontrollsum MaximizeWindow.label = Maksimer MaximizeWindow.label.mac_os_x = Zoom MinimizeWindow.label = Minimer MinimizeWindow.label.mac_os_x = Minimer til Dock GoToDocumentation.label = Online dokumentasjon ShowParentFoldersQL.label = Overordnede mapper ShowRecentLocationsQL.label = Nylig brukte plasser ShowRecentExecutedFilesQL.label = Nylig åpnede filer SplitFile.tooltip = Del en fil opp i flere deler CombineFiles.tooltip = Kombiner oppdelte filer for å gjenskape orginalfilen ShowDebugConsole.label = Feilsøkingskonsoll FocusPrevious.label = Fokuser på forrige komponent FocusNext.label = Fokuser på neste komponent file_menu = Fil file_menu.open_with = Åpne med mark_menu = Marker view_menu = Vis view_menu.show_hide_columns = Vis/skjul kolonner go_menu = Gå bookmarks_menu = Bokmerker bookmarks_menu.no_bookmark = Ingen bokmerker drive_popup.network_shares = Nettverksmapper quick_lists_menu = Hurtiglister window_menu = Vindu help_menu = Hjelp status_bar.selected_files = %1 av %2 markerte status_bar.connecting_to_folder = Kobler til mappe. Trykk ESC for å avbryte. status_bar.volume_free = Ledig: %1 status_bar.volume_capacity = Kapasitet: %1 shortcuts_panel.title = Snarveier shortcuts_panel.restore_defaults = Gjenopprett innstillinger shortcuts_panel.show = Vis shortcuts_panel.default_message = Trykk Enter eller dobbeltklikk på snarveien du vil redigere shortcuts_table.action_description = Handlingsbeskrivelse shortcuts_table.shortcut = Snarvei shortcuts_table.alternate_shortcut = Alternativ snarvei shortcuts_table.type_in_a_shortcut = Skriv inn snarvei command_bar_dialog.help = Dra i knappene for å tilpasse kommandolinjen table.folder_access_error_title = Feil ved mappetilgang table.folder_access_error = Kan ikke lese mappeinnhold table.download_or_browse = Vil du utforske eller laste ned filen? version_dialog.no_new_version_title = Ingen ny versjon version_dialog.no_new_version = Gratulerer, du har allerede den nyeste versjonen. version_dialog.new_version_title = Ny versjon tilgjengelig version_dialog.new_version = En ny versjon av trolCommander er tilgjengelig. version_dialog.new_version_url = En ny versjon av trolCommander er tilgjengelig på %1. version_dialog.not_available_title = Tjeneren er utilgjengelig version_dialog.not_available = Kan ikke hente informasjon om nyeste versjon fra tjeneren. version_dialog.install_and_restart = Installer og restart version_dialog.preparing_for_update = Forbereder oppdatering... quit_dialog.title = Avslutt trolCommander quit_dialog.desc = Du har %1 vindu(er) åpne. Er du sikker på at du vil avslutte? quit_dialog.show_next_time = Vis neste gang destination_dialog.file_exists_action = Standardhandling når fil allerede finnes destination_dialog.verify_integrity = Bekreft dataintegritet destination_dialog.skip_errors = Ignorer feil file_collision_dialog.title = Filen finnes allerede rename_dialog.new_name = Nytt navn copy_dialog.destination = Kopierer valgt(e) fil(er) til copy_dialog.error_title = Feil ved kopiering copy_dialog.copying = Kopierer filer copy_dialog.copying_file = Kopierer %1 pack_dialog.packing = Pakker filer pack_dialog.packing_file = Komprimerer %1 pack_dialog.error_title = Feil ved pakking pack_dialog_description = Legg markerte filer til i pack_dialog.archive_format = Arkivformat unpack_dialog.destination = Pakk ut markert(e) fil(er) i unpack_dialog.error_title = Feil ved utpakking unpack_dialog.unpacking = Pakker ut filer unpack_dialog.unpacking_file = Pakker ut %1 optimizing_archive = Optimaliserer arkiv %1 error_while_optimizing_archive = Feil ved optimalisering av arkiv %1 move_dialog.move_description = Flytt til move_dialog.error_title = Feil ved flytting move_dialog.moving = Flytter filer move_dialog.moving_file = Flytter %1 download_dialog.description = Last ned fil til download_dialog.error_title = Feil ved nedlasting download_dialog.downloading = Laster ned download_dialog.downloading_file = Laster ned %1 mkfile_dialog.allocate_space = Tildel plass delete_dialog.permanently_delete.confirmation = Slett markert(e) fil(er) permanent? delete_dialog.move_to_trash.confirmation = Slett markert(e) fil(er)? delete_dialog.move_to_trash.confirmation_details = Filene vil bli flyttet til papirkurven. delete_dialog.move_to_trash.option = Flytt til papirkurven delete_dialog.move_to_trash.failed = En eller flere filer kunne ikke flyttes til papirkurven. delete_dialog.deleting = Sletter delete_dialog.error_title = Feil ved sletting delete.deleting_file = Sletter %1 email_dialog.prefs_not_set_title = Epost er ikke konfigurert email_dialog.prefs_not_set = Du må stille inn epost-konfigurasjonen din først. email_dialog.from = Fra email_dialog.to = Til email_dialog.subject = Emne email_dialog.send = Send email_dialog.error_title = Feil ved sending av filene email_dialog.read_error = Kunne ikke lese filene i undermappene. email_dialog.sending = Sender filer email.sending_file = Sender %1 email.connecting_to_server = Kobler til %1 email.server_unavailable = Kunne ikke kontakte epost-tjeneren %1. Sjekk epost-instillingene dine eller prøv igjen senere. email.connection_closed = Forbindelsen ble stengt av tjeneren, eposten ble ikke sendt. email.goodbye_failed = Feil ved lukking av forbindelse, eposten ble kanskje ikke sendt. email.send_file_error = Kunne ikke sende filen %1, eposten ble ikke sendt. split_file_dialog.error_title = Feil ved oppdeling av fil split_file_dialog.file_to_split = Fil som skal deles split_file_dialog.target_directory = Målmappe split_file_dialog.part_size = Størrelse per del split_file_dialog.parts = Antall deler split_file_dialog.generate_CRC = Generer CRC-fil split_file_dialog.max_parts = Maksimum tillatte deler er %1 split_file_dialog.auto = Automatisk split_file_dialog.insert_new_media = Sett inn et nytt medium combine_files_dialog.error_title = Feil ved kombinering av fil combine_files_job.no_crc_file = Sammenslåelse fullført. Ingen CRC-fil. combine_files_job.crc_read_error = Feil ved lesing av CRC-fil. combine_files_job.crc_check_failed = CRC feil: forventet %2, fant %1 combine_files_job.crc_ok = Sammenslåelse lyktes. CRC kontrollsum ok. file_selection_dialog.mark = Marker file_selection_dialog.unmark = Fjern markering file_selection_dialog.mark_description = Marker filer hvis navn file_selection_dialog.unmark_description = Fjern markering av filer hvis navn file_selection_dialog.case_sensitive = Forskjell på små og store bokstaver file_selection_dialog.include_folders = Inkluder mapper progress_dialog.starting = Starter overføring... progress_dialog.transferred = Overført %1 med %2 progress_dialog.elapsed_time = Tid brukt progress_dialog.advanced = Avansert progress_dialog.current_speed = Nåværende hastighet progress_dialog.limit_speed = Begrens hastighet progress_dialog.close_when_finished = Lukk vinduet når ferdig progress_dialog.processing_files = Bearbeider filer progress_dialog.processing_file = Bearbeider %1 progress_dialog.verifying_file = Verifiserer %1 progress_dialog.job_finished = Jobb ferdig progress_dialog.job_error = Feil ved utførelse properties_dialog.file_properties = %1 Egenskaper properties_dialog.contents = Innhold properties_dialog.calculating = Beregner... calculate_checksum_dialog.checksum_algorithm = Algoritme for kontrollsum calculate_checksum_dialog.temporary_file = Midlertidig fil change_date_dialog.now = Nå change_date_dialog.specific_date = Angi dato run_dialog.run_command_description = Kjør i gjeldene mappe run_dialog.run_in_home_description = Kjør i hjemmappe run_dialog.command_output = Kommandoutskrift run_dialog.run = Kjør run_dialog.clear_history = Slett historikk server_connect_dialog.server_type = Tilkoblingstype server_connect_dialog.server = Tjener server_connect_dialog.share = Del server_connect_dialog.domain = Domene server_connect_dialog.username = Brukernavn server_connect_dialog.initial_dir = Startmappe server_connect_dialog.port = Port server_connect_dialog.server_url = Tjenerens URL server_connect_dialog.http_url = Nettsidens URL server_connect_dialog.connect = Koble til server_connect_dialog.protocol = Protokoll server_connect_dialog.nfs_version = NFS-versjon server_connect_dialog.private_key = Privat nøkkel server_connect_dialog.passphrase = Passord ftp_connect.passive_mode = Aktiver passiv modus ftp_connect.anonymous_user = Anonym bruker ftp_connect.nb_connection_retries = Antall forsøk på å tilkoble ftp_connect.retry_delay = Mellomrom mellom tilkoblinger (i sekunder) http_connect.basic_authentication = Grunnleggende HTTP autorisering (frivillig) server_connections_dialog.disconnect = Koble fra server_connections_dialog.connection_busy = Opptatt server_connections_dialog.connection_idle = Inaktiv bonjour.bonjour_services = Bonjour-tjenester bonjour.no_service_discovered = Fant ingen tjeneste bonjour.bonjour_disabled = Bonjour deaktivert auth_dialog.title = Godkjenning auth_dialog.desc = Vennligst angi brukernavn og passord auth_dialog.server = Tjener auth_dialog.store_credentials = Lagre brukernavn og passord (svak kryptering) auth_dialog.connect_as = Koble til som auth_dialog.authentication_failed = Godkjenning feilet sortable_list.move_up = Flytt oppover sortable_list.move_down = Flytt nedover add_bookmark_dialog.add = Legg til edit_bookmarks_dialog.new = Ny file_viewer.view_error_title = Feil ved filvisning file_viewer.view_error = Filen kunne ikke vises. file_viewer.file_menu = Fil file_viewer.close = Lukk file_viewer.large_file_warning = Filen kan være for stor for denne handlingen. file_viewer.open_anyway = Åpne allikevel text_viewer.edit = Rediger text_viewer.copy = Kopier text_viewer.select_all = Velg alt text_viewer.find = Finn text_viewer.find_next = Finn neste text_viewer.find_previous = Finn forrige text_viewer.view = Vis text_viewer.line_wrap = Tekstbrytning text_viewer.line_numbers = Linjenummere text_viewer.binary_file_warning = Dette ser ut til å være en binærfil image_viewer.controls_menu = Funksjoner image_viewer.zoom_in = Forstørr image_viewer.zoom_out = Forminsk file_editor.edit_error_title = Feil under redigering file_editor.edit_error = Kan ikke redigere filen. file_editor.save = Lagre file_editor.save_as = Lagre som... file_editor.save_warning = Lagre endringer til filen før lukking? file_editor.cannot_write = Kan ikke skrive til filen. text_editor.cut = Klipp ut text_editor.paste = Lim inn shortcuts_dialog.quick_search.start_search = Skiv inn et tegn for å starte hurtigsøk shortcuts_dialog.quick_search.cancel_search = Avbryt hurtigsøk shortcuts_dialog.quick_search.remove_last_char = Fjern det siste tegnet fra strengen i hurtigssøket shortcuts_dialog.quick_search.jump_to_previous = Hopp to resultatene fra forrige hurtigsøk shortcuts_dialog.quick_search.jump_to_next = Hopp to resultatene fra neste hurtigsøk shortcuts_dialog.quick_search.mark_jump_next = Merk/fjern markering av gjeldende fil og hopp til neste søkeresultat theme_editor.title = Temabehandler theme_editor.folder_tab = Mappepanel theme_editor.shell_tab = Skall theme_editor.shell_history_tab = Skall-historikk theme_editor.statusbar_tab = Statuslinje theme_editor.free_space = Ledig plass theme_editor.free_space.ok = OK theme_editor.free_space.warning = Advarsel theme_editor.free_space.critical = Kritisk theme_editor.locationbar_tab = Adresselinje theme_editor.editor_tab = Filbehandler theme_editor.font = Skrifttype theme_editor.active_panel = Aktivt theme_editor.inactive_panel = Inaktivt theme_editor.general = Generelt theme_editor.could_not_save_theme = Kunne ikke lagre temaet %1 theme_editor.border = Kanter theme_editor.background = Bakgrunn theme_editor.alternate_background = Sekundær bakgrunn theme_editor.unfocused_background = Bakgrunn (ute av fokus) theme_editor.quick_search.unmatched_file = Filer som ikke matcher theme_editor.text = Tekst theme_editor.progress = Fremdrift theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (ute av fokus) theme_editor.selected = Markert theme_editor.selected_unfocused = Markert (ute av fokus) theme_editor.color = Farge theme_editor.colors = Farger theme_editor.plain_file = Vanlig fil theme_editor.marked_file = Markert fil theme_editor.hidden_file = Skjult fil theme_editor.folder = Mappe theme_editor.archive_file = Arkiv theme_editor.symbolic_link = Symbolsk lenke theme_editor.header = Overskrift theme_editor.item = Element command_bar_customize_dialog.available_actions = Tilgjengelige handlinger command_bar_customize_dialog.modifier = Endrer prefs_dialog.title = Innstillinger prefs_dialog.general_tab = Generelt prefs_dialog.day = Dag prefs_dialog.month = Måned prefs_dialog.year = År prefs_dialog.language = Språk (krever omstart) prefs_dialog.date_time = Tids- og datoformat prefs_dialog.time = Tid prefs_dialog.date = Dato prefs_dialog.date_separator = Separator prefs_dialog.time_12_hour = 12-timersklokke prefs_dialog.time_24_hour = 24-timersklokke prefs_dialog.show_seconds = Vis sekunder prefs_dialog.show_century = Vis århundre prefs_dialog.check_for_updates_on_startup = Søk etter oppdateringer ved oppstart prefs_dialog.show_splash_screen = Vis oppstartsbilde prefs_dialog.folders_tab = Mapper prefs_dialog.startup_folders = Oppstartsmapper prefs_dialog.left_folder = Venstre mappe prefs_dialog.right_folder = Høyre mappe prefs_dialog.last_folder = Sist besøkte mappe prefs_dialog.custom_folder = Velg mappe prefs_dialog.show_hidden_files = Vis skjulte filer prefs_dialog.show_ds_store_files = Vis .DS_Store filer prefs_dialog.show_system_folders = Vis systemmapper prefs_dialog.compact_file_size = Rund av filstørrelser prefs_dialog.follow_symlinks_when_cd = Følg symbolske lenker ved endring av gjeldende mappe prefs_dialog.appearance_tab = Utseende prefs_dialog.look_and_feel = Stil prefs_dialog.icons_size = Ikonstørrelse prefs_dialog.toolbar_icons = Verktøylinje prefs_dialog.command_bar_icons = Kommandolinje prefs_dialog.file_icons = Filtyper prefs_dialog.use_system_file_icons = Bruk systemets filikoner prefs_dialog.use_system_file_icons.always = Alltid prefs_dialog.use_system_file_icons.never = Aldri prefs_dialog.use_system_file_icons.applications = Kun for programmer prefs_dialog.edit_current_theme = Rediger gjeldende tema... prefs_dialog.themes = Temaer prefs_dialog.import_theme = Importer tema prefs_dialog.import_look_and_feel = Importer stil prefs_dialog.no_look_and_feel = Fant ingen stil prefs_dialog.error_in_import = Feil ved importering av temaet %1. prefs_dialog.cannot_read_theme = Kan ikke laste inn temaet fre filen %1 prefs_dialog.export_theme = Eksporter %1 prefs_dialog.import = Importer prefs_dialog.export = Eksporterer prefs_dialog.theme_type = Type: %1 prefs_dialog.delete_theme = Permanent sletting av temaet %1 ? prefs_dialog.delete_look_and_feel = Permanent sletting av stilen %1 ? prefs_dialog.rename_failed = Kunne ikke gi nytt navn til temaet %1 prefs_dialog.xml_file = XML-fil prefs_dialog.jar_file = JAR-fil prefs_dialog.mail_tab = Epost prefs_dialog.mail_settings = Innstillinger for utgående epost prefs_dialog.mail_name = Ditt navn prefs_dialog.mail_address = Din epost-adresse prefs_dialog.mail_server = SMTP-tjener prefs_dialog.misc_tab = Diverse prefs_dialog.use_brushed_metal = Bruk stilen 'børstet metall' (krever omstart) prefs_dialog.confirm_on_quit = Spør etter bekreftelse før lukking prefs_dialog.default_shell = Bruk systemets standardskall prefs_dialog.custom_shell = Bruk annet skall prefs_dialog.shell_encoding = Skallets tegnsett prefs_dialog.auto_detect_shell_encoding = Velg automatisk prefs_dialog.enable_bonjour_discovery = Aktiver oppdagelse av Bonjour-tjenester prefs_dialog.enable_system_notifications = Aktiver systemmeldinger debug_console_dialog.level = Nivå unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/sek duration.seconds = %1sek duration.minutes = %1min duration.hours = %1t duration.days = %1dag(er) duration.months = %1mnd duration.years = %1år theme.custom_theme = Tilpasset tema theme.custom = Tilpasset theme.built_in = Innebygd theme.add_on = Tillegg theme.current = aktivt theme_could_not_be_loaded = En feil oppstod ved lastingen av dette temaet. setup.title = Velkommen til trolCommander setup.intro = Vennligst velg hvordan du ønsker at trolCommander skal oppføre seg. setup.look_and_feel = Velg stil setup.theme = Velg tema font_chooser.font_size = Størrelse font_chooser.font_bold = Fet font_chooser.font_italic = Kursiv color_chooser.red = Rød color_chooser.green = Grønn color_chooser.blue = Blå color_chooser.hue = Fargetone color_chooser.brightness = Lysstyrke color_chooser.swatches = Fargekart color_chooser.saturation = Metning color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Nylig color_chooser.alpha = Gjennomsiktighet/Alfa-verdi color_chooser.title = Velg en farge batch_rename_dialog.mask = Navngi mønster batch_rename_dialog.search_replace = Søk og erstatt batch_rename_dialog.search_for = Søk etter batch_rename_dialog.replace_with = Erstatt med batch_rename_dialog.counter = Teller batch_rename_dialog.start_at = Begynn ved batch_rename_dialog.step_by = Øk med batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Store/små bokstaver batch_rename_dialog.no_change = Uendret batch_rename_dialog.lower_case = små bokstaver batch_rename_dialog.upper_case = STORE BOKSTAVER batch_rename_dialog.first_upper = Stor bokstav først batch_rename_dialog.word = På Begynnelsen Av Hvert Ord batch_rename_dialog.old_name = Gammelt navn batch_rename_dialog.new_name = Nytt navn batch_rename_dialog.block_name = Bevar batch_rename_dialog.range = Område batch_rename_dialog.proceed_renaming = %1 filer av %2 vil få nytt navn. Ønsker du å fortsette? batch_rename_dialog.duplicate_names = Dupliser navn! batch_rename_dialog.names_conflict = Navnkonflikt! Samme verdi i gammelt og nytt navn. parent_folders_quick_list.empty_message = Gjeldende mappe har ingen overmappe recent_locations_quick_list.empty_message = Ingen nylig besøkte steder recent_executed_files_quick_list.empty_message = Ingen nylig åpnede filer #server_connect_dialog.auth_error = Ugyldig brukernavn eller passord. #move_dialog.cannot_move_to_itself = Kan ikke flytte fil til undermappe. #pack.error_on_file = Feil ved komprimering av %1 #pack_dialog.cannot_write = Kunne ikke opprette fil i målmappe. #unpack.unable_to_open_zip = Kunne ikke åpne zip-fil %1. #image_viewer.previous_image = Forrige bilde #image_viewer.next_image = Neste bilde #mkdir_dialog.title = Neste mappe #mkdir_dialog.error_title = Feil ved opprettelse #edit_bookmarks_dialog.remove = Fjern #mkdir_dialog.description = Opprett mappe #mkfile_dialog.description = Opprett ny tom fil #done = Utført #progress_dialog.hide = Skjul #move_dialog.rename_description = Gi nytt navn til #theme_editor.shell_font = Skrifttype for skall #theme_editor.history_font = Skrifttype for historie #theme_editor.shell_colors = Farger for skall #theme_editor.history_colors = Farger for historikk #ToggleHiddenFiles.hide = Ikke vis skulte filer #auth_dialog.error_was = Feilen var: %1 #table.hide_column = Skjul kolonne #delete.symlink_warning_title = Symbolsk lenke funnet #delete.symlink_warning = Denne filen ser ut til å være en symbolsk lenke:\n\n Fil: %1\n Lenker til: %2\n\nDelete symlink only or\nFølg symbolsk lenke og slett mappen(FORSIKTIG) ? #delete.delete_link_only = Slett lenke #delete.delete_linked_folder = Slett mappe #Unpack.label = Pakk ut filer #Pack.label = Pakk filer ================================================ FILE: src/main/resources/dictionary_pl_PL.properties ================================================ ok = OK yes = TAK no = Nie cancel = Anuluj edit = Edycja close = Zamknij reset = Resetuj rename = Zmień nazwę apply = Zastosuj change = Zmień save = Zapisz dont_save = Nie zapisuj replace = Zastąp dont_replace = Nie zastępuj delete = Usuń skip = Pomiń skip_all = Pomiń wszystkie retry = Ponów resume = Wznów overwrite = Nadpisz overwrite_if_older = Nadpisz jeśli starszy duplicate = Powiel apply_to_all = Zastosuj do wszystkich copy = Kopiuj move = Przenieś pack = Spakuj unpack = Rozpakuj download = Download split = Podziel combine = Scal browse = Przeglądaj ask = Zapytaj stop = Zatrzymaj pause = Pauza quick_search = Szybkie wyszukiwanie file_manager = Menedżer plików create = Utwórz creating_file = Tworzenie %1 choose = Wybierz customize = Dostosuj choose_folder = Wybierz katalog login = Login password = Hasło user = Właciciel encoding = Kodowanie preferred_encodings = Preferowane kodowanie license = Licencja name = Nazwa size = Rozmiar date = Data extension = Rozszerzenie permissions = Uprawnienia owner = Właściciel group = Grupa location = Lokalizacja untitled = Bez nazwy source = Źródło destination = Miejsce docelowe recurse_directories = Przetwórz zaznaczone katalogi rekurencyjnie go_to = Przejdź do example = Przykład preview = Podgląd comment = Komentarz sample_text = Przykładowy tekst nb_files = %1 plik(i) nb_folders = %1 katalog(i) loading = Ładowanie... this_operation_cannot_be_undone = Operacja nie może być cofnięta. remove = Usuń details = Szczegóły warning = Uwaga error = Błąd generic_error = Wystąpił błąd podczas wykonywania operacji. folder_does_not_exist = Ten katalog nie istnieje lub jest nieosiągalny. this_folder_does_not_exist = Ten katalog nie istnieje lub jest nieosiągalny: %1 this_file_does_not_exist = Plik nie istnieje lub jest niedostępny: %1 invalid_path = Błędna ścieżka: %1 directory_already_exists = Katalog %1 już istnieje. file_exists_in_destination = Plik już istnieje w miejscu docelowym source_parent_of_destination = Próba przeniesienia katalogu do zawartego w nim podkatalogu cannot_read_file = Nie mogę czytać pliku %1 cannot_write_file = Nie mogę zapisać pliku %1 cannot_create_folder = Nie mogę utworzyć katalogu %1 cannot_read_folder = Nie mogę odczytać zawartości katalogu %1 cannot_delete_file = Nie mogę skasować pliku %1 cannot_delete_folder = Nie mogę skasować katalogu %1 error_while_transferring = Błąd przenoszenia pliku %1 same_source_destination = Taki sam katalog źródłowy i docelowy file_already_exists = %1 już istnieje, chcesz zastąpić ? write_error = Błąd zapisu read_error = Błąd odczytu integrity_check_error = Błąd integralności danych: plik źródlowy i docelowy nie zgadzają się startup_error = Błąd uniemożliwił uruchomienie trolCommander'a. permissions.read = Odczytu permissions.write = Zapis permissions.executable = Wykonywanie permissions.group = Grupa permissions.other = Inni permissions.octal_notation = Ósemkowo action_categories.all = Wszystkie action_categories.navigation = Nawigacja action_categories.selection = Wybór action_categories.view = Podgląd action_categories.file_operations = Operacje na plikach action_categories.windows = Okna Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Dodaj zakładkę AddBookmark.tooltip = Dodaj do zakładek aktualny katalog BatchRename.label = Zmiana nazw plików EditBookmarks.label = Edytuj Zakładki ExploreBookmarks.label = Przeglądaj zakładki EditCredentials.label = Zmień uprawnienia ChangeLocation.label = Zmień aktualne położenie ChangeDate.label = Zmień datę ChangeDate.tooltip = Zmień datę zaznaczonym plikom ChangePermissions.label = Zmień uprawnienia ChangePermissions.tooltip = Zmień uprawnienia zaznaczonym plikom CheckForUpdates.label = Sprawdź aktualizacje CompareFolders.label = Porównaj katalogi ConnectToServer.label = Połączenie z serwerem ConnectToServer.tooltip = Podłącz do zdalnego serwera View.label = Podgląd InternalView.label = Podgląd (wewnętrzny) View.tooltip = Zobacz wybrany plik InternalEdit.label = Edycja (wewnętrzny) Edit.tooltip = Edytuj wybrany plik Copy.tooltip = Kopiuj zaznaczone pliki LocalCopy.label = Kopiuj lokalnie LocalCopy.tooltip = Kopiuj zaznaczone pliki do aktywnego katalogu Move.tooltip = Przenieś zaznaczone pliki Rename.tooltip = Przemianuj zaznaczone pliki Mkdir.label = Utwórz katalog Mkdir.tooltip = Utwórz katalog w aktualnym katalogu Mkfile.label = Utwórz plik Mkfile.tooltip = Utwórz plik w aktualnym katalogu Delete.tooltip = Usuwa zaznaczone pliki (przenosi do kosza, jeżeli możliwe) PermanentDelete.label = Usuń trwale PermanentDelete.tooltip = Trwale usuwa zaznaczone pliki (nie używa systemowego kosza) Refresh.label = Odśwież Refresh.tooltip = Odśwież aktualny katalog CloseWindow.label = Zamknij okno CloseWindow.tooltip = Zamknij aktualne okno CopyFileNames.label = Kopiuj nazwę(y) CopyFilePaths.label = Kopiuj ścieżkę(ki) CopyFilesToClipboard.label = Kopiuj plik(i) PasteClipboardFiles.label = Wklej plik(i) Email.label = Wyślij pliki pocztą Email.tooltip = Wyślij zaznaczone pliki jako załącznik GoBack.label = Wstecz GoBack.tooltip = Idź do poprzedniego katalogu GoForward.label = Dalej GoForward.tooltip = Idź do następnego katalogu GoToHome.label = Idź do katalogu domowego GoToParent.label = Idź do katalogu nadrzędnego GoToParent.tooltip = Idź do katalogu nadrzędnego GoToParentInOtherPanel.label = Idź do katalogu nadrzędnego w drugim panelu GoToParentInBothPanels.label = Idź do katalogu nadrzędnego w obu panelach GoToRoot.label = Idź do katalogu głównego (Root) SortByName.label = Sortuj po nazwie SortByDate.label = Sortuj po dacie SortBySize.label = Sortuj po rozmiarze SortByExtension.label = Sortuj po rozszerzeniu SortByPermissions.label = Sortuj po uprawnieniach SortByOwner.label = Sortuj po właścicielu SortByGroup.label = Sortuj po grupie MarkGroup.label = Zaznacz pliki MarkGroup.tooltip = Zaznacz grupę plików UnmarkGroup.label = Odznacz pliki UnmarkGroup.tooltip = Odznacz grupę plików MarkAll.label = Zaznacz wszystko UnmarkAll.label = Odznacz wszystko MarkSelectedFile.label = Zaznacz/Odznacz MarkSelectedFile.tooltip = Zaznacz/Odznacz wybrany plik MarkNextBlock.label = Zaznacz blok w dół MarkPreviousBlock.label = Zaznacz blok w górę MarkNextRow.label = Zaznacz wiersz poniżej MarkPreviousRow.label = Zaznacz wiersz powyżej MarkNextPage.label = Zaznacz strone niżej (page down) MarkPreviousPage.label = Zaznacz strone wyżej (page up) MarkToFirstRow.label = Zaznacz pliki do początku MarkToLastRow.label = Zaznacz pliki do końca MarkExtension.label = Zaznacz wg rozszerzenia InvertSelection.label = Odwróć zaznaczenie SwapFolders.label = Zamień katalogi SwapFolders.tooltip = Zmień katalogi z lewej i prawej strony SetSameFolder.label = Źródłowy=Docelowy SetSameFolder.tooltip = Ustawe te same katalogi w obu oknach NewWindow.label = Nowe okno NewWindow.tooltip = Otwórz nowe okno Open.label = Otwórz Open.tooltip = Otwórz katalog / Otwórz archiwum / Wykonaj OpenNatively.label = Otwórz skojarzoną aplikacją OpenNatively.tooltip = Uruchom wybrany plik za pomocą skojarzonego programu OpenInOtherPanel.label = Otwórz w drugim panelu OpenInBothPanels.label = Otwórz w dwóch panelach RevealInDesktop.label = Otwórz w %1 RunCommand.label = Uruchom RunCommand.tooltip = Wykonaj polecenie w aktualnym katalogu Pack.tooltip = Dodaj zaznaczone pliku do archiwu Unpack.tooltip = Rozpakuj zaznaczone archiwum(a) ShowFileProperties.label = Właściwości ShowFileProperties.tooltip = Pokaż właściwości zaznaczonych plików ShowPreferences.label = Preferencje ShowPreferences.tooltip = Konfiguruj trolCommandera ShowServerConnections.label = Pokaż otwarte połączenia Quit.label = Koniec ReverseSortOrder.label = Odwrotna kolejność ToggleAutoSize.label = Automatycznie rozszerz kolumny Stop.label = Zaniechaj zmiany katalogu ToggleColumn.show = Pokaż kolumnę %1 ToggleColumn.hide = Ukryj kolumnę %1 ToggleCommandBar.show = Pokaż pasek komend ToggleCommandBar.hide = Ukryj pasek komend ToggleToolBar.show = Pokaż pasek narzędzi ToggleToolBar.hide = Ukryj pasek narzędzi CustomizeCommandBar.label = Dostosuj pasek komend ToggleStatusBar.show = Pokaż pasek statusu ToggleStatusBar.hide = Ukryj pasek statusu ToggleShowFoldersFirst.label = Pokazuj foldery jako pierwsze ToggleTree.label = Pokaż drzewo katalogów PopupLeftDriveButton.label = Zmień lewy katalog PopupRightDriveButton.label = Zmień prawy katalog RecallPreviousWindow.label = Przełącz do poprzedniego okna RecallNextWindow.label = Przełącz do następnego okna RecallWindow.label = Wywołaj okno #%1 BringAllToFront.label = Pokaż wszystkie okna SwitchActiveTable.label = Zamień aktualne panel SelectNextBlock.label = Skocz blok w dół SelectPreviousBlock.label = Skocz blok w dół SelectNextPage.label = Skocz stronę w dół SelectPreviousPage.label = Skocz stronę w górę SelectNextRow.label = Skocz wiersz niżej SelectPreviousRow.label = Skocz wiersz wyżej SelectFirstRow.label = Zaznacz pierwszy plik w aktualnym katalogu SelectLastRow.label = Zaznacz ostatnij plik w aktualnym katalogu SplitEqually.label = Podziel okno SplitVertically.label = Podziel pionowo SplitHorizontally.label = Podziel poziomo ShowKeyboardShortcuts.label = Skróty klawiaturowe GoToWebsite.label = Idź do strony internetowej GoToForums.label = Zobacz forum ReportBug.label = Zgłoś błąd Donate.label = Wesprzyj ShowAbout.label = O programie trolCommander OpenTrash.label = Otwórz kosz EmptyTrash.label = Opróżnij kosz CalculateChecksum.label = Oblicz sumę kontrolną MaximizeWindow.label = Maksymalizuj MinimizeWindow.label = Minimalizuj GoToDocumentation.label = Dokumentacja online ShowParentFoldersQL.label = Katalogi nadrzędne ShowRecentLocationsQL.label = Ostatnie lokalizacje ShowRecentExecutedFilesQL.label = Ostatnio wykonywane pliki SplitFile.tooltip = Podziel plik na wiele części CombineFiles.tooltip = Scal części podzielonego pliku aby uzyskać oryginalny plik ShowDebugConsole.label = Konsola debugowania FocusPrevious.label = Przenieś fokus do poprzedniego komponentu FocusNext.label = Przenieś fokus do następnego komponentu file_menu = Plik file_menu.open_with = Otwórz za pomocą mark_menu = Zaznacz view_menu = Podgląd view_menu.show_hide_columns = Pokaż/Ukryj kolumny go_menu = Idź bookmarks_menu = Zakładki bookmarks_menu.no_bookmark = Brak zakładek drive_popup.network_shares = Sieć quick_lists_menu = Szybkie listy window_menu = Okno help_menu = Pomoc status_bar.selected_files = %1 z %2 zaznaczonych status_bar.connecting_to_folder = Podłączam do katalogu, naciśnij ESCAPE aby anulować. status_bar.volume_free = Wolne: %1 status_bar.volume_capacity = Pojemność: %1 shortcuts_panel.title = Skróty shortcuts_panel.restore_defaults = Przywróć domyślne shortcuts_panel.show = Pokaż shortcuts_panel.default_message = Wciśnij Enter lub kliknij dwukrotnie na skrócie, który chcesz edytować shortcuts_table.action_description = Opis akcji shortcuts_table.shortcut = Skrót shortcuts_table.alternate_shortcut = Alternatywny skrót shortcuts_table.type_in_a_shortcut = Wprowadź skrót command_bar_dialog.help = Przenieś przyciski aby dostosować pasek komend table.folder_access_error_title = Błąd dostępu do katalogu table.folder_access_error = Nie mogę odczytać zawartości katalogu table.download_or_browse = Chcesz przeglądać lub ściągnąć ten plik? version_dialog.no_new_version_title = Brak nowych wersji version_dialog.no_new_version = Gratulacje, posiadasz najnowszą wersję. version_dialog.new_version_title = Dostępna jest nowsza wersja version_dialog.new_version = Dostępna jest nowsza wersja trolCommandera. version_dialog.new_version_url = Dostępna jest nowsza wersja trolCommandera pod adresem %1. version_dialog.not_available_title = Serwer nie odpowiada version_dialog.not_available = Nie mogę uzyskać informacji o wersji z serwera. version_dialog.install_and_restart = Instaluj i uruchom version_dialog.preparing_for_update = Przygotowanie do uaktualnienia... quit_dialog.title = Zakończ trolCommandera quit_dialog.desc = Masz otwartych %1 okien. Zamknąć wszystkie okna i zakończyć pracę trolCommandera ? quit_dialog.show_next_time = Pokaż następnym razem destination_dialog.file_exists_action = Domyślna akcja kiedy plik istnieje destination_dialog.verify_integrity = Weryfikuj zgodność danych destination_dialog.skip_errors = Pomiń błędy file_collision_dialog.title = Kolizja plików rename_dialog.new_name = Nowa nazwa copy_dialog.destination = Kopiuj zaznaczone pliki do copy_dialog.error_title = Błąd kopiowania copy_dialog.copying = Kopiuje pliki copy_dialog.copying_file = Kopiuję %1 pack_dialog.packing = Pakuję pack_dialog.packing_file = Spakuj %1 pack_dialog.error_title = Błąd kompresji pack_dialog_description = Dodaj wybrane pliki do pack_dialog.archive_format = Typ archiwum unpack_dialog.destination = Rozpakuj zaznaczone pliki do unpack_dialog.error_title = Błąd rozpakowania unpack_dialog.unpacking = Rozpakuj pliki unpack_dialog.unpacking_file = Rozpakuj %1 optimizing_archive = Optymalizacja archiwum %1 error_while_optimizing_archive = Błąd podczas optymalizacji archiwum %1 move_dialog.move_description = Przenieś do move_dialog.error_title = Błąd przenoszenia move_dialog.moving = Przenoszę pliki move_dialog.moving_file = Przenoszę %1 download_dialog.description = Pobierz plik do download_dialog.error_title = Błąd pobierania download_dialog.downloading = Pobierz download_dialog.downloading_file = Pobieram %1 mkfile_dialog.allocate_space = Ustaw wielkość delete_dialog.permanently_delete.confirmation = Trwale usunąć zaznaczone pliki ? delete_dialog.move_to_trash.confirmation_details = Pliki zostaną przeniesione do kosza. delete_dialog.move_to_trash.option = Przenieś do kosza delete_dialog.move_to_trash.failed = Jeden lub więcej plików nie może być przeniesionych do kosza. delete_dialog.deleting = Usuwam delete_dialog.error_title = Błąd usuwania delete.deleting_file = Usuwam %1 email_dialog.prefs_not_set_title = Poczta nie skonfigurowana email_dialog.prefs_not_set = Musisz ustwaić najpierw parametry poczty. email_dialog.from = Od email_dialog.to = Do email_dialog.subject = Temat email_dialog.send = Wyślij email_dialog.error_title = Błąd wysyłania plików email_dialog.read_error = Nie mogę czytać plików w podkatalogu. email_dialog.sending = Wysyłam pliki email.sending_file = Wysyłam %1 email.connecting_to_server = Podłączam się do %1 email.server_unavailable = Nie mogę się z kontaktować z serwerem poczyowym %1, sprawdź ustawienia lub spróbuj ponownie. email.connection_closed = Połączenie przerwane przez serwer, poczta nie wysłana. email.goodbye_failed = Błąd podczas zamykania połączenia, poczta może zostać niewysłana. email.send_file_error = Nie mogę wysłać pliku %1, poczta nie wysłana. split_file_dialog.error_title = Błąd podziału pliku split_file_dialog.file_to_split = Plik do podziału split_file_dialog.target_directory = Katalog docelowy split_file_dialog.part_size = Wielkość jednej części split_file_dialog.parts = Liczba części split_file_dialog.generate_CRC = Wygeneruj plik CRC split_file_dialog.max_parts = Maksymalna liczba części wynosi %1 split_file_dialog.auto = Automatycznie split_file_dialog.insert_new_media = Włóż kolejny dysk combine_files_dialog.error_title = Błąd łączenia plików combine_files_job.no_crc_file = Scalanie zakończone pomyślnie. Brak pliku CRC. combine_files_job.crc_read_error = Błąd podczas odczytywania pliku CRC. combine_files_job.crc_check_failed = Pliki CRC nie zgadzają się (oczekiwano %2, jest %1) combine_files_job.crc_ok = Scalanie zakończone pomyślnie. Plik CRC zweryfikowany. file_selection_dialog.mark = Zaznacz file_selection_dialog.unmark = Odznacz file_selection_dialog.mark_description = Zaznacz pliki nazywające się file_selection_dialog.unmark_description = Odznacz pliki nazywające się file_selection_dialog.case_sensitive = Rozróżniaj wielkość liter file_selection_dialog.include_folders = Uwzględnij podfoldery progress_dialog.starting = Transfer rozpoczęty... progress_dialog.transferred = Przesłano %1 z %2 progress_dialog.elapsed_time = Czas progress_dialog.advanced = Zaawansowane progress_dialog.current_speed = Aktualna prędkość progress_dialog.limit_speed = Ograniczenie prędkości progress_dialog.close_when_finished = Zamknij okno po zakończeniu progress_dialog.processing_files = Przetwarzam pliki progress_dialog.processing_file = Przetworzono %1 progress_dialog.verifying_file = Weryfikacja %1 progress_dialog.job_finished = Zakończono progress_dialog.job_error = Błąd properties_dialog.file_properties = %1 Właściwości properties_dialog.contents = Zawartość properties_dialog.calculating = Obliczam... calculate_checksum_dialog.checksum_algorithm = Algorytm sumy kontrolnej calculate_checksum_dialog.temporary_file = Plik tymczasowy change_date_dialog.now = Teraz change_date_dialog.specific_date = Określona data run_dialog.run_command_description = Uruchom w aktywnym katalogu run_dialog.run_in_home_description = Uruchom w katalogu domowym run_dialog.command_output = Wynik polecenia run_dialog.run = Uruchom run_dialog.clear_history = Wyczyć historię server_connect_dialog.server_type = Typ połączenia server_connect_dialog.server = Serwer server_connect_dialog.share = Udział server_connect_dialog.domain = Domena server_connect_dialog.username = Nazwa użytkownika server_connect_dialog.initial_dir = Katalog początkowy server_connect_dialog.port = Port server_connect_dialog.server_url = Adres serwera server_connect_dialog.http_url = Adres strony internetowej server_connect_dialog.connect = Połącz server_connect_dialog.protocol = Protokół server_connect_dialog.nfs_version = Wersja NFS server_connect_dialog.private_key = Klucz prywatny server_connect_dialog.passphrase = Hasło ftp_connect.passive_mode = Użyj trybu pasywnego ftp_connect.anonymous_user = Użytkownik anonimowy ftp_connect.nb_connection_retries = Liczba ponowień połączenia ftp_connect.retry_delay = Opóźnienie między ponowieniem połączenia (w sekundach) http_connect.basic_authentication = HTTP Prosta Autoryzacja/Basic Authentication (opcja) server_connections_dialog.disconnect = Rozłącz server_connections_dialog.connection_busy = Zajęty server_connections_dialog.connection_idle = Nieaktywny bonjour.bonjour_services = Powitanie bonjour.no_service_discovered = Nie rozpoznano serwisu bonjour.bonjour_disabled = Powitanie nieaaktywne auth_dialog.title = Autoryzacja auth_dialog.desc = Proszę wprowadzić nazwę użytkownika i hasło auth_dialog.server = Serwer auth_dialog.store_credentials = Zapisz login i hasło (słabe szyfrowanie) auth_dialog.connect_as = Połącz jako auth_dialog.authentication_failed = Autentykacja nieudana sortable_list.move_up = Przenieś do góry sortable_list.move_down = Przenieś na dół add_bookmark_dialog.add = Dodaj edit_bookmarks_dialog.new = Nowa file_viewer.view_error_title = Błąd podglądu pliku file_viewer.view_error = Nie mogę wyświetlić pliku file_viewer.file_menu = Plik file_viewer.close = Zamknij file_viewer.large_file_warning = Wybrany plik może być zbyt duży dla tej operacji. file_viewer.open_anyway = Otwórz mimo wszystko text_viewer.edit = Edytuj text_viewer.copy = Kopiuj text_viewer.select_all = Zaznacz wszystko text_viewer.find = Znajdź text_viewer.find_next = Znajdź następne text_viewer.find_previous = Znajdź poprzednie text_viewer.binary_file_warning = Wygląda że jest to plik binarny nie tekstowy image_viewer.controls_menu = Sterowanie image_viewer.zoom_in = Powiększ image_viewer.zoom_out = Pomniejsz file_editor.edit_error_title = Błąd edycji file_editor.edit_error = Błąd edycji pliku. file_editor.save = Zapisz file_editor.save_as = Zapisz jako... file_editor.save_warning = Zapisać zmiany przed zamknięciem ? file_editor.cannot_write = Nie mogę zapisać pliku. text_editor.cut = Wytnij text_editor.paste = Wklej shortcuts_dialog.quick_search.start_search = Wpisz dowolne znaki aby rozpocząć wyszukiwanie shortcuts_dialog.quick_search.cancel_search = Anuluj szybkie wyszukiwanie shortcuts_dialog.quick_search.remove_last_char = Usuń ostatni znak z szybkiego wyszukiwania shortcuts_dialog.quick_search.jump_to_previous = Przejdź do poprzednich wyników wyszukiwania shortcuts_dialog.quick_search.jump_to_next = Przejdź do natępnych wyników wyszukiwania shortcuts_dialog.quick_search.mark_jump_next = Zaznacz/Odznacz aktualny plik i skocz do następnych wyników wyszukiwania theme_editor.title = Edytor tematu theme_editor.folder_tab = Panel plików theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Historia shell'a theme_editor.statusbar_tab = Pasek stanu theme_editor.free_space = Wolne miejsce theme_editor.free_space.ok = OK theme_editor.free_space.warning = Ostrzeżenie theme_editor.free_space.critical = Krytyczne theme_editor.locationbar_tab = Pasek lokalizacji theme_editor.editor_tab = Edytor plików theme_editor.font = Czcionka theme_editor.active_panel = Aktywny theme_editor.inactive_panel = Nieaktywny theme_editor.general = Ogólne theme_editor.could_not_save_theme = Nie mogę zapisać tematu %1 theme_editor.border = Ramka theme_editor.background = Tło theme_editor.alternate_background = Tło (alternatywne) theme_editor.unfocused_background = Background (bez fokusa) theme_editor.quick_search.unmatched_file = Pliki niepasujące theme_editor.text = Tekst theme_editor.progress = Postęp theme_editor.normal = Zwykły theme_editor.normal_unfocused = Normalne (bez fokusa) theme_editor.selected = Zaznaczone theme_editor.selected_unfocused = Zaznaczone (bez fokusa) theme_editor.color = Kolor theme_editor.colors = Kolory theme_editor.plain_file = Zwykły plik theme_editor.marked_file = Zaznaczony plik theme_editor.hidden_file = Ukryty plik theme_editor.folder = Katalog theme_editor.archive_file = Plik archiwum theme_editor.symbolic_link = Dowiązanie symboliczne theme_editor.header = Nagłówek theme_editor.item = Pozycja command_bar_customize_dialog.available_actions = Dostępne akcje command_bar_customize_dialog.modifier = Modyfikator prefs_dialog.title = Ustawienia prefs_dialog.general_tab = Główne prefs_dialog.day = Dzień prefs_dialog.month = Miesiąc prefs_dialog.year = Rok prefs_dialog.language = Język (Wymaga restartu) prefs_dialog.date_time = Format daty i czasu prefs_dialog.time = Czas prefs_dialog.date = Data prefs_dialog.date_separator = Separator/Odstęp prefs_dialog.time_12_hour = format 12 godzinny prefs_dialog.time_24_hour = format 24 godzinny prefs_dialog.show_seconds = Pokaż sekundy prefs_dialog.show_century = Pokaż wiek prefs_dialog.check_for_updates_on_startup = Sprawdź czy są aktulizacje przy starcie prefs_dialog.show_splash_screen = Pokaż ekran powitalny prefs_dialog.folders_tab = Katalogi prefs_dialog.startup_folders = Katalogi startowe prefs_dialog.left_folder = Lewy panel prefs_dialog.right_folder = Prawy panel prefs_dialog.last_folder = Ostatnio przeglądany katalog prefs_dialog.custom_folder = Katalog użytkownika prefs_dialog.show_hidden_files = Pokaż ukryte pliki prefs_dialog.show_ds_store_files = Pokaż pliki .DS_Store prefs_dialog.show_system_folders = Pokaż katalogi systemowe prefs_dialog.compact_file_size = Zaokrągl rozmiar wyświetlanych plików prefs_dialog.follow_symlinks_when_cd = Zmień katalog przy przechodzeniu do dowiązania symbolicznego prefs_dialog.appearance_tab = Wygląd prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Rozmiar ikon prefs_dialog.toolbar_icons = Pasek narzędzi prefs_dialog.command_bar_icons = Linia komend prefs_dialog.file_icons = Typy plików prefs_dialog.use_system_file_icons = Używaj ikon systemowych prefs_dialog.use_system_file_icons.always = Zawsze prefs_dialog.use_system_file_icons.never = Nigdy prefs_dialog.use_system_file_icons.applications = Tylko dla aplikacji prefs_dialog.edit_current_theme = Edytuj bieżący temat... prefs_dialog.themes = Tematy wizualne (skórki) prefs_dialog.import_theme = Importuj temat prefs_dialog.import_look_and_feel = Zaimportuj wygląd i zachowanie (look & feel) prefs_dialog.no_look_and_feel = Nie znaleziono pliku ustawień wyglądu i zachowania (look & feel). prefs_dialog.error_in_import = Błąd podczas importu tematu %1. prefs_dialog.cannot_read_theme = Nie można załadować tematu z pliku %1 prefs_dialog.export_theme = Eksport %1 prefs_dialog.import = Import prefs_dialog.export = Eksport prefs_dialog.theme_type = Typ: %1 prefs_dialog.delete_theme = Czy na pewno usunąć temat %1 ? prefs_dialog.delete_look_and_feel = Definitywnie skasować ustawienia wyglądu i zachowania (look & feel) %1 ? prefs_dialog.rename_failed = Nie można zmienić nazwy tematu %1 prefs_dialog.xml_file = plik XML prefs_dialog.jar_file = Plik JAR prefs_dialog.mail_tab = Poczta elektroniczna prefs_dialog.mail_settings = Ustawienia poczty wychodzącej prefs_dialog.mail_name = Twoie imię prefs_dialog.mail_address = Twój adres email prefs_dialog.mail_server = Serwer poczty wychodzącej SMTP prefs_dialog.misc_tab = Różne prefs_dialog.use_brushed_metal = Użyj wyglądu 'brushed metal' (Wymaga restartu) prefs_dialog.confirm_on_quit = Pokaż okno potwierdzające przy wyjściu prefs_dialog.default_shell = Użyj domylnego systemowego shella prefs_dialog.custom_shell = Użyj shella użytkownika prefs_dialog.shell_encoding = Kodowanie shella prefs_dialog.auto_detect_shell_encoding = Auto-wybór prefs_dialog.enable_bonjour_discovery = Uaktywnij Powitanie prefs_dialog.enable_system_notifications = Włącz powiadomienia debug_console_dialog.level = Poziom unit.byte = bajt unit.bytes = bajty unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1g duration.days = %1d duration.months = %1M duration.years = %1r theme.custom_theme = Temat użytkownika theme.custom = Użytkownika theme.built_in = Wbudowany theme.add_on = Dodatek theme.current = bieżący theme_could_not_be_loaded = Wystąpł błąd podczas ładowania tematu wizualnego setup.title = Witamy w programie trolCommander setup.intro = Proszę wybrać jak ma się zachowywać trolCommander. setup.look_and_feel = Wygląd i zachowanie setup.theme = Wybierz temat wizualny font_chooser.font_size = Rozmiar font_chooser.font_bold = Pogrubiony font_chooser.font_italic = Kursywa color_chooser.red = Czerwony color_chooser.green = Zielony color_chooser.blue = Niebieski color_chooser.hue = Odcień color_chooser.brightness = Jasność color_chooser.swatches = Próbki color_chooser.saturation = Nasycenie color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Ostatnie color_chooser.alpha = Przeźroczystość color_chooser.title = Wybierz kolor batch_rename_dialog.mask = Matryca batch_rename_dialog.search_replace = Znajdź i zamień batch_rename_dialog.search_for = Szukaj batch_rename_dialog.replace_with = Zamień na batch_rename_dialog.counter = Licznik batch_rename_dialog.start_at = Start batch_rename_dialog.step_by = Krok batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Wielkość znaków batch_rename_dialog.no_change = Bez zmian batch_rename_dialog.lower_case = małe litery batch_rename_dialog.upper_case = DUŻE LITERY batch_rename_dialog.first_upper = Pierwsza litera duża batch_rename_dialog.word = Pierwsza Każdego Słowa batch_rename_dialog.old_name = Stara nazwa batch_rename_dialog.new_name = Nowa nazwa batch_rename_dialog.block_name = Zablokuj batch_rename_dialog.range = Zakres batch_rename_dialog.proceed_renaming = %1 nazw plików z %2 będzie zmienionych. Czy kontynuować? batch_rename_dialog.duplicate_names = Wykryto zduplikowane nazwy! batch_rename_dialog.names_conflict = Wykryto konflikt nazw! Wartości w kolumnie stara i nowa nazwa są takie same. parent_folders_quick_list.empty_message = Bieżąca lokalizacja nie posiada katalogu nadrzędnego recent_locations_quick_list.empty_message = Brak ostanich lokalizacji recent_executed_files_quick_list.empty_message = Brak ostatnio uruchamianych plików #pack.error_on_file = Błąd podczas kompresji %1 #pack_dialog.cannot_write = Nie mogę utworzyć archiwum w katalogu docelowym #unpack.unable_to_open_zip = Nie mogę otworzyć archiwum %1. #mkdir_dialog.title = Utwórz katalog #mkdir_dialog.error_title = Błąd tworzenia katalogu #edit_bookmarks_dialog.remove = Usuń #mkdir_dialog.description = Utwórz katalog #done = Zrobione #progress_dialog.hide = Ukryj #move_dialog.rename_description = Zmień nazwę na #theme_editor.shell_colors = Kolor okna terminala (shell) #auth_dialog.error_was = Wystąpił błąd: %1 #table.hide_column = Ukryj kolumny #delete.symlink_warning_title = Znaleziono łącze symboliczne #delete.symlink_warning = Ten plik wygląda jak łącze symboliczne:\n\n Plik: %1\n wskazuje na: %2\n\nSkasuj łącze symboliczne tylko lub\npodąż za dowiązaniem i skasuj katalog (UWAGA) ? #delete.delete_link_only = Usuń dowiązanie (link) #delete.delete_linked_folder = Usuń katalog #Unpack.label = Rozpakuj pliki #Pack.label = Spakuj pliki ================================================ FILE: src/main/resources/dictionary_pt_BR.properties ================================================ ok = Aceitar yes = Sim no = Não cancel = Cancelar edit = Editar close = Fechar reset = Reiniciar rename = Renomear apply = Aplicar change = Alterar save = Salvar dont_save = Não salvar replace = Substituir dont_replace = Não substituir delete = Remover skip = Ignorar skip_all = Ignorar tudo retry = Tentar novamente resume = Resumir overwrite = Sobrescrever overwrite_if_older = Sobrescrever se mais antigo duplicate = Duplicar apply_to_all = Aplicar a todos copy = Copiar move = Mover pack = Compactar unpack = Descompactar download = Baixar split = Dividir combine = Combinar browse = Navegar ask = Perguntar stop = Parar pause = Pausa quick_search = Busca rápida file_manager = Gerenciador de arquivos create = Criar creating_file = Criando %1 choose = Escolha customize = Customize choose_folder = Escolha um diretório login = Usuário password = Senha user = Usuário encoding = Codificação preferred_encodings = Encode preferido license = Licença name = Nome size = Tamanho date = Data extension = Extensão permissions = Permissões owner = Proprietário group = Grupo location = Localização untitled = Sem título source = Fonte destination = Destino recurse_directories = Processar recursivamente aos diretórios selecionados go_to = Ir para example = Exemplo preview = Visualizar comment = Comentário sample_text = Texto de exemplo nb_files = %1 arquivo(s) nb_folders = %1 diretório(s) loading = Carregando... this_operation_cannot_be_undone = Esta operação não poderá ser desfeita remove = Remover details = Detalhes warning = Alerta error = Erro generic_error = Ocorreu um erro durante a operação folder_does_not_exist = Diretório inexistente ou não disponível this_folder_does_not_exist = Este diretório não existe ou não está disponível: %1 this_file_does_not_exist = Este arquivo não existe ou não está disponível: %1 invalid_path = Caminho inválido: %1 directory_already_exists = Este diretório já existe file_exists_in_destination = Existe um arquivo com mesmo nome neste caminho source_parent_of_destination = Tentativa de transferir um diretório para um de seus subdiretórios cannot_read_file = Não foi possível ler o arquivo %1 cannot_write_file = Não foi possível escrever o arquivo %1 cannot_create_folder = Não foi possível criar o diretório %1 cannot_read_folder = Não foi possível ler o diretório %1 cannot_delete_file = Não foi possível apagar o arquivo %1 cannot_delete_folder = Não foi possível apagar o diretório %1 error_while_transferring = Erro ao transfer o arquivo %1 same_source_destination = Diretório fonte e de destino são os mesmos file_already_exists = %1 já existe, deseja sobrescrevê-lo ? write_error = Erro de escrita read_error = Erro de leitura integrity_check_error = Falha na checagem de integridade: fonte e destino estão diferentes startup_error = Um erro impediu trolCommander de iniciar. permissions.read = Leitura permissions.write = Gravação permissions.executable = Execução permissions.group = Grupo permissions.other = Outros permissions.octal_notation = Notação Octal action_categories.all = Todos action_categories.navigation = Navegação action_categories.selection = Seleção action_categories.view = Visualizar action_categories.file_operations = Operações com arquivos action_categories.windows = Janelas Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Adicionar aos favoritos AddBookmark.tooltip = Adicionar o diretório atual à lista de favoritos BatchRename.label = Multipla renomeação EditBookmarks.label = Editar favoritos ExploreBookmarks.label = Explorar favoritos EditCredentials.label = Editar credenciais ChangeLocation.label = Alterar localização corrente ChangeDate.label = Alterar data ChangeDate.tooltip = Alterar data do(s) arquivo(s) selecionado(s) ChangePermissions.label = Alterar permissões ChangePermissions.tooltip = Alterar permissões do(s) seguinte(s) arquivo(s) CheckForUpdates.label = Checar atualizações CompareFolders.label = Comparar diretórios ConnectToServer.label = Conectar ao servidor ConnectToServer.tooltip = Conectar ao servidor remoto View.label = Ver InternalView.label = Ver (interno) View.tooltip = Ver arquivo selecionado InternalEdit.label = Editar (interno) Edit.tooltip = Editar arquivo selecionado Copy.tooltip = Copiar arquivos selecionados LocalCopy.label = Cópia local LocalCopy.tooltip = Copiar arquivo selecionado para o diretório atual Move.tooltip = Mover arquivos selecionados Rename.tooltip = Renomear os arquivos selecionados Mkdir.label = Criar diretório Mkdir.tooltip = Criar um diretório no diretório atual Mkfile.label = Criar arquivo Mkfile.tooltip = Criar um arquivo no diretório atual Delete.tooltip = Apagar arquivos selecionados PermanentDelete.label = Remover definitivamente PermanentDelete.tooltip = Remover arquivos marcados definitivamente Refresh.label = Atualizar Refresh.tooltip = Atualizar diretório atual CloseWindow.label = Fechar janela CloseWindow.tooltip = Fechar esta janela CopyFileNames.label = Copiar nome(s) CopyFilePaths.label = Copiar caminho(s) CopyFilesToClipboard.label = Copiar arquivo(s) PasteClipboardFiles.label = Colar arquivo(s) Email.label = Enviar por email Email.tooltip = Enviar arquivos selecionados como anexos de email GoBack.label = Voltar GoBack.tooltip = Ir para o diretório anterior GoForward.label = Seguir GoForward.tooltip = Ir para o próximo diretório GoToHome.label = Ir para o diretório inicial GoToParent.label = Ir para o diretório superior GoToParent.tooltip = Ir para o diretório superior GoToParentInOtherPanel.label = Exibir o nível superior em outro painel GoToParentInBothPanels.label = Exibir o nível superior em ambos os paineis GoToRoot.label = Ir à raiz SortByName.label = Order por nome SortByDate.label = Ordenar por data SortBySize.label = Ordenar pelo tamanho SortByExtension.label = Ordenar pelo extensão SortByPermissions.label = Ordenar pela permissão SortByOwner.label = Ordenar pelo proprietário SortByGroup.label = Ordenar pelo grupo MarkGroup.label = Marcar arquivos MarkGroup.tooltip = Marcar um grupo de arquivos UnmarkGroup.label = Desmarcar arquivos UnmarkGroup.tooltip = Desmarcar um grupo de arquivos MarkAll.label = Marcar tudo UnmarkAll.label = Desmarcar tudo MarkSelectedFile.label = Marcar/Desmarcar MarkSelectedFile.tooltip = Marcar/Desmarcar arquivos selecionados MarkNextBlock.label = Marque um próximo bloco MarkPreviousBlock.label = Marque um bloco anterior MarkNextRow.label = Marque uma próxima linha MarkPreviousRow.label = Marque uma linha anterior MarkNextPage.label = Marcar página abaixo MarkPreviousPage.label = Marcar página acima MarkToFirstRow.label = Marcar arquivos até o início MarkToLastRow.label = Marcar arquivos até o final MarkExtension.label = Marcar extensão InvertSelection.label = Inverter seleção SwapFolders.label = Trocar diretórios SwapFolders.tooltip = Trocar diretórios SetSameFolder.label = Marcar mesmo diretório SetSameFolder.tooltip = Abilitar mesmo diretório em ambos os lados NewWindow.label = Nova janela NewWindow.tooltip = Abrir uma nova janela Open.label = Abrir Open.tooltip = Abrir diretório / Abrir arquivo / Executar OpenNatively.label = Abrir com programa padrão OpenNatively.tooltip = Executar o arquivo com o programa padrão OpenInOtherPanel.label = Abrir em um novo painel OpenInBothPanels.label = Abrir em ambos os paineis RevealInDesktop.label = Mostrar em %1 RunCommand.label = Executar comando RunCommand.tooltip = Executar comando no diretório atual Pack.tooltip = Compactar arquivos selecionados Unpack.tooltip = Descompactar arquivos selecionados ShowFileProperties.label = Propriedades ShowFileProperties.tooltip = Exibir propriedades dos arquivos selecionados ShowPreferences.label = Preferências ShowPreferences.tooltip = Configurar trolCommander ShowServerConnections.label = Exibir conexões abertas Quit.label = Sair ReverseSortOrder.label = Inverter ordem ToggleAutoSize.label = Dimensionar colunas automaticamente Stop.label = Parar mudança de diretórios ToggleColumn.show = Exibir coluna %1 ToggleColumn.hide = Esconder coluna %1 ToggleCommandBar.show = Exibir barra de comandos ToggleCommandBar.hide = Esconder barra de comandos ToggleToolBar.show = Exibir barra de ferramentas ToggleToolBar.hide = Esconder barra de ferramentas CustomizeCommandBar.label = Customizar barra de comando ToggleStatusBar.show = Exibir barra de status ToggleStatusBar.hide = Esconder barra de status ToggleShowFoldersFirst.label = Exibir diretórios primeiro ToggleTree.label = Visualização em árvore PopupLeftDriveButton.label = Mudar diretório esquerdo PopupRightDriveButton.label = Mudar diretório direito RecallPreviousWindow.label = Recarregar janela anterior RecallNextWindow.label = Recarregar próxima janela RecallWindow.label = Recarregar a janela #%1 BringAllToFront.label = Trazer tudo para frete SwitchActiveTable.label = Alternar entre os paneis esquerdo e direito SelectNextBlock.label = Pular para próximo bloco SelectPreviousBlock.label = Pular para bloco anterior SelectNextPage.label = Pular para próxima página SelectPreviousPage.label = Pular para página anterior SelectNextRow.label = Pular para próxima linha SelectPreviousRow.label = Pular para linha anterior SelectFirstRow.label = Selecionar primeiro arquivo do diretório corrente SelectLastRow.label = Selecionar último arquivo do diretório corrente SplitEqually.label = Dividir igualmente SplitVertically.label = Dividir verticalmente SplitHorizontally.label = Dividir horizontalmente ShowKeyboardShortcuts.label = Atalhos de teclado GoToWebsite.label = Ir para website GoToForums.label = Ir para fóruns ReportBug.label = Reportar uma falha Donate.label = Fazer uma doação ShowAbout.label = Sobre trolCommander OpenTrash.label = Abrir lixeira EmptyTrash.label = Esvaziar lixeira CalculateChecksum.label = Calcular checksum MaximizeWindow.label = Maximizar MaximizeWindow.label.mac_os_x = Zoom MinimizeWindow.label = Minimizar GoToDocumentation.label = Documentação online ShowParentFoldersQL.label = Diretório superior ShowRecentLocationsQL.label = Locais recentes ShowRecentExecutedFilesQL.label = Arquivos executados recentemente SplitFile.tooltip = Dividir um arquivo em várias partes CombineFiles.tooltip = Recriar arquivo dividido em várias partes ShowDebugConsole.label = Console de debug FocusPrevious.label = Focar componente anterior FocusNext.label = Focar próximo componente file_menu = Arquivo file_menu.open_with = Abrir com mark_menu = Marcar view_menu = Ver view_menu.show_hide_columns = Exibir/Ocultar colunas go_menu = Ir bookmarks_menu = Favoritos bookmarks_menu.no_bookmark = Nenhum favorito definido quick_lists_menu = Quick lists window_menu = Janela help_menu = Ajuda status_bar.selected_files = %1 de %2 selecionados status_bar.connecting_to_folder = Conectando ao diretório, tecle ESC para cancelar. status_bar.volume_free = Livre: %1 status_bar.volume_capacity = Capacidade: %1 shortcuts_panel.title = Atalhos shortcuts_panel.restore_defaults = Restaurar shortcuts_panel.show = Exibir shortcuts_panel.default_message = Aperte Enter ou dê dois cliques sobre o atalho que você deseja editar shortcuts_table.action_description = Descrição da ação shortcuts_table.shortcut = Atalho shortcuts_table.alternate_shortcut = Atalho secundário shortcuts_table.type_in_a_shortcut = Digite um atalho command_bar_dialog.help = Arraste os butões para customizar a barra de comandos table.folder_access_error_title = Erro ao acessar diretório table.folder_access_error = Não foi possível ler conteúdo do diretório table.download_or_browse = Você gostaria de explorar ou baixar este arquivo ? version_dialog.no_new_version_title = Nenhuma nova versão disponível version_dialog.no_new_version = Parabéns, você já possue a última versão. version_dialog.new_version_title = Nova versão disponível version_dialog.new_version = Uma nova versão do trolCommander está disponível. version_dialog.new_version_url = Uma nova versão do trolCommander está disponível em %1. version_dialog.not_available_title = Servidor não disponível version_dialog.not_available = Não foi possível obter informação de versão no servidor. version_dialog.install_and_restart = Instalar e reiniciar version_dialog.preparing_for_update = Preparando atualização... quit_dialog.title = Sair do trolCommander quit_dialog.desc = Você tem %1 janela(s) aberta(s). Deseja realmente sair? quit_dialog.show_next_time = Exibir na próxima vez destination_dialog.file_exists_action = Ação padrão quando o arquivo existir destination_dialog.verify_integrity = Verificando integridade dos dados destination_dialog.skip_errors = Ignore erros file_collision_dialog.title = Colisão de arquivo rename_dialog.new_name = Novo nome copy_dialog.destination = Copiar arquivo(s) selecionado(s) para copy_dialog.error_title = Erro ao copiar copy_dialog.copying = Copiando arquivos copy_dialog.copying_file = Copiando %1 pack_dialog.packing = Compactando arquivos pack_dialog.packing_file = Compactando %1 pack_dialog.error_title = Erro na compactação pack_dialog_description = Adicionar arquivos selecionados ao pack_dialog.archive_format = Formato de arquivo unpack_dialog.destination = Descompactar arquivo(s) selecionado(s) para unpack_dialog.error_title = Erro ao descompactar unpack_dialog.unpacking = Descompactando arquivos unpack_dialog.unpacking_file = Descompactando %1 optimizing_archive = Otimizando arquivo %1 error_while_optimizing_archive = Erro ao otimizar arquivo %1 move_dialog.move_description = Mover para move_dialog.error_title = Erro ao mover move_dialog.moving = Movendo arquivos move_dialog.moving_file = Movendo %1 download_dialog.description = Baixar arquivo para download_dialog.error_title = Erro ao baixar download_dialog.downloading = Baixando download_dialog.downloading_file = Baixando %1 mkfile_dialog.allocate_space = Tamanho delete_dialog.permanently_delete.confirmation = Remover permanentemente os arquivo(s) selecionado(s) ? delete_dialog.move_to_trash.confirmation = Remover o(s) arquivo(s) selecionado(s) ? delete_dialog.move_to_trash.confirmation_details = Arquivos serão movidos para a lixeira. delete_dialog.move_to_trash.option = Mover para lixeira delete_dialog.move_to_trash.failed = Um ou mais arquivos não puderam ser movidos para a lixeira. delete_dialog.deleting = Removendo delete_dialog.error_title = Erro ao remover delete.deleting_file = Removendo %1 email_dialog.prefs_not_set_title = Conta de email não configurada email_dialog.prefs_not_set = Você deve primeiramente configurar sua conta de email. email_dialog.from = De email_dialog.to = Para email_dialog.subject = Assunto email_dialog.send = Enviar email_dialog.error_title = Erro ao enviar arquivos email_dialog.read_error = Não foi possível ler os arquivos no subdiretório email_dialog.sending = Enviando arquivos email.sending_file = Enviando %1 email.connecting_to_server = Conectando à %1 email.server_unavailable = Não foi possível contactar o servidor de email %1, verifique as configurações de email ou tente novamente mais tarde. email.connection_closed = Conexão encerrada pelo servidor, email não enviado. email.goodbye_failed = Erro ao encerrar a conexão, o email talvez não tenha sido enviado. email.send_file_error = Não foi possível enviar o arquivo %1, email não enviado. split_file_dialog.error_title = Erro ao dividir arquivo split_file_dialog.file_to_split = Arquivo a ser dividido split_file_dialog.target_directory = Diretório de destino split_file_dialog.part_size = Tamanho de uma parte split_file_dialog.parts = Número de partes split_file_dialog.generate_CRC = Gerar arquivo CRC split_file_dialog.max_parts = Número máximo de partes permitidas é %1 split_file_dialog.auto = Automático split_file_dialog.insert_new_media = Insira nova mídia combine_files_dialog.error_title = Erro ao combinar arquivo combine_files_job.no_crc_file = Arquivo combinado com sucesso. Sem arquivo CRC. combine_files_job.crc_read_error = Erro ao ler arquivo CRC. combine_files_job.crc_check_failed = CRC erro de checagem: esperado %2, encontrado %1 combine_files_job.crc_ok = Combinação realizada com sucesso. CRC verificado. file_selection_dialog.mark = Marcar file_selection_dialog.unmark = Desmarcar file_selection_dialog.mark_description = Marcar arquivos com o nome file_selection_dialog.unmark_description = Desmarcar arquivos com o nome file_selection_dialog.case_sensitive = Diferenciar maiúsculas de minúsculas file_selection_dialog.include_folders = Incluir diretórios progress_dialog.starting = Iniciando transferência... progress_dialog.transferred = Transferido %1 a %2 progress_dialog.elapsed_time = Tempo decorrido progress_dialog.advanced = Avançado progress_dialog.current_speed = Velocidade atual progress_dialog.limit_speed = Limitar a velocidade progress_dialog.close_when_finished = Fechar a janela quando finalizado progress_dialog.processing_files = Processando arquivos progress_dialog.processing_file = Processando %1 progress_dialog.verifying_file = Verificando %1 progress_dialog.job_finished = Trabalho finalizado progress_dialog.job_error = Erro properties_dialog.file_properties = Propriedades de %1 properties_dialog.contents = Conteúdo properties_dialog.calculating = Calculando... calculate_checksum_dialog.checksum_algorithm = Algorítimo de checksum calculate_checksum_dialog.temporary_file = Arquivo temporário change_date_dialog.now = Agora change_date_dialog.specific_date = Data específica run_dialog.run_command_description = Executar no diretório corrente run_dialog.run_in_home_description = Executar no diretório inicial run_dialog.command_output = Saída do camando run_dialog.run = Executar run_dialog.clear_history = Limpar histórico server_connect_dialog.server_type = Tipo de conexão server_connect_dialog.server = Servidor server_connect_dialog.share = Compartilhar server_connect_dialog.domain = Domínio server_connect_dialog.username = Usuário server_connect_dialog.initial_dir = Diretório inicial server_connect_dialog.port = Porta server_connect_dialog.server_url = Endereço do servidor server_connect_dialog.http_url = Endereço do website server_connect_dialog.connect = Conectar server_connect_dialog.protocol = Protocolo server_connect_dialog.nfs_version = Versão NFS server_connect_dialog.private_key = Chave privada server_connect_dialog.passphrase = Frase secreta ftp_connect.passive_mode = Abilitando modo passivo ftp_connect.anonymous_user = Usuário anônimo ftp_connect.nb_connection_retries = Numero de tentativas de conexão ftp_connect.retry_delay = Tempo de espera entre as tentativas (segundos) http_connect.basic_authentication = HTTP Autenticação básica (opcional) server_connections_dialog.disconnect = Desconectar server_connections_dialog.connection_busy = Ocupado server_connections_dialog.connection_idle = Inativo bonjour.bonjour_services = Serviço Bonjour bonjour.no_service_discovered = Nenhum serviço encontrado bonjour.bonjour_disabled = Bonjour desabilitado auth_dialog.title = Autenticação auth_dialog.desc = Por favor, insira o usuário e a senha auth_dialog.server = Servidor auth_dialog.store_credentials = Gravar usuário e senha (criptografia baixa) auth_dialog.connect_as = Conectar como auth_dialog.authentication_failed = Falha na autenticação sortable_list.move_up = Subir sortable_list.move_down = Descer add_bookmark_dialog.add = Adicionar edit_bookmarks_dialog.new = Novo file_viewer.view_error_title = Erro de visualização file_viewer.view_error = Não possível visualizar arquivo. file_viewer.file_menu = Arquivo file_viewer.close = Fechar file_viewer.large_file_warning = Este arquivo parece ser muito grande para esta operação. file_viewer.open_anyway = Ignorar e abrir text_viewer.edit = Editar text_viewer.copy = Copiar text_viewer.select_all = Selecionar tudo text_viewer.find = Procurar text_viewer.find_next = Procurar próxima ocorrência text_viewer.find_previous = Procurar ocorrência anterior text_viewer.binary_file_warning = Parece ser um arquivo binário image_viewer.controls_menu = Controles image_viewer.zoom_in = Mais zoom image_viewer.zoom_out = Menos zoom file_editor.edit_error_title = Erro ao editar file_editor.edit_error = Não foi possível editar o arquivo. file_editor.save = Salvar file_editor.save_as = Salvar como... file_editor.save_warning = Salvar mudanças feitas ao arquivo antes de fechar ? file_editor.cannot_write = Não foi possível escrever o arquivo. text_editor.cut = Cortar text_editor.paste = Colar shortcuts_dialog.quick_search.start_search = Tecle qualquer caractere para iniciar uma busca rápida shortcuts_dialog.quick_search.cancel_search = Cancelar busca rápida shortcuts_dialog.quick_search.remove_last_char = Remover o último caractere da cadeia utilizada na busca rápida shortcuts_dialog.quick_search.jump_to_previous = Pular para o resultado anterior da busca rápida shortcuts_dialog.quick_search.jump_to_next = Pular para o próximo resultado da busca rápida shortcuts_dialog.quick_search.mark_jump_next = Marcar/Desmarcar arquivo corrente e pular para o próximo resultado da busca theme_editor.title = Editor de tema theme_editor.folder_tab = Painel de diretório theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Histórico do Shell theme_editor.statusbar_tab = Barra de status theme_editor.free_space = Espaço livre theme_editor.free_space.ok = OK theme_editor.free_space.warning = Alerta theme_editor.free_space.critical = Crítico theme_editor.locationbar_tab = Barra de endereço theme_editor.editor_tab = Editor de arquivos theme_editor.font = Fonte theme_editor.active_panel = Ativo theme_editor.inactive_panel = Inativo theme_editor.general = Geral theme_editor.could_not_save_theme = Não foi possível salvar o tema %1 theme_editor.border = Borda theme_editor.background = Fundo theme_editor.alternate_background = Cor de fundo alternativa theme_editor.unfocused_background = Fundo (sem o foco) theme_editor.quick_search.unmatched_file = Arquivo não encontrado theme_editor.text = Texto theme_editor.progress = Progresso theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (sem o foco) theme_editor.selected = Selecionado theme_editor.selected_unfocused = Selecionado (sem o foco) theme_editor.color = Cor theme_editor.colors = Cores theme_editor.plain_file = Arquivo normal theme_editor.marked_file = Arquivo marcado theme_editor.hidden_file = Arquivo oculto theme_editor.folder = Diretório theme_editor.archive_file = Arquivar theme_editor.symbolic_link = Link simbólico theme_editor.header = Cabeçalho theme_editor.item = Item command_bar_customize_dialog.available_actions = Ações disponíveis command_bar_customize_dialog.modifier = Modificador prefs_dialog.title = Preferências prefs_dialog.general_tab = Geral prefs_dialog.day = Dia prefs_dialog.month = Mês prefs_dialog.year = Ano prefs_dialog.language = Idioma (requer reiniciar programa) prefs_dialog.date_time = Formato da data e hora prefs_dialog.time = Hora prefs_dialog.date = Data prefs_dialog.date_separator = Separador prefs_dialog.time_12_hour = Formato 12 horas prefs_dialog.time_24_hour = Formato 24 horas prefs_dialog.show_seconds = Exibir segundos prefs_dialog.show_century = Exibir centésimos prefs_dialog.check_for_updates_on_startup = Verificar por atualizações quando iniciar prefs_dialog.show_splash_screen = Show splash screen prefs_dialog.folders_tab = Diretórios prefs_dialog.startup_folders = Diretórios de início prefs_dialog.left_folder = Diretório esquerdo prefs_dialog.right_folder = Diretório direito prefs_dialog.last_folder = Último diretório visitado prefs_dialog.custom_folder = Diretório personalizado prefs_dialog.show_hidden_files = Exibir arquivos ocultos prefs_dialog.show_ds_store_files = Exibir arquivos .DS_Store prefs_dialog.show_system_folders = Exibir diretórios de sistema prefs_dialog.compact_file_size = Arendondar o tamanho dos arquivos exibidos prefs_dialog.follow_symlinks_when_cd = Follow symlinks when changing current directory prefs_dialog.appearance_tab = Aparência prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Tamanho dos ícones prefs_dialog.toolbar_icons = Barra de ferramentas prefs_dialog.command_bar_icons = Barra de comandos prefs_dialog.file_icons = Tipos de arquivos prefs_dialog.use_system_file_icons = Utilisar arquivos de ícones do sistema prefs_dialog.use_system_file_icons.always = Sempre prefs_dialog.use_system_file_icons.never = Nunca prefs_dialog.use_system_file_icons.applications = Somente para aplicações prefs_dialog.edit_current_theme = Editar tema atual... prefs_dialog.themes = Temas prefs_dialog.import_theme = Importar tema prefs_dialog.import_look_and_feel = Importar aparência (look & feel) prefs_dialog.no_look_and_feel = Nenhuma aparência (look & feel) foi encontrada. prefs_dialog.error_in_import = Erro ao importar o tema %1 prefs_dialog.cannot_read_theme = Não foi possível carregar o tema a partir do arquivo %1 prefs_dialog.export_theme = Exportar %1 prefs_dialog.import = Importar prefs_dialog.export = Exportar prefs_dialog.theme_type = Tipo: %1 prefs_dialog.delete_theme = Remover permanentemente o tema %1 ? prefs_dialog.delete_look_and_feel = Remover completamente a aparência (look & feel) %1 ? prefs_dialog.rename_failed = Falha ao renomear tema %1 prefs_dialog.xml_file = Arquivo XML prefs_dialog.jar_file = Arquivo JAR prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Parâmetros de saída de email prefs_dialog.mail_name = Seu nome prefs_dialog.mail_address = Seu endereço de email prefs_dialog.mail_server = Servidor SMTP prefs_dialog.misc_tab = Miscelânia prefs_dialog.use_brushed_metal = Usar aparência de 'brushed metal' (requer reiniciar) prefs_dialog.confirm_on_quit = Pedir confirmação ao sair prefs_dialog.default_shell = Usar prompt de comando padrão do sistema prefs_dialog.custom_shell = Usar prompt de comando personalizado prefs_dialog.shell_encoding = Codificação do Shell prefs_dialog.auto_detect_shell_encoding = Auto-detectar prefs_dialog.enable_bonjour_discovery = Abilitar Bonjour service discovery prefs_dialog.enable_system_notifications = Ativar notificações do sistema debug_console_dialog.level = Nível unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1m duration.years = %1a theme.custom_theme = Tema personalizado theme.custom = Customizado theme.built_in = Pré-instalado theme.add_on = Complementos theme.current = Atual theme_could_not_be_loaded = Ocorreu um erro ao tentar carregar este tema. setup.title = Bem-vindo ao trolCommander setup.intro = Por favor, selecione como o trolCommander deve se comportar. setup.look_and_feel = Selecione seu Look & feel setup.theme = Selecione seu tema font_chooser.font_size = Tamanho font_chooser.font_bold = Negrito font_chooser.font_italic = Itálico color_chooser.red = Vermelho color_chooser.green = Verde color_chooser.blue = Azul color_chooser.hue = Tonalidade color_chooser.brightness = Brilho color_chooser.swatches = Swatches color_chooser.saturation = Saturação color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Recente color_chooser.alpha = Transparência alpha color_chooser.title = Escolha uma cor batch_rename_dialog.mask = Modelo de renomeação batch_rename_dialog.search_replace = Pesquisar e Substituir batch_rename_dialog.search_for = Pesquisar por batch_rename_dialog.replace_with = Substituir por batch_rename_dialog.counter = Contador batch_rename_dialog.start_at = Começar batch_rename_dialog.step_by = Incrementar batch_rename_dialog.format = Formatar batch_rename_dialog.upper_lower_case = Maiúsculo/Minúsculo batch_rename_dialog.no_change = Inalterado batch_rename_dialog.lower_case = minúsculo batch_rename_dialog.upper_case = MAIÚSCULO batch_rename_dialog.first_upper = Primeira letra em maiúsculo batch_rename_dialog.word = Primeira de cada palavra batch_rename_dialog.old_name = Nome antigo batch_rename_dialog.new_name = Novo nome batch_rename_dialog.block_name = Preservar batch_rename_dialog.range = Intervalo batch_rename_dialog.proceed_renaming = %1 arquivos de %2 serão renomeados. Continuar? batch_rename_dialog.duplicate_names = Nomes duplicados! parent_folders_quick_list.empty_message = Localização atual não possue superior recent_locations_quick_list.empty_message = Nenhum local recente recent_executed_files_quick_list.empty_message = Nenhum arquivo executado recentemente #move_dialog.rename_description = Renomear arquivo para #theme_editor.shell_font = Fonte do shell #theme_editor.history_font = Fonte do histórico #theme_editor.shell_colors = Cor do prompt de comandos (terminal) #theme_editor.history_colors = Cores do histórico #ToggleHiddenFiles.hide = Esconder arquivos ocultos #auth_dialog.error_was = O erro foi: %1 #table.hide_column = Ocultar colunas #delete.symlink_warning_title = Link simbólico encontrado #delete.symlink_warning = Este arquivo parece ser um link simbólico:\n\n Arquivo: %1\n Linkado ao: %2\n\nRemover somente o link simbólico or\nSeguir link simbólico e remover diretório (cuidado) ? #delete.delete_link_only = Remover link #delete.delete_linked_folder = Remover diretório #Unpack.label = Descompactar arquivos #Pack.label = Compactar arquivos ================================================ FILE: src/main/resources/dictionary_ro_RO.properties ================================================ ok = OK yes = Da no = Nu cancel = Renunță edit = Editează close = Închide reset = Reinițializează rename = Redenumește apply = Aplică change = Schimbă save = Salvează dont_save = Nu salva replace = Înlocuiește dont_replace = Nu înlocui delete = Șterge skip = Treci mai departe skip_all = Sari peste toate retry = Încearca din nou resume = Continuă overwrite = Suprascrie overwrite_if_older = Suprascrie dacă e mai vechi duplicate = Duplică apply_to_all = Aplică la toate copy = Copiază move = Mută pack = Arhivează unpack = Dearhivează download = Descarcă split = Împarte combine = Combină browse = Navighează ask = Întreabă stop = Stop pause = Pauză quick_search = Căutare rapidă file_manager = Manager de fișiere create = Crează creating_file = Creez %1 choose = Alege customize = Personalizează choose_folder = Alege un director login = Login password = Parolă user = Utilizator encoding = Codificare preferred_encodings = Codificări preferate license = Licență name = Nume size = Dimensiune date = Dată extension = Extensie permissions = Permisiuni owner = Proprietar group = Grup location = Locație untitled = Fara nume source = Sursă destination = Destinație recurse_directories = Prelucrează directoarele recursiv go_to = Mergi la example = Exemplu preview = Previzualizare comment = Comentarii sample_text = Text exemplu nb_files = %1 fișier(e) nb_folders = %1 directo(a)r(e) loading = Încărcare în curs... this_operation_cannot_be_undone = Această operațiune este definitivă și irevocabilă. remove = Elimină details = Detalii warning = Avertisment error = Eroare generic_error = O eroare a apărut in timp ce executam operațiunea cerută folder_does_not_exist = Acest director nu există sau nu este disponibil. this_folder_does_not_exist = Acest director nu exista sau nu este disponibil: %1 this_file_does_not_exist = Acest fișier nu există sau nu este disponibil: %1 invalid_path = Cale greșită: %1 directory_already_exists = Directorul %1 exista deja. file_exists_in_destination = Fișierul există deja la destinație source_parent_of_destination = Încercare de a transfera un director într-un subdirector al său cannot_read_file = Nu pot citi fișierul %1 cannot_write_file = Nu pot scrie fișierul %1 cannot_create_folder = Nu pot crea directorul %1 cannot_read_folder = Nu pot citi conținutul directorului %1 cannot_delete_file = Nu pot șterge fișierul %1 cannot_delete_folder = Nu pot șterge directorul %1 error_while_transferring = Eroare la transferul fișierului %1 same_source_destination = Directoarele sursă și destinație coincid. file_already_exists = %1 există deja, vreți să îl înlocuiți? write_error = Eroare la scriere read_error = Eroare la citire integrity_check_error = Controlul integrității a eșuat: sursa și destinația sunt diferite startup_error = O eroare a prevenit trolCommander să pornească. permissions.read = Citire permissions.write = Scriere permissions.executable = Executare permissions.group = Grup permissions.other = Altele permissions.octal_notation = Notație în baza opt action_categories.all = Toate action_categories.navigation = Navigare action_categories.selection = Selectare action_categories.view = Vizualizare action_categories.file_operations = Operații cu fișiere action_categories.windows = Ferestre Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Adaugă la favorite AddBookmark.tooltip = Adauga directorul curent la favorite BatchRename.label = Redenumire multiplă EditBookmarks.label = Modifică favoritele ExploreBookmarks.label = Explorează favoritele EditCredentials.label = Schimbă identitatea ChangeLocation.label = Schimbă calea actuala ChangeDate.label = Schimbă data ChangeDate.tooltip = Schimbă data fișierelor selectate ChangePermissions.label = Schimbă permisiuni ChangePermissions.tooltip = Schimbă permisiunile fișierelor selectate CheckForUpdates.label = Actualizează programul CompareFolders.label = Compară directoarele ConnectToServer.label = Conectare la un server ConnectToServer.tooltip = Conectare la un server la distanță View.label = Vezi InternalView.label = Vizualizează (intern) View.tooltip = Vezi fișierul selectat InternalEdit.label = Editează (internal) Edit.tooltip = Editează fișierul selectat Copy.tooltip = Copiază fișierele selectate LocalCopy.label = Copiază local LocalCopy.tooltip = Copiază fișierele selectate în directorul curent Move.tooltip = Mută fișierele selectate Rename.tooltip = Redenumește fișierul selectat Mkdir.label = Director nou Mkdir.tooltip = Crează un director nou în directorul curent Mkfile.label = Fișier nou Mkfile.tooltip = Crează un fișier nou în directorul curent Delete.tooltip = Șterge fișierele selectate PermanentDelete.label = Șterge definitiv PermanentDelete.tooltip = Șterge definitiv fișierele selectate fără a folosi coșul de gunoi Refresh.label = Actualizează Refresh.tooltip = Actualizează directorul curent CloseWindow.label = Închide fereastră CloseWindow.tooltip = Închide această fereastră CopyFileNames.label = Copiază nume CopyFilePaths.label = Copiază cale/căi CopyFilesToClipboard.label = Copiază fișier(e) PasteClipboardFiles.label = Lipește fișier(e) Email.label = Trimite prin email Email.tooltip = Trimite fișierele selectate prin email GoBack.label = Înapoi GoBack.tooltip = Mergi înapoi în directorul anterior GoForward.label = Înainte GoForward.tooltip = Mergi în directorul următor GoToHome.label = Mergi în directorul utilizatorului GoToParent.label = Părinte GoToParent.tooltip = Mergi în directorul părinte GoToParentInOtherPanel.label = Mergi în directorul părinte în cealaltă fereastră GoToParentInBothPanels.label = Mergi în directorul părinte în ambele ferestre GoToRoot.label = Mergi în directorul rădăcină SortByName.label = Sortează după nume SortByDate.label = Sortează după dată SortBySize.label = Sortează după dimensiune SortByExtension.label = Sortează după extensie SortByPermissions.label = Sortează după permisiuni SortByOwner.label = Sortează după proprietar SortByGroup.label = Sortează după grup MarkGroup.label = Selectează fișiere MarkGroup.tooltip = Selectează un grup de fișiere UnmarkGroup.label = Deselectează fișiere UnmarkGroup.tooltip = Deselectează un grup de fișiere MarkAll.label = Selecteză tot UnmarkAll.label = Deselectează tot MarkSelectedFile.label = Selectează/Deselectează MarkSelectedFile.tooltip = Selectează/Deselectează fișier MarkNextBlock.label = Selectează un bloc în jos MarkPreviousBlock.label = Selectează un bloc în sus MarkNextRow.label = Selectează un rând în jos MarkPreviousRow.label = Selectează un rând în sus MarkNextPage.label = Selectează fișierele din pagina următoare MarkPreviousPage.label = Selectează fișierele din pagina anterioară MarkToFirstRow.label = Selectează fișierele până la început MarkToLastRow.label = Selectează fișierele până la sfârșit MarkExtension.label = Selectează aceeași extensie InvertSelection.label = Inversează selectarea SwapFolders.label = Interschimbă ferestrele SwapFolders.tooltip = Interschimbă fereastra stângă și cea dreaptă SetSameFolder.label = Alege același director SetSameFolder.tooltip = Alege același director pentru fereastra stângă și dreaptă NewWindow.label = Fereastră nouă NewWindow.tooltip = Deschide o fereastră nouă Open.label = Deschide Open.tooltip = Deschide director / Deschide arhivă / Execută OpenNatively.label = Deschide cu aplicația asociată OpenNatively.tooltip = Deschide fișierul selectat folosind aplicația asociată OpenInOtherPanel.label = Deschide în cealaltă fereastră OpenInBothPanels.label = Deschide în ambele ferestre RevealInDesktop.label = Arată în %1 RunCommand.label = Execută comandă RunCommand.tooltip = Execută o comandă în directorul curent Pack.tooltip = Arhivează fișierele selectate Unpack.tooltip = Dearhivează fișierele arhivate selectate ShowFileProperties.label = Proprietăți ShowFileProperties.tooltip = Afișează proprietățile fișierelor selectate ShowPreferences.label = Preferințe ShowPreferences.tooltip = Configurează trolCommander ShowServerConnections.label = Afișează conexiunile deschise Quit.label = Închide ReverseSortOrder.label = Inversează ordinea ToggleAutoSize.label = Dimensiune automată la coloane Stop.label = Oprește schimbarea de director ToggleColumn.show = Afișează coloana cu %1 ToggleColumn.hide = Ascunde coloana cu %1 ToggleCommandBar.show = Arată bara de comenzi ToggleCommandBar.hide = Ascunde bara de comenzi ToggleToolBar.show = Arată bara de unelte ToggleToolBar.hide = Ascunde bara de unelte CustomizeCommandBar.label = Personalizează bara de comande ToggleStatusBar.show = Arată bara de stare ToggleStatusBar.hide = Ascunde bara de stare ToggleShowFoldersFirst.label = Arată directoarele la început ToggleTree.label = Arată arbore directoare PopupLeftDriveButton.label = Schimbă directorul din stânga PopupRightDriveButton.label = Schimbă directorul din dreapta RecallPreviousWindow.label = Schimbă la fereastra anterioară RecallNextWindow.label = Schimbă la fereastra următoare RecallWindow.label = Schimbă la fereastra #%1 BringAllToFront.label = Toate ferestrele în prim plan SwitchActiveTable.label = Interschimbă fereastra stângă și cea dreaptă SelectNextBlock.label = Coboară un bloc SelectPreviousBlock.label = Urcă un bloc SelectNextPage.label = Coboară o pagină SelectPreviousPage.label = Urcă o pagină SelectNextRow.label = Coboară un rând SelectPreviousRow.label = Urcă un rând SelectFirstRow.label = Selectează primul fișier din directorul curent SelectLastRow.label = Selectează ultimul fișier din directorul curent SplitEqually.label = Împarte în mod egal SplitVertically.label = Împarte vertical SplitHorizontally.label = Împarte orizontal ShowKeyboardShortcuts.label = Combinații de taste GoToWebsite.label = Vizitează situl Web GoToForums.label = Vizitează forumul ReportBug.label = Raportează un bug Donate.label = Fă o donație ShowAbout.label = Despre trolCommander OpenTrash.label = Deschide coșul de gunoi EmptyTrash.label = Golește coșul de gunoi CalculateChecksum.label = Calculează suma de control MaximizeWindow.label = Maximizează MinimizeWindow.label = Minimizează GoToDocumentation.label = Documentație online ShowParentFoldersQL.label = Directoarele părinte ShowRecentLocationsQL.label = Locații recente ShowRecentExecutedFilesQL.label = Fișiere executate recent SplitFile.tooltip = Împarte un fișier în mai multe bucăți CombineFiles.tooltip = Combină un fișier împărțit în bucăți pentru a recrea originalul ShowDebugConsole.label = Consolă pentru debug FocusPrevious.label = Selectează componenta precedentă FocusNext.label = Selectează component următoare file_menu = Fișiere file_menu.open_with = Deschide cu mark_menu = Selectează view_menu = Vizualizare view_menu.show_hide_columns = Ascunde/Arată coloane go_menu = Mergi bookmarks_menu = Favorite bookmarks_menu.no_bookmark = Nici un director favorit quick_lists_menu = Liste rapide window_menu = Fereastră help_menu = Ajutor status_bar.selected_files = %1 din %2 selectate status_bar.connecting_to_folder = În curs de conectare la un director, apasă ESCAPE pentru a anula. status_bar.volume_free = Liber: %1 status_bar.volume_capacity = Capacitate: %1 shortcuts_panel.title = Scurtături shortcuts_panel.restore_defaults = Revine la configurația inițială shortcuts_panel.show = Arată shortcuts_panel.default_message = Apăsați Enter sau dați dublu-clic pe scurtătura pe care doriți să o editați shortcuts_table.action_description = Descrierea acțiunii shortcuts_table.shortcut = Scurtătură shortcuts_table.alternate_shortcut = Scurtătură alternativă shortcuts_table.type_in_a_shortcut = Tipul de scurtătură command_bar_dialog.help = Trageți butoane pentru a personaliza bara de comenzi table.folder_access_error_title = Eroare la accesul la director table.folder_access_error = Nu pot citi conținutul directorului table.download_or_browse = Vreți să navigați sau să descărcați acest fișier? version_dialog.no_new_version_title = Nici o versiune nouă version_dialog.no_new_version = Felicitări, aveți deja versiunea cea mai actuală. version_dialog.new_version_title = O versiune nouă este disponibilă version_dialog.new_version = O versiune nouă de trolCommander este disponibilă. version_dialog.new_version_url = O versiune nouă de trolCommander este disponibilă la: %1. version_dialog.not_available_title = Server indisponibil version_dialog.not_available = Informația despre versiuni nu a putut fi accesată. version_dialog.install_and_restart = Instalează și repornește version_dialog.preparing_for_update = Pregătire pentru actualizare... quit_dialog.title = Închide trolCommander quit_dialog.desc = Aveți %1 ferestre deschise. Sunteți sigur ca doriti sa închideți trolCommander? quit_dialog.show_next_time = Arată și data viitoare destination_dialog.file_exists_action = Acțiune implicită când fișierul există deja destination_dialog.verify_integrity = Verifică integritatea datelor destination_dialog.skip_errors = Ignoră erorile file_collision_dialog.title = Coliziune între fișiere rename_dialog.new_name = Nume nou copy_dialog.destination = Copiază fisierul(ele) la copy_dialog.error_title = Eroare la copiere copy_dialog.copying = În curs de copiere copy_dialog.copying_file = În curs de copiere %1 pack_dialog.packing = În curs de arhivare pack_dialog.packing_file = În curs de arhivare %1 pack_dialog.error_title = Eroare la arhivare pack_dialog_description = Adaugă fișierele selectate la pack_dialog.archive_format = Formatul arhivei unpack_dialog.destination = Dearhivează fisierul(ele) selectat(e) la unpack_dialog.error_title = Eroare la dearhivare unpack_dialog.unpacking = În curs de dearhivare unpack_dialog.unpacking_file = În curs de dearhivare %1 optimizing_archive = Optimizez arhiva %1 error_while_optimizing_archive = Eroare la optimizarea arhivei %1 move_dialog.move_description = Mută în move_dialog.error_title = Eroare la mutare move_dialog.moving = În curs de mutare move_dialog.moving_file = În curs de mutare %1 download_dialog.description = Descarcă fișierul în download_dialog.error_title = Eroare la descărcare download_dialog.downloading = În curs de descărcare download_dialog.downloading_file = În curs de descărcare %1 mkfile_dialog.allocate_space = Alocă spațiu delete_dialog.permanently_delete.confirmation = Șterg definitiv fișierul(ele) selectat(e) ? delete_dialog.move_to_trash.confirmation = Șterge fișierele selectate ? delete_dialog.move_to_trash.confirmation_details = Fișierele selectate vor fi aruncate la gunoi. delete_dialog.move_to_trash.option = Aruncă la gunoi delete_dialog.move_to_trash.failed = Unul sau mai multe fișiere nu au putut fi mutate la gunoi. delete_dialog.deleting = În curs de ștergere delete_dialog.error_title = Eroare la ștergere delete.deleting_file = În curs de ștergere %1 email_dialog.prefs_not_set_title = Email-ul nu e configurat email_dialog.prefs_not_set = Mai întâi trebuie să configurați email-ul email_dialog.from = De la email_dialog.to = Pentru email_dialog.subject = Subiect email_dialog.send = Trimite email_dialog.error_title = Eroare la trimiterea fișierelor prin email email_dialog.read_error = Nu pot citi fișierele din subdirectoare. email_dialog.sending = Fișiere în curs de trimitere email.sending_file = În curs de trimitere %1 email.connecting_to_server = În curs de conectare la %1 email.server_unavailable = Nu pot contacta serverul de mail %1, verificați configurația pentru email și încercați din nou mai târziu. email.connection_closed = Conexiunea întreruptă de server, email-ul nu a fost trimis. email.goodbye_failed = Eroare la închiderea conexiunii, se poate ca email-ul sa nu fi fost trimis. email.send_file_error = Nu pot trimite fișierul %1, email-ul nu a fost trimis. split_file_dialog.error_title = Eroare la împărțirea fișierul split_file_dialog.file_to_split = Fișierul care trebuie împărțit split_file_dialog.target_directory = Directorul destinație split_file_dialog.part_size = Dimensiunea fiecărei părți split_file_dialog.parts = Numărul de părți split_file_dialog.generate_CRC = Generează sumă de control split_file_dialog.max_parts = Numărul maxim de părți este %1 split_file_dialog.auto = Automatic split_file_dialog.insert_new_media = Introduceți următorul disc combine_files_dialog.error_title = Eroare la combinare combine_files_job.no_crc_file = Combinarea s-a terminat cu succes. Fără sumă de control. combine_files_job.crc_read_error = Eroare la citirea sumei de control. combine_files_job.crc_check_failed = Suma de control nu se potrivește: așteptată %2, găsită %1 combine_files_job.crc_ok = Combinarea a reușit. Suma de control este OK. file_selection_dialog.mark = Selectează file_selection_dialog.unmark = Deselectează file_selection_dialog.mark_description = Selectează fișierele ale căror nume file_selection_dialog.unmark_description = Deselectează fișierele ale căror nume file_selection_dialog.case_sensitive = Respectă capitalizarea file_selection_dialog.include_folders = Inclusiv directoare progress_dialog.starting = Încep să transfer... progress_dialog.transferred = Am transferat %1 la %2 progress_dialog.elapsed_time = Durata progress_dialog.advanced = Avansat progress_dialog.current_speed = Viteza actuală progress_dialog.limit_speed = Limitare de viteză progress_dialog.close_when_finished = Închide când e gata progress_dialog.processing_files = Fișierele sunt procesate progress_dialog.processing_file = Procesez %1 progress_dialog.verifying_file = Verificare %1 progress_dialog.job_finished = Sarcină îndeplinită progress_dialog.job_error = Eroare de proces properties_dialog.file_properties = %1 Proprietăți properties_dialog.contents = Conținut properties_dialog.calculating = calculez... calculate_checksum_dialog.checksum_algorithm = Algoritm pentru suma de control calculate_checksum_dialog.temporary_file = Fișier temporar change_date_dialog.now = Acum change_date_dialog.specific_date = Dară specifică run_dialog.run_command_description = Execută în directorul curent run_dialog.run_in_home_description = Execută în directorul utilizatorului run_dialog.command_output = Rezultatul comenzii run_dialog.run = Execută run_dialog.clear_history = Șterge istoria server_connect_dialog.server_type = Tipul conexiunii server_connect_dialog.server = Server server_connect_dialog.share = Partajează server_connect_dialog.domain = Domeniu server_connect_dialog.username = Nume utilizator server_connect_dialog.initial_dir = Directorul inițial server_connect_dialog.port = Port server_connect_dialog.server_url = URL-ul serverului server_connect_dialog.http_url = URL-ul sitului Web server_connect_dialog.connect = Conectare server_connect_dialog.protocol = Protocol server_connect_dialog.nfs_version = Versiunea de NFS server_connect_dialog.private_key = Cheia privată server_connect_dialog.passphrase = Frază secretă ftp_connect.passive_mode = Folosește modul pasiv ftp_connect.anonymous_user = Utilizator anonim ftp_connect.nb_connection_retries = Numărul de încercări de conectare ftp_connect.retry_delay = Durata între încercări (în secunde) http_connect.basic_authentication = HTTP Basic Authentication (optional) server_connections_dialog.disconnect = Deconectează server_connections_dialog.connection_busy = Ocupată server_connections_dialog.connection_idle = Inactivă bonjour.bonjour_services = Servicii Bonjour bonjour.no_service_discovered = Nici un serviciu descoperit bonjour.bonjour_disabled = Bonjour dezactivat auth_dialog.title = Autentificare auth_dialog.desc = Vă rog să întroduceți parola auth_dialog.server = Server auth_dialog.store_credentials = Salvează numele de utilizator și parola (protecție redusă) auth_dialog.connect_as = Connectează ca auth_dialog.authentication_failed = Autentificare nereușită sortable_list.move_up = Mută în sus sortable_list.move_down = Mută în jos add_bookmark_dialog.add = Adaugă edit_bookmarks_dialog.new = Nou file_viewer.view_error_title = Eroare la vizualizare file_viewer.view_error = Nu pot vizualiza fișierul. file_viewer.file_menu = Fișier file_viewer.close = Închide file_viewer.large_file_warning = Acest fișier ar putea fi prea mare pentru această operațiune. file_viewer.open_anyway = Deschide oricum text_viewer.edit = Editează text_viewer.copy = Copiază text_viewer.select_all = Selectează tot text_viewer.find = Caută text_viewer.find_next = Caută urmatoarea apariție text_viewer.find_previous = Caută apariția precedentă text_viewer.binary_file_warning = Se pare că acest fișier este binar image_viewer.controls_menu = Control image_viewer.zoom_in = Mărește image_viewer.zoom_out = Micșorează file_editor.edit_error_title = Eroare la editare file_editor.edit_error = Nu pot edita fișierul. file_editor.save = Salvează file_editor.save_as = Salvează ca ... file_editor.save_warning = Salvez modificările efectuate înainte de a închide editorul? file_editor.cannot_write = Nu pot scrie fișierul. text_editor.cut = Taie text_editor.paste = Lipește shortcuts_dialog.quick_search.start_search = Introduceți orice caracter pentru a începe o căutare rapidă shortcuts_dialog.quick_search.cancel_search = Anulează căutarea rapidă shortcuts_dialog.quick_search.remove_last_char = Șterge ultimul caracter introdus la o cautare rapidă shortcuts_dialog.quick_search.jump_to_previous = Mergi la rezultatul anterior al căutării rapide shortcuts_dialog.quick_search.jump_to_next = Mergi la rezultatul următor al căutării rapide shortcuts_dialog.quick_search.mark_jump_next = Selectează/Deselectează fișierul curent si mergi la următorul rezultat al căutării theme_editor.title = Editor de teme theme_editor.folder_tab = Panoul cu directoare theme_editor.shell_tab = Linie de comandă theme_editor.shell_history_tab = Istoria terminalului theme_editor.statusbar_tab = Bara de stare theme_editor.free_space = Spațiu liber theme_editor.free_space.ok = OK theme_editor.free_space.warning = Atenție theme_editor.free_space.critical = Situație critică theme_editor.locationbar_tab = Bara de locație theme_editor.editor_tab = Editorul de fișiere theme_editor.font = Font theme_editor.active_panel = Activ theme_editor.inactive_panel = Inactiv theme_editor.general = General theme_editor.could_not_save_theme = Nu am reușit să salvez tema %1 theme_editor.border = Margine theme_editor.background = Fond theme_editor.alternate_background = Fond alternativ theme_editor.unfocused_background = În fundal (fără focus) theme_editor.quick_search.unmatched_file = Fișiere care nu corespund theme_editor.text = Text theme_editor.progress = Progres theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (fără focus) theme_editor.selected = Selectat theme_editor.selected_unfocused = Selectat (fără focus) theme_editor.color = Culoare theme_editor.colors = Culori theme_editor.plain_file = Fișier normal theme_editor.marked_file = Fișier selectat theme_editor.hidden_file = Fișier ascuns theme_editor.folder = Director theme_editor.archive_file = Arhivă theme_editor.symbolic_link = Legătură simbolică theme_editor.header = Antet theme_editor.item = Element command_bar_customize_dialog.available_actions = Acțiuni disponibile command_bar_customize_dialog.modifier = Modifică prefs_dialog.title = Preferințe prefs_dialog.general_tab = Generale prefs_dialog.day = Zi prefs_dialog.month = Lună prefs_dialog.year = An prefs_dialog.language = Limbă (necesită repornirea programului) prefs_dialog.date_time = Formatul datei și orei prefs_dialog.time = Ora prefs_dialog.date = Dată prefs_dialog.date_separator = Separator prefs_dialog.time_12_hour = Format 12 ore prefs_dialog.time_24_hour = Format 24 ore prefs_dialog.show_seconds = Afișează secunde prefs_dialog.show_century = Afișează secol prefs_dialog.check_for_updates_on_startup = Caută versiuni mai recente la pornire prefs_dialog.show_splash_screen = Arată ecran de pornire prefs_dialog.folders_tab = Directoare prefs_dialog.startup_folders = Director inițial prefs_dialog.left_folder = Directorul din stânga prefs_dialog.right_folder = Directorul din dreapta prefs_dialog.last_folder = Ultimul director vizitat prefs_dialog.custom_folder = Director definit de utilizator prefs_dialog.show_hidden_files = Afișează fișierele ascunse prefs_dialog.show_ds_store_files = Afișează fișierele .DS_Store prefs_dialog.show_system_folders = Afișează directoarele system prefs_dialog.compact_file_size = Rotunjește dimensiunile afișate ale fișierelor prefs_dialog.follow_symlinks_when_cd = Urmează legăturile simbolice la schimbarea directorului curent prefs_dialog.appearance_tab = Înfățișare prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = Dimensiunea iconițelor prefs_dialog.toolbar_icons = Bara de unelte prefs_dialog.command_bar_icons = Bara de comenzi prefs_dialog.file_icons = Tipuri de fișiere prefs_dialog.use_system_file_icons = Folosește iconițele sistemului prefs_dialog.use_system_file_icons.always = Întotdeauna prefs_dialog.use_system_file_icons.never = Niciodata prefs_dialog.use_system_file_icons.applications = Numai pentru aplicații prefs_dialog.edit_current_theme = Modifică tema curentă... prefs_dialog.themes = Teme prefs_dialog.import_theme = Importă temă prefs_dialog.import_look_and_feel = Importă look & feel prefs_dialog.no_look_and_feel = Acest look & feel nu a fost gasit. prefs_dialog.error_in_import = Eroare la importarea temei %1. prefs_dialog.cannot_read_theme = Nu am putut încărca tema din fișierul %1 prefs_dialog.export_theme = Exportare %1 prefs_dialog.import = Importă prefs_dialog.export = Export prefs_dialog.theme_type = Tipul: %1 prefs_dialog.delete_theme = Sterg definitiv tema %1 ? prefs_dialog.delete_look_and_feel = Șterge look & feel-ul %1 definitiv? prefs_dialog.rename_failed = Nu am reușit să redenumesc tema %1 prefs_dialog.xml_file = Fișier XML prefs_dialog.jar_file = Fișier JAR prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Parametrii email-urilor trimise prefs_dialog.mail_name = Numele dumneavoastră prefs_dialog.mail_address = Adresa dumneavoastră de email prefs_dialog.mail_server = Serverul SMTP prefs_dialog.misc_tab = Altele prefs_dialog.use_brushed_metal = Folosește "look"-ul "brushed metal" (necesită repornirea programului) prefs_dialog.confirm_on_quit = Afișează dialogul de confirmare la ieșire prefs_dialog.default_shell = Folosește terminalul implicit pe sistem prefs_dialog.custom_shell = Folosește un terminal anume prefs_dialog.shell_encoding = Codificarea terminalului prefs_dialog.auto_detect_shell_encoding = Detectează automat prefs_dialog.enable_bonjour_discovery = Activează descoperirea serviciilor Bonjour prefs_dialog.enable_system_notifications = Activează sistemul de notificare debug_console_dialog.level = Nivel unit.byte = octet unit.bytes = octeți unit.bytes_short = o unit.kb = Ko unit.mb = Mo unit.gb = Go unit.tb = To unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1z duration.months = %1l duration.years = %1a theme.custom_theme = Temă personalizată theme.custom = Personalizată theme.built_in = Predefinită theme.add_on = Adițională theme.current = curentă theme_could_not_be_loaded = O eroare a apărut la încărcarea acestei teme. setup.title = Bine ați venit în trolCommander setup.intro = Vă rugăm să alegeți modul în care trolCommander se va comporta. setup.look_and_feel = Alegeți aspectul setup.theme = Alegeți o temă. font_chooser.font_size = Dimensiune font_chooser.font_bold = Îngroșat font_chooser.font_italic = Cursiv color_chooser.red = Roșu color_chooser.green = Verde color_chooser.blue = Albastru color_chooser.hue = Tonalitate color_chooser.brightness = Luminozitate color_chooser.swatches = Mostre color_chooser.saturation = Saturație color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Recent color_chooser.alpha = Transparență alpha color_chooser.title = Alegeți o culoare batch_rename_dialog.mask = Șablon redenumire batch_rename_dialog.search_replace = Caută & Înlocuiește batch_rename_dialog.search_for = Caută batch_rename_dialog.replace_with = Înlocuiește cu batch_rename_dialog.counter = Numără batch_rename_dialog.start_at = Începe la batch_rename_dialog.step_by = Increment batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Capitalizare batch_rename_dialog.no_change = Neschimbat batch_rename_dialog.lower_case = minuscule batch_rename_dialog.upper_case = MAJUSCULE batch_rename_dialog.first_upper = Prima literă majusculă batch_rename_dialog.word = Prima Literă Din Fiecare Cuvânt batch_rename_dialog.old_name = Numele vechi batch_rename_dialog.new_name = Numele nou batch_rename_dialog.block_name = Păstrează batch_rename_dialog.range = Interval batch_rename_dialog.proceed_renaming = %1 din %2 fișiere vor fi redenumite. Vreți să continuați? batch_rename_dialog.duplicate_names = Nume care se repetă! parent_folders_quick_list.empty_message = Locația curentă nu are părinte recent_locations_quick_list.empty_message = Nici o locație recentă recent_executed_files_quick_list.empty_message = Nici un fișier executat recent #mkdir_dialog.description = Crează director #mkfile_dialog.description = Crează un nou fișier gol #done = Închide #move_dialog.rename_description = Redenumește fișierul la #theme_editor.shell_font = Font linie de comandă #theme_editor.history_font = Font pentru istorie #theme_editor.shell_colors = Culori pentru shell #theme_editor.history_colors = Culori pentru istorie #ToggleHiddenFiles.hide = Nu arăta fișierele ascunse #auth_dialog.error_was = Eroare: %1 #table.hide_column = Ascunde coloană #delete.symlink_warning_title = Am găsit o legătură #delete.symlink_warning = Acest fișier pare a fi o legătură simbolică (symbolic link):\n\n Fișierul: %1\n Arată spre: %2\n\nȘterg doar legătura sau \nUrmez legătura și șterg directorul destinație (ATENȚIE) ? #delete.delete_link_only = Șterge legătura #delete.delete_linked_folder = Șterge directorul #Unpack.label = Dearhivează fișiere #Pack.label = Arhivează fișiere ================================================ FILE: src/main/resources/dictionary_ru_RU.properties ================================================ ok = OK yes = Да no = Нет cancel = Отмена edit = Редактировать close = Закрыть reset = сброс rename = Переименование apply = Применить change = Изменить save = Сохранить dont_save = Не сохранять replace = Заменить dont_replace = Не заменять delete = Удаление skip = Пропустить skip_all = Пропустить все overwrite_all = Перезаписать все retry = Повторить resume = Продолжить overwrite = Заменить overwrite_if_older = Только старые duplicate = Скопировать apply_to_all = Применить ко всему copy = Копировать move = Перенести pack = Архивация unpack = Разархивация download = Скачать split = Разъединить combine = Соединить browse = Обзор ask = Задать вопрос stop = Прервать pause = Приостановить quick_search = Быстрый поиск file_manager = Файловый менеджер create = Создать creating_file = Создается %1 choose = Выберите customize = Персонализация clean = Очистить search = Поиск choose_folder = Выберите каталог login = Логин password = Пароль user = Пользователь encoding = Кодировка preferred_encodings = Используемые кодировки license = Лицензия name = Имя size = Размер date = Дата extension = Расширение permissions = Права доступа owner = Владелец group = Группа location = Местоположение untitled = Без имени source = Источник destination = Получатель recurse_directories = Обрабатывать подкаталоги go_to = Перейти к example = Пример preview = Предпросмотр comment = Комментарий sample_text = Пример текста nb_files = %1 файл(ов) nb_folders = %1 каталог(ов) loading = Загрузка... title = Заголовок this_operation_cannot_be_undone = Эта операция не может быть отменена remove = Удалить details = Подробнее warning = Предупреждение error = Ошибка files = файлов parent = Предок generic_error = При выполнении запрошенной операции возникла ошибка folder_does_not_exist = Каталог с таким именем не существует или недоступен this_folder_does_not_exist = Каталог %1 не существует или недоступен this_file_does_not_exist = Файл %1 не существует или недоступен failed_to_read_folder = Не могу прочитать текущую эту директорию. invalid_path = Неправильный путь: %1 directory_already_exists = Каталог %1 уже существует. file_exists_in_destination = Файл уже существует source_parent_of_destination = Попытка скопировать каталог в его собственный подкаталог cannot_read_file = Не удается прочитать файл %1 cannot_write_file = Не удается записать файл %1 overwrite_readonly_file = Файл только для чтения: %1 cannot_create_folder = Не удается создать каталог %1. cannot_read_folder = Не удается прочесть содержимое каталога %1 cannot_delete_file = Не удается удалить файл %1 cannot_delete_folder = Не удается создать каталог %1 error_while_transferring = Ошибка при передаче файла %1 same_source_destination = Исходный и конечный каталог - один и тот же file_already_exists = Файл %1 уже существует. Вы хотите его заменить? write_error = Ошибка записи read_error = Ошибка считывания integrity_check_error = Проверка целостности не прошла: файл-источник и файл-приемник не совпадают startup_error = Предстартовая ошибка trolCommander'а. image_size = Размеры изображения cannot_write_symlink=Не могу создать символическую ссылку %1 cannot_write_symlink_already_exists=Ссылка уже существует: %1 cannot_write_symlink_access_denied=Ошибка доступа при создании символической ссылки %1 permissions.read = Чтение permissions.write = Запись permissions.executable = Выполнение permissions.group = Группа permissions.other = Остальные permissions.octal_notation = Цифровое обозначение permissions.user = Пользователь action_categories.all = Все action_categories.navigation = Перемещение action_categories.selection = Выбор action_categories.view = Вид action_categories.file_operations = Файловые операции action_categories.windows = Окна action_categories.tabs = Вкладки action_categories.misc = Прочее action_categories.commands = Пользовательские команды Terminal.label = Терминал Terminal.tooltip = Открыть консоль в текущей директории TerminalPanel.label = Терминал TerminalPanel.tooltip = Открыть/закрыть панель терминала UserMenu.label = Пользовательское меню UserMenu.tooltip = Открыть пользовательское меню UserMenu.press_f4_to_edit_menu = Нажмите F4 для редактирования меню UserMenu.command_not_defined = Команда не задана FindFile.label = Поиск файлов FindFile.tooltip = Поиск файлов AddBookmark.label = Добавить закладку AddBookmark.tooltip = Добавить текущий каталог в закладки BatchRename.label = Групповое переименование EditBookmarks.label = Редактировать закладки ExploreBookmarks.label = Просмотреть закладки EditCredentials.label = Редактировать реквизиты ChangeLocation.label = Перемещение ChangeDate.label = Изменение даты ChangeDate.tooltip = Изменить дату выбранных файлов ChangePermissions.label = Изменение прав доступа ChangePermissions.tooltip = Изменить права доступа выбранных файлов CheckForUpdates.label = Проверить обновления CompareFolders.label = Сравнить каталоги CompareFolderFiles.label = Сравнить файлы CompareFolderFiles.tooltip = Сравнить файлы и отметить изменённые в текущей директории ConnectToServer.label = Соединение с сервером ConnectToServer.tooltip = Соединиться с удаленным сервером TextEditorsList.label = Редактируемые файлы TextEditorsList.tooltip = Показать все редактируемые файлы View.label = Просмотреть ViewAs.label = Просмотреть как InternalView.label = Просмотр (встроенным) CompareFiles.label = Сравнить текстовые файлы CompareFiles.tooltip = Сравнить текстовые файлы (FileMerge) viewer_type.text = Текстовый файл viewer_type.hex = Двоичный файл viewer_type.image = Рисунок viewer_type.pdf = Документ PDF viewer_type.audio = Аудио файл viewer_type.html = Документ HTML View.tooltip = Просмотр выбранного файла InternalEdit.label = Редактировать (встроенным) Calculator.label = Калькулятор CreateSymlink.label = Создать ссылку LocateSymlink.label = Перейти к файлу ссылки ShowFoldersSize.label = Показать размеры директорий EditCommands.label = Редактирование команд EditCommands.group.view = Программы просмотра EditCommands.group.edit = Редакторы EditCommands.group.others = Прочие EditCommands.new = Новая EditCommands.alias = Имя EditCommands.command = Команда EditCommands.display_name = Отображаемое имя EditCommands.filemask = Маски файлов symboliclinkeditor.edit = Редактировать ссылку symboliclinkeditor.create = Символическая ссылка symboliclinkeditor.target_file_create = Существующий файл (ссылка будет указывать на него) symboliclinkeditor.target_file_edit = Ссылка '%s' указывает на symboliclinkeditor.link_name = Имя ссылки Edit.label = Редактировать Edit.tooltip = Редактирование выбранного файла EditAs.label = Редактировать как Copy.tooltip = Копирование выделенных файлов LocalCopy.label = Копировать локально Copy.label = Копировать LocalCopy.tooltip = Копирование выбранного файла в текущий каталог Move.label = Перенести Move.tooltip = Перенос выделенных файлов Rename.label = Переименовать Rename.tooltip = Переименование выбранного файла Mkdir.label = Создать каталог Mkdir.tooltip = Создание подкаталог в текущем каталоге Mkfile.label = Создать файл Mkfile.tooltip = Создать файл в текущем каталоге Delete.label = Удалить Delete.tooltip = Удаление выделенных файлов в корзину, если это возможно PermanentDelete.label = Удалить сразу PermanentDelete.tooltip = Удалить выделенные файлы минуя корзину Refresh.label = Обновить Refresh.tooltip = Обновление списка файлов в текущем каталоге CloseWindow.label = Закрыть окно CloseWindow.tooltip = Закрытие окна CopyFileNames.label = Копировать полные имена CopyFileBaseNames.label = Копировать только имена CopyFilePaths.label = Копировать пути CopyFilesToClipboard.label = Копировать файл(ы) PasteClipboardFiles.label = Вставить файл(ы) Email.label = Отправить файлы по Email Email.tooltip = Отправить выделенные файлы как приложения по Email GoBack.label = Назад GoBack.tooltip = Перейти к предыдущему каталогу GoForward.label = Вперед GoForward.tooltip = Перейти к следующему каталогу GoToHome.label = Перейти в домашний каталог GoToParent.label = Перейти в родительский каталог GoToParent.tooltip = Перейти к родительскому каталогу GoToParentInOtherPanel.label = Перейти наверх в другой панели GoToParentInBothPanels.label = Перейти наверх в обеих панелях GoToRoot.label = Перейти в корень SortByName.label = Сортировать по имени SortByDate.label = Сортировать по дате SortBySize.label = Сортировать по размеру SortByExtension.label = Сортировать по расширению SortByPermissions.label = Сортировать по правам доступа SortByOwner.label = Сортировать по владельцу SortByGroup.label = Сортировать по группе MarkGroup.label = Выделить файлы MarkGroup.tooltip = Выделить группу файлов UnmarkGroup.label = Снять выделение с файлов UnmarkGroup.tooltip = Отменить выделение группы файлов MarkAll.label = Выделить все UnmarkAll.label = Снять выделение со всего MarkSelectedFile.label = Выделить/снять MarkSelectedFile.tooltip = Выделить/снять выделение выбранного файла MarkNextBlock.label = Выделить блок строк вниз MarkPreviousBlock.label = Выделить блок строк вверх MarkNextRow.label = Построчное выделение вниз MarkPreviousRow.label = Построчное выделение вверх MarkNextPage.label = Выделить файлы на следующей странице MarkPreviousPage.label = Выделить файлы на предыдущей странице MarkToFirstRow.label = Выделить файлы до начала MarkToLastRow.label = Выделить файлы до конца MarkExtension.label = Выделить по расширению MarkEmpty.label = Выделить пустые InvertSelection.label = Инвертировать выделение SwapFolders.label = Поменять местами каталоги SwapFolders.tooltip = Поменять местами панели SetSameFolder.label = Сделать каталоги одинаковыми SetSameFolder.tooltip = Одинаковые каталоги в обеих панелях ToggleTableViewModeFull.label = Полный режим ToggleTableViewModeFull.tooltip = Переключиться в полный режим ToggleTableViewModeCompact.label = Компактный режим ToggleTableViewModeCompact.tooltip = Переключиться в компактный режим ToggleTableViewModeShort.label = Короткий режим ToggleTableViewModeShort.tooltip = Переключиться в короткий режим TogglePanelPreviewMode.label = Быстрый просмотр TogglePanelPreviewMode.tooltip = Переключиться в режим быстрого просмотра NewWindow.label = Новое окно NewWindow.tooltip = Открыть новое окно Open.label = Открыть Open.tooltip = Войти в каталог / Войти в архив / Выполнить OpenNatively.label = Открыть в связанной программе OpenNatively.tooltip = Выполнить выделенный файл с помощью программы, связанной с ним в настройках системы OpenInOtherPanel.label = Открыть в другой панели OpenInBothPanels.label = Открыть в обеих панелях OpenLeftInRightPanel.label = Открыть левый объект в правой панели OpenRightInLeftPanel.label = Открыть правый объект в левой панели OpenInNewTab.label = Открыть в новой вкладке CloseTab.label = Закрыть CloseTab.tooltip = Закрыть вкладку PreviousTab.label = Предыдущая вкладка PreviousTab.tooltip = Переключиться на вкладку левее NextTab.label = Следующая вкладка NextTab.tooltip = Переключиться на вкладку правее MoveTabToOtherPanel.label = Перенести на другую панель MoveTabToOtherPanel.tooltip = Перенести вкладку на другую панель CloseOtherTabs.label = Закрыть остальные CloseOtherTabs.tooltip = Закрыть остальные вкладки CloseDuplicateTabs.label = Закрыть дубликаты CloseDuplicateTabs.tooltip = Закрыть вкладки дубликатов CloneTabToOtherPanel.label = Клонировать в другую панель CloneTabToOtherPanel.tooltip = Добавить такую же вкладку в другой панели SetTabTitle.label = Задать заголовок SetTabTitle.tooltip = Задать заголовок вкладки RevealInDesktop.label = Отобразить в %1 RunCommand.label = Выполнение команды RunCommand.tooltip = Выполнить команду из текущего каталога Pack.label = Архивировать файлы Pack.tooltip = Архивировать выбранные файлы Unpack.label = Разархивировать файлы Unpack.tooltip = Разархивировать выделенные файлы ShowFileProperties.label = Свойства ShowFileProperties.tooltip = Показать свойства выбранных файлов ShowFilePopupMenu.label = Контекстное меню ShowFilePopupMenu.tooltip = Показать контекстное меню для выбранного файла ShowPreferences.label = Настройки ShowPreferences.tooltip = Настройка trolCommander ShowServerConnections.label = Показать установленные соединения Quit.label = Выход ShowBookmarksQL.label = Закладки ShowEditorBookmarksQL.label = Закладки на файлы EjectDrive.label = Извлечь диск EjectDrive.tooltip = Безопасно извлечь диск ReverseSortOrder.label = В обратном порядке ToggleAutoSize.label = Автоподбор размера колонок Stop.label = Прекратить изменение каталога ToggleColumn.show = Показать колонку %1 ToggleColumn.hide = Убрать колонку %1 ToggleCommandBar.show = Показать панель команд ToggleCommandBar.hide = Скрыть панель команд ToggleToolBar.show = Показать панель инструментов ToggleToolBar.hide = Скрыть панель инструментов CustomizeCommandBar.label = Настройка панели команд ToggleHiddenFiles.label = Показывать скрытые файлы ToggleStatusBar.show = Показать панель статуса ToggleStatusBar.hide = Скрыть панель статуса ToggleShowFoldersFirst.label = Вынести каталоги в начало списка ToggleFoldersAlwaysAlphabetical.label = Сортировать каталоги всегда по алфавиту ToggleTree.label = Показать древовидный вид ToggleSinglePanel.label = Режим одной панели PopupLeftDriveButton.label = Изменить левый каталог PopupRightDriveButton.label = Изменить правый каталог RecallPreviousWindow.label = Переключиться в предыдущее окно RecallNextWindow.label = Переключиться в следующее окно RecallWindow.label = Восстановить окно #%1 RecallWindow1.label = Восстановить окно #%1 RecallWindow2.label = Восстановить окно #%1 RecallWindow3.label = Восстановить окно #%1 RecallWindow4.label = Восстановить окно #%1 RecallWindow5.label = Восстановить окно #%1 RecallWindow6.label = Восстановить окно #%1 RecallWindow7.label = Восстановить окно #%1 RecallWindow8.label = Восстановить окно #%1 RecallWindow9.label = Восстановить окно #%1 RecallWindow10.label = Восстановить окно #%1 BringAllToFront.label = Показать все окна ShowTabsQL.label = Открыть вкладки ShowRootFoldersQL.label = Корневые директории SwitchActiveTable.label = Переключение между левой и правой панелью SelectNextBlock.label = Спуститься на блок строк вниз SelectPreviousBlock.label = Подняться на блок строк вверх SelectNextPage.label = Перейти в конец страницы SelectPreviousPage.label = Перейти в начало страницы SelectNextRow.label = Спуститься на одну строку вниз SelectPreviousRow.label = Подняться на одну строку вверх SelectFirstRow.label = Перейти к первому файлу в текущем каталоге SelectLastRow.label = Перейти к последнему файлу в текущем каталоге LeftArrowAction.label = Перейти левее RightArrowAction.label = Перейти правее SplitEqually.label = Разделить поровну SplitVertically.label = Разделить вертикально SplitHorizontally.label = Разделить горизонтально ShowKeyboardShortcuts.label = Быстрые клавиши GoToWebsite.label = Перейти на домашнюю страницу программы GoToForums.label = Перейти на форум ReportBug.label = Сообщить об ошибке Donate.label = Сделать пожертвование на развитие программы ShowAbout.label = О программе trolCommander OpenTrash.label = Открыть Корзину EmptyTrash.label = Очистить корзину CalculateChecksum.label = Расчет контрольной суммы MinimizeWindow.label = Свернуть MaximizeWindow.label = Развернуть MaximizeWindow.label.mac_os_x = Развернуть GoToDocumentation.label = Документация онлайн ShowParentFoldersQL.label = Родительские каталоги ShowRecentLocationsQL.label = Недавно посещенные каталоги ShowRecentExecutedFilesQL.label = Недавно используемые файлы ShowRecentViewedFilesQL.label = Недавно просматриваемые файлы ShowRecentEditedFilesQL.label = Недавно редактируемые файлы SplitFile.label = Разъединить SplitFile.tooltip = Разъединить файл на несколько частей CombineFiles.label = Собрать CombineFiles.tooltip = Собрать разъединенный на части файл в исходный файл ShowDebugConsole.label = Консоль Debug FocusPrevious.label = Перейти к предыдущему компоненту FocusNext.label = Перейти к следующему компоненту file_menu = Файл file_menu.open_with = Открыть с помощью file_menu.open_as = Open как mark_menu = Выделение view_menu = Вид view_menu.show_hide_columns = Показать/скрыть столбцы view_menu.table_mode = Режим go_menu = Перейти tools_menu = Инструменты eject_menu = Извлечь диск bookmarks_menu = Закладки bookmarks_menu.no_bookmark = Закладок нет drive_popup.network_shares = Сетевой общий ресурс quick_lists_menu = Быстрый список window_menu = Окна help_menu = Справка AddTab.label = Добавить вкладку AddTab.tooltip = Добавить новую вкладку в активной панели DuplicateTab.label = Дублировать вкладку DuplicateTab.tooltip = Дублировать текущую вкладку на текущей панели ToggleLockTab.label = Блокировать/Разблокировать ToggleLockTab.tooltip = Изменить состояние блокировки вкладки ToggleLockTab.lock = Блокировать ToggleLockTab.unlock = Разблокировать status_bar.selected_files = %1 из %2 файлов выбрано status_bar.connecting_to_folder = Подключаемся к каталогу status_bar.volume_free = Свободно: %1 status_bar.volume_capacity = Емкость диска: %1 status_bar.quick_search.press_esc_to_stop_search = нажмите Esc для отмены поиска shortcuts_panel.title = Комбинации клавиш shortcuts_panel.restore_defaults = Вернуть стандартные shortcuts_panel.show = Показать shortcuts_panel.search = Поиск shortcuts_panel.default_message = Нажатие Enter или двойной-щелчек обеспечит редактирование комбинаций клавиш shortcuts_table.action_description = Описание действия shortcuts_table.shortcut = Комбинации клавиш shortcuts_table.alternate_shortcut = Альтернативные комбинации shortcuts_table.type_in_a_shortcut = Нажмите комбинацию клавиш command_bar_dialog.help = Перетаскивайте кнопки для настройки панели команд table.folder_access_error_title = Ошибка: нет доступа в каталог table.folder_access_error = Не удается прочесть содержимое каталога table.download_or_browse = Вы хотите просмотреть или скачать данный файл? version_dialog.no_new_version_title = Новых версий не обнаружено version_dialog.no_new_version = У вас уже установлена самая последняя версия version_dialog.new_version_title = Доступна новая версия version_dialog.new_version = Доступна новая версия trolCommander. version_dialog.new_version_url = Новая версия trolCommander доступна по адресу %1. version_dialog.not_available_title = Сервер недоступен version_dialog.not_available = Не удается получить информацию о новых версиях с сервера. version_dialog.install_and_restart = Установить и перезапустить version_dialog.preparing_for_update = Идет подготовка к обновлению... quit_dialog.title = Выход из trolCommander quit_dialog.desc = Закрыть %1 окон и выйти из trolCommander ? quit_dialog.show_next_time = Показывать следующий раз destination_dialog.file_exists_action = Действие по умолчанию если файл уже существует destination_dialog.verify_integrity = Проверить целостность данных destination_dialog.skip_errors = Пропустить ошибки destination_dialog.background_mode = Фоновый режим file_collision_dialog.title = Совпадение файлов rename_dialog.new_name = Новое имя copy_dialog.destination = Копировать выбранные файлы в copy_dialog.error_title = Ошибка при копировании copy_dialog.copying = Копирование файлов copy_dialog.copying_file = Копирую %1 pack_dialog.packing = Архивируются файлы pack_dialog.packing_file = Архивируется файл %1 pack_dialog.error_title = Ошибка архивации pack_dialog_description = Добавить выбранные файлы в pack_dialog.archive_format = Формат архива unpack_dialog.destination = Разархивировать выбранные файлы в unpack_dialog.error_title = Ошибка разархивации unpack_dialog.unpacking = Разархивация файлов unpack_dialog.unpacking_file = Разархивируется файл %1 optimizing_archive = Оптимизация архива %1 error_while_optimizing_archive = Ошибка при оптимизации архива %1 move_dialog.move_description = Перенести в move_dialog.error_title = Ошибка при переносе move_dialog.moving = Перемещение файлов move_dialog.moving_file = Перемещается файл %1 download_dialog.description = Скачать файл в download_dialog.error_title = Ошибка при скачивании download_dialog.downloading = Скачивание download_dialog.downloading_file = Скачивается файл %1 download_dialog.download = Скачать mkfile_dialog.allocate_space = Выделить место mkfile_dialog.open_in_editor = Открыть в редакторе mkfile_dialog.make_executable = Исполняемый файл mkfile_dialog.convert_whitespace = Преобразовать пробелы delete_dialog.permanently_delete.confirmation = Удалить выбранные файлы без возможности восстановления? delete_dialog.permanently_delete.confirmation_1 = Удалить выбранный файл без возможности восстановления? delete_dialog.permanently_delete.symlink_confirmation_1 = Удалить выбранную символическую ссылку без возможности восстановления? delete_dialog.move_to_trash.confirmation = Удалить выбранные файлы? delete_dialog.move_to_trash.confirmation_1 = Удалить выбранный файл? delete_dialog.move_to_trash.confirmation_details = Файлы будут помещены в Корзину delete_dialog.move_to_trash.confirmation_details_1 = Файл будет помещён в Корзину delete_dialog.move_to_trash.option = Поместить в Корзину delete_dialog.move_to_trash.failed = Один или несколько файлов не могут быть перемещены в корзину. delete_dialog.deleting = Удаление файлов delete_dialog.error_title = Ошибка при удалении файла delete.deleting_file = Удаляется файл %1 email_dialog.prefs_not_set_title = Почта не настроена email_dialog.prefs_not_set = Вам необходимо настроить параметры почты перед тем, как пытаться что-то отправить. email_dialog.from = От кого email_dialog.to = Кому email_dialog.subject = Тема email_dialog.send = Отправить email_dialog.error_title = Ошибка при отправке файлов email_dialog.read_error = Невозможно прочитать файлы в подкаталогах. email_dialog.sending = Отправка файлов email.sending_file = Отправляется %1 email.connecting_to_server = Установка соединения с %1 email.server_unavailable = Не удалось соединиться с почтовым сервером %1, проверьте настройки почты и попробуйте еще раз позже. email.connection_closed = Соединение закрыто сервером, почта не была отправлена. email.goodbye_failed = Ошибка при завершении соединения, возможно, почта не была отправлена. email.send_file_error = Невозможно отправить файл %1, почта не была отправлена. split_file_dialog.error_title = Ошибка в разъединении файла split_file_dialog.file_to_split = Файл для разъединения split_file_dialog.target_directory = Результирующий каталог split_file_dialog.part_size = Размер каждой части split_file_dialog.parts = Число частей split_file_dialog.generate_CRC = Сформировать CRC файл split_file_dialog.max_parts = Максимально допустимое число частей %1 split_file_dialog.auto = Авто split_file_dialog.insert_new_media = Вставьте новый носитель combine_files_dialog.error_title = Ошибка соединения файла combine_files_job.no_crc_file = Соеденино успешно. Нет CRC файла. combine_files_job.crc_read_error = Ошибка чтения CRC файла. combine_files_job.crc_check_failed = CRC расхождения: ожидается %2, получено %1 combine_files_job.crc_ok = Соеденино успешно. CRC контрольная сумма ok. file_selection_dialog.mark = Выделение file_selection_dialog.unmark = Снятие выделения file_selection_dialog.mark_description = Выделить файлы по маске file_selection_dialog.unmark_description = Снять выделение с файлов по маске file_selection_dialog.case_sensitive = С учетом регистра file_selection_dialog.include_folders = Включая каталоги progress_dialog.starting = Передача файлов начата... progress_dialog.transferred = Передано %1, в среднем %2 progress_dialog.elapsed_time = Потрачено времени progress_dialog.advanced = Ещё progress_dialog.current_speed = Cкорость progress_dialog.limit_speed = Предел скорости progress_dialog.close_when_finished = Закрыть окно после завершения progress_dialog.processing_files = Обработка файлов progress_dialog.processing_file = Обрабатывается %1 progress_dialog.verifying_file = Идет проверка %1 progress_dialog.job_finished = Обработка завершена progress_dialog.job_error = Ошибка обработки progress_dialog.hide = Свернуть в фон properties_dialog.file_properties = Свойства %1 properties_dialog.contents = Содержимое properties_dialog.calculating = идет подсчет... calculate_checksum_dialog.checksum_algorithm = Алгоритм расчета контр. суммы calculate_checksum_dialog.temporary_file = Временный файл change_date_dialog.now = Текущая дата change_date_dialog.specific_date = Указанная дата run_dialog.run_command_description = Запустить из текущего каталога run_dialog.run_in_home_description = Выполнить в домашнем каталоге run_dialog.command_output = Вывод программы run_dialog.run = Запустить run_dialog.clear_history = Очистить буфер команд run_dialog.stop = Остановить server_connect_dialog.server_type = Тип соединения server_connect_dialog.server = Сервер server_connect_dialog.share = Общий ресурс server_connect_dialog.domain = Домен server_connect_dialog.username = Имя пользователя server_connect_dialog.initial_dir = Начальный каталог server_connect_dialog.port = Порт server_connect_dialog.server_url = Адрес сервера server_connect_dialog.http_url = URL сайта server_connect_dialog.connect = Соединиться server_connect_dialog.protocol = Протокол server_connect_dialog.nfs_version = Версия NFS server_connect_dialog.private_key = Закрытый ключ server_connect_dialog.passphrase = Ключевая фраза ftp_connect.passive_mode = Использовать пассивный режим ftp_connect.anonymous_user = Анонимное соединение ftp_connect.nb_connection_retries = Количество попыток установления соединения ftp_connect.retry_delay = Задержка между повторами (в секундах) http_connect.basic_authentication = Базовая аутентификация HTTP (не обязательно) server_connections_dialog.disconnect = Отключиться server_connections_dialog.connection_busy = Занято server_connections_dialog.connection_idle = Свободно bonjour.bonjour_services = Служба Bonjour bonjour.no_service_discovered = Служб не обнаружено bonjour.bonjour_disabled = Служба Bonjour отключена auth_dialog.title = Аутентификация auth_dialog.desc = Введите логин и пароль auth_dialog.server = Сервер auth_dialog.store_credentials = Сохранить логин и пароль (со слабым шифрованием) auth_dialog.connect_as = Просоединиться как auth_dialog.authentication_failed = Аутентификации не пройдена sortable_list.move_up = Выше sortable_list.move_down = Ниже add_bookmark_dialog.add = Добавить edit_bookmarks_dialog.new = Новый edit_bookmarks_dialog.location = Местоположение edit_bookmarks_dialog.is_separator = Указанное имя задает разделитель file_viewer.view_error_title = Ошибка при просмотре file_viewer.view_error = Невозможно просмотреть файл. file_viewer.file_menu = Файл file_viewer.close = Закрыть file_viewer.large_file_warning = Этот файл может оказаться слишком большим для просмотра. file_viewer.open_anyway = Открыть все равно file_viewer.open_hex = Двоичный просмотр text_viewer.edit = Редактировать text_viewer.copy = Копировать text_viewer.select_all = Выбрать все text_viewer.find = Поиск text_viewer.find_button = Найти text_viewer.find_next = Искать далее text_viewer.find_previous = Искать предыдущий text_viewer.find.case_sensitive = Различать регистр text_viewer.find.whole_word = Целые слова text_viewer.find.regexp = Regexp text_viewer.find.mark_all = Пометить всё text_viewer.find.direction = Направление text_viewer.find.up = Вверх text_viewer.find.down = Вниз text_viewer.view = Просмотр text_viewer.line_wrap = Перенос строк text_viewer.line_numbers = Нумерация строк text_viewer.binary_file_warning = Этот файл, скорее всего, двоичный text_viewer.goto_line = Перейти к строке text_viewer.line = Строка text_viewer.open_file_error = Не могу открыть файл image_viewer.controls_menu = Элементы управления image_viewer.zoom_in = Увеличить image_viewer.zoom_out = Уменьшить file_editor.file_menu = Файл file_editor.close = Закрыть file_editor.edit_error_title = Ошибка при редактировании file_editor.edit_error = Невозможно отредактировать файл. file_editor.save = Сохранить file_editor.save_as = Сохранить как... file_editor.save_warning = Сохранить сделанные изменения в файл перед выходом ? file_editor.cannot_write = Не удается записать файл. file_editor.open_anyway = Все равно открыть file_editor.save_anyway = Все равно сохранить file_editor.overwrite_readonly = Файл только для чтения file_editor.files = Файлы file_editor.files.list = Выбор файла file_editor.show_file_manager = Менеджер файлов file_editor.add_to_bookmark = Добавить в закладки file_editor.remove_from_bookmark = Удалить из закладок file_editor.goto_header_source = Перейти к заголовку/исходнику text_editor.cut = Вырезать text_editor.paste = Вставить text_editor.undo = Отменить изменение text_editor.redo = Вернуть изменение text_editor.edit = Правка text_editor.copy = Копировать text_editor.select_all = Выделить все text_editor.view = Просмотр text_editor.find = Искать... text_editor.find_next = Искать следующее text_editor.find_previous = Искать предыдущее text_editor.search = Поиск text_editor.replace_menu = Заменить.. text_editor.replace_button = Заменить text_editor.replace = Замена text_editor.replace_with = Заменить на text_editor.replace_all = Заменить всё text_editor.replaced = Заменено text_editor.occurrences = вхождений text_editor.line_wrap = Пенерос строк text_editor.line_numbers = Нумерация строк text_editor.syntax = Синтаксис text_editor.format = Форматировать text_editor.writing = Сохраняю... text_editor.modified = Изменен text_editor.saved = Файл сохранен text_editor.text_not_found = Текст не найден text_editor.found = Найдено text_editor.matches = вхождений text_editor.cant_save_file = Не удалось сохранить файл text_editor.press_alt_enter_to_open_file = Нажмите Alt+Enter чтобы открыть файл text_editor.tools=Инструменты text_editor.build=Собрать text_editor.invisible_chars = Невидимые символы shortcuts_dialog.quick_search = Быстрый поиск shortcuts_dialog.quick_search.start_search = Введите любой символ для начала быстрого поиска shortcuts_dialog.quick_search.cancel_search = Отмена быстрого поиска shortcuts_dialog.quick_search.remove_last_char = Удалить последний символ из строки быстрого поиска shortcuts_dialog.quick_search.jump_to_previous = Перейти к предыдущему результату быстрого поиска shortcuts_dialog.quick_search.jump_to_next = Перейти к следующему результату быстрого поиска shortcuts_dialog.quick_search.mark_jump_next = Выделить/снять выделение с файла и перейти к следующему результату поиска theme_editor.title = Редактор темы theme_editor.folder_tab = Панель каталога theme_editor.shell_tab = Оболочка theme_editor.shell_history_tab = История команд theme_editor.terminal_tab = Терминал theme_editor.statusbar_tab = Строка состояния theme_editor.free_space = Свободное место theme_editor.free_space.ok = OK theme_editor.free_space.warning = Заканчивается theme_editor.free_space.critical = Очень мало theme_editor.locationbar_tab = Указатель местонахождения theme_editor.editor_tab = Редактор файлов theme_editor.font = Шрифт theme_editor.active_panel = Активная theme_editor.inactive_panel = Неактивная theme_editor.general = Общий theme_editor.could_not_save_theme = Не удается сохранить тему %1 theme_editor.border = Рамка theme_editor.background = Фон theme_editor.alternate_background = Другой фон theme_editor.unfocused_background = Фон (без фокуса) theme_editor.quick_search.unmatched_file = Несовпадающие файлы theme_editor.text = Текст theme_editor.progress = Индикатор выполнения theme_editor.normal = Обычный theme_editor.normal_unfocused = Обычный (без фокуса) theme_editor.selected = Выбранный theme_editor.selected_unfocused = Выбранный (без фокуса) theme_editor.color = Цвет theme_editor.colors = Цвета theme_editor.plain_file = Обычный файл theme_editor.marked_file = Выделенный файл theme_editor.hidden_folder = Скрытый каталог theme_editor.hidden_file = Скрытый файл theme_editor.folder = Каталог theme_editor.archive_file = Архив theme_editor.symbolic_link = Символьная ссылка theme_editor.executable_file = Исполняемый файл theme_editor.header = Заголовок theme_editor.current = Подсветка строки theme_editor.file_groups = Группы файлов theme_editor.group_ = Группа theme_editor.normal_color = Нормальный цвет theme_editor.selected_color = Выделенный цвет theme_editor.filemask = Маски файлов theme_editor.group_file_ = Файл группы theme_editor.item = Элемент theme_editor.quick_search = Быстрый поиск theme_editor.text_editor_tab = Текстовый редактор и вьювер theme_editor.hex_viewer_tab = Шестнадцатиричный вьювер theme_editor.normal_hex = Дамп theme_editor.normal_offset = Адрес theme_editor.normal_ascii = ASCII theme_editor.alternate = Альтернативный theme_editor.selected_hex = Выбранный дамп theme_editor.selected_ascii = Выбранный ASCII command_bar_customize_dialog.available_actions = Доступные действия command_bar_customize_dialog.modifier = Переключатель prefs_dialog.title = Настройки prefs_dialog.general_tab = Общие prefs_dialog.day = День prefs_dialog.month = Месяц prefs_dialog.year = Год prefs_dialog.language = Язык (потребуется перезапуск) prefs_dialog.date_time = Формат даты и времени prefs_dialog.time = Время prefs_dialog.date = Дата prefs_dialog.date_separator = Разделитель prefs_dialog.time_12_hour = 12-часовой формат prefs_dialog.time_24_hour = 24-часовой формат prefs_dialog.show_seconds = Показывать секунды prefs_dialog.show_century = Показывать 4 цифры года prefs_dialog.check_for_updates_on_startup = Проверять наличие новых версий при запуске prefs_dialog.show_splash_screen = Показывать заставку prefs_dialog.folders_tab = Каталоги prefs_dialog.startup_folders = Каталоги при запуске prefs_dialog.left_folder = Каталог в левой панели prefs_dialog.right_folder = Каталог в правой панели prefs_dialog.last_folder = Последний посещенный каталог prefs_dialog.custom_folder = Заданный каталог prefs_dialog.quick_search = Быстрый поиск prefs_dialog.quick_search_timeout = Таймаут prefs_dialog.show_quick_search_matches_first = Показывать найденныые файлы первыми prefs_dialog.quick_search_timeout_never = Без таймаута prefs_dialog.quick_search_timeout_sec = Сек prefs_dialog.show_hidden_files = Показывать скрытые файлы prefs_dialog.show_ds_store_files = Показывать файлы .DS_Store prefs_dialog.show_system_folders = Показывать системные каталоги prefs_dialog.compact_file_size = Округлять показываемые размеры файлов prefs_dialog.follow_symlinks_when_cd = Переходить по ссылкам при смене каталога prefs_dialog.show_tab_header = Всегда показывать вкладки prefs_dialog.appearance_tab = Внешний вид prefs_dialog.look_and_feel = Настройки отображения prefs_dialog.icons_size = Размер значков prefs_dialog.toolbar_icons = Панель инструментов prefs_dialog.command_bar_icons = Панель команд prefs_dialog.file_icons = Типы файлов prefs_dialog.use_system_file_icons = Использовать системные значки файлов prefs_dialog.use_system_file_icons.always = Всегда prefs_dialog.use_system_file_icons.never = Никогда prefs_dialog.use_system_file_icons.applications = Только для приложений prefs_dialog.edit_current_theme = Редактировать тему оформления... prefs_dialog.themes = Темы prefs_dialog.syntax_themes = Тема подсветки синтаксиса в редакторе prefs_dialog.import_theme = Импортировать тему prefs_dialog.import_look_and_feel = Импорт настроек внешнего вида prefs_dialog.no_look_and_feel = Настроек внешнего вида не найдено. prefs_dialog.error_in_import = Ошибка при импорте темы %1. prefs_dialog.cannot_read_theme = Не удается загрузить тему из файла %1 prefs_dialog.export_theme = Экспорт %1 prefs_dialog.import = Импорт prefs_dialog.export = Экспорт prefs_dialog.theme_type = Тип: %1 prefs_dialog.delete_theme = Удалить тему %1 полностью? prefs_dialog.delete_look_and_feel = Удалить настройки %1 навсегда? prefs_dialog.rename_failed = Не удалось переименовать тему %1 prefs_dialog.xml_file = XML-файл prefs_dialog.jar_file = JAR-файл prefs_dialog.mail_tab = Email prefs_dialog.mail_settings = Настройки исходящей почты prefs_dialog.mail_name = Ваше имя prefs_dialog.mail_address = Ваш адрес Email prefs_dialog.mail_server = SMTP-сервер prefs_dialog.misc_tab = Еще prefs_dialog.use_brushed_metal = Использовать тему 'brushed metal' (потребуется перезапуск) prefs_dialog.confirm_on_quit = Запрашивать подтверждение при выходе prefs_dialog.default_shell = Использовать системную оболочку по умолчанию prefs_dialog.custom_shell = Использовать указанную оболочку prefs_dialog.shell_encoding = Кодировка командной строки prefs_dialog.auto_detect_shell_encoding = Автоопределение prefs_dialog.external_terminal = Внешний терминал prefs_dialog.default_terminal = Использовать команду по умолчанию prefs_dialog.custom_terminal = Использовать указанную команду prefs_dialog.iterm_terminal = Использовать iTerm prefs_dialog.builtin_terminal = Встроенный терминал prefs_dialog.enable_bonjour_discovery = Включить службу обнаружения Bonjour prefs_dialog.enable_system_notifications = Разрешить системные уведомления prefs_dialog.calculate_folder_size_on_mark = Вычислять размер директорий при выделении prefs_dialog.shell = Команда запуска debug_console_dialog.level = Уровень debug_console_dialog.threads = Потоки debug_console_dialog.active_threads = Активные потоки unit.byte = байт unit.bytes = байтов unit.bytes_short = б unit.kb = Кб unit.mb = Мб unit.gb = Гб unit.tb = ТБ unit.speed = %1/сек duration.seconds = %1с duration.minutes = %1м duration.hours = %1ч duration.days = %1д duration.months = %1мес duration.years = %1г theme.custom_theme = Собственная тема theme.custom = Пользовательская тема theme.built_in = Встроенная theme.add_on = Дополнение theme.current = текущая theme_could_not_be_loaded = В процессе загрузки темы произошла ошибка. setup.title = Добро пожаловать в trolCommander setup.intro = Выберите основные настройки trolCommander. setup.look_and_feel = Выберите пользовательский интерфейс setup.theme = Выберите тему оформления font_chooser.font_size = Размер font_chooser.font_bold = Полужирный font_chooser.font_italic = Курсив color_chooser.red = Красный color_chooser.green = Зеленый color_chooser.blue = Синий color_chooser.hue = Оттенок color_chooser.brightness = Яркость color_chooser.swatches = Образцы color_chooser.saturation = Насыщенность color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Предыдущий color_chooser.alpha = Прозрачность color_chooser.title = Выбор цвета batch_rename_dialog.mask = Шаблон переименования batch_rename_dialog.search_replace = Поиск и замена batch_rename_dialog.search_for = Искать batch_rename_dialog.replace_with = Заменить на batch_rename_dialog.counter = Счетчик batch_rename_dialog.start_at = Начать с batch_rename_dialog.step_by = Шаг batch_rename_dialog.format = Формат batch_rename_dialog.upper_lower_case = Верхний/нижний регистр batch_rename_dialog.no_change = Без изменений batch_rename_dialog.lower_case = нижний регистр batch_rename_dialog.upper_case = ВЕРХНИЙ РЕГИСТР batch_rename_dialog.first_upper = Первая буква заглавная batch_rename_dialog.word = Первый от каждого слова batch_rename_dialog.regexp = RegExp batch_rename_dialog.regexp_error = Синтаксическая ошибка в RegExp-е batch_rename_dialog.old_name = Старое имя batch_rename_dialog.new_name = Новое имя batch_rename_dialog.block_name = Без изменений batch_rename_dialog.range = Интервал batch_rename_dialog.proceed_renaming = Количество файлов %1 из %2 будут переименованны. Начать процесс? batch_rename_dialog.duplicate_names = Совпадение имен! batch_rename_dialog.names_conflict = Конфликт имен! Некоторые значения совпадают в станых и новых именах. parent_folders_quick_list.empty_message = Текущее местонахождение не имеет родительского recent_locations_quick_list.empty_message = Не найдено недавних местонахождений recent_executed_files_quick_list.empty_message = Не найдено недавно используемых файлов recent_edited_files_quick_list.empty_message = Не найдено недавно редактируемых файлов recent_viewed_files_quick_list.empty_message = Не найдено недавно просматриваемых файлов editor_bookmarks_quick_list.empty_message = Нет закладок editor_bookmarks_quick_list.file_not_found = Файл не найден editor_bookmarks_quick_list.press_f4_to_edit_list = Нажмите F4 для редактирования списка #pack.error_on_file = Ошибка при архивации файла %1 #pack_dialog.cannot_write = Невозможно создать архив в каталоге-получателе #unpack.unable_to_open_zip = Не удается открыть архив %1. #mkdir_dialog.title = Создание каталога #mkdir_dialog.error_title = Ошибка при создании каталога #edit_bookmarks_dialog.remove = Удалить #mkdir_dialog.description = Создать каталог #done = Готово #progress_dialog.hide = Скрыть #move_dialog.rename_description = Переименовать файл в #theme_editor.shell_font = Шрифт в оболочке #theme_editor.history_font = Шрифт буфера команд #theme_editor.shell_colors = Системные цвета #theme_editor.history_colors = Цвета буфера команд #auth_dialog.error_was = Ошибка: %1 #table.hide_column = Скрыть столбец #delete.symlink_warning_title = Обнаружена символьная ссылка #delete.symlink_warning = Этот файл представляет собой символьную ссылку:\n\n Файл: %1\n Ссылается на: %2\n\nУдалить только символьную ссылку или\nПерейти по ссылке и удалить весь каталог (ВНИМАНИЕ - ОПАСНОЕ ДЕЙСТВИЕ)? #delete.delete_link_only = Удалить ссылку #delete.delete_linked_folder = Удалить каталог find_dialog.name = Имя файла find_dialog.contains = Содержит текст find_dialog.initial_directory = Стартовая директория find_dialog.search_subdirectories = Поиск в поддиректориях find_dialog.search_archives = Поиск в архивах find_dialog.case_sensitive = Различать регистр find_dialog.ignore_hidden = Игнорировать скрытые файлы find_dialog.search_results = Результаты поиска find_dialog.found = Найдено файлов find_dialog.encoding = Кодировка текста find_dialog.search_hex = HEX-байты image_viewer.next_image = Следующий рисунок image_viewer.previous_image = Предыдущий рисунок hex_viewer.offset = Адрес hex_viewer.ascii_dump = Дамп ASCII hex_viewer.view = Просмотр hex_viewer.goto = Переход hex_viewer.goto.offset = Смещение hex_viewer.search = Поиск hex_viewer.searchNext = Искать далее hex_viewer.searchPrev = Искать предыдущий hex_viewer.find = Найти hex_view.text = Искать hex_viewer.hex = Hex hex_viewer.search_not_found = Шаблон не найден calculator.calculator = Калькулятор calculator.expression = Выражение calculator.error = Ошибка в выражении vsphere_connections_dialog.guest_password = Гостевой пароль vsphere_connections_dialog.guest_user = Гостевой логин vsphere_connections_dialog.guest_server = Гостевой сервер %1 tabs_quick_list.empty_message = Только одна вкладка cannot_open_cyclic_symlink = Не могу открыть выбранную ссылку поскольку она циклическая roots_quick_list.empty_message = Не найдено корневых директорий adb.no_devices = Нет устройств eject.no_mounted_devices = Нет примонтировнных устройств retry_as_root = Повторить от администратора ================================================ FILE: src/main/resources/dictionary_sk_SK.properties ================================================ ok = OK yes = Áno no = Nie cancel = Zrušiť edit = Upraviť close = Zatvoriť reset = Obnoviť rename = Premenovať apply = Použiť change = Zmeniť save = Uložiť dont_save = Neukladať replace = Nahradiť dont_replace = Nenahrádzať delete = Zmazať skip = Preskočiť skip_all = Preskočiť všetko retry = Opakovať resume = Pokračovať overwrite = Prepísať overwrite_if_older = Prepísať staršie duplicate = Duplikovať apply_to_all = Použiť na všetky copy = Kopírovať move = Presunúť pack = Komprimovať unpack = Dekomprimovať download = Stiahnuť split = Rozdeliť combine = Spojiť browse = Prechádzať ask = Spýtať sa stop = Zastaviť pause = Pauza quick_search = Rýchle hľadanie file_manager = Správca súborov create = Vytvoriť creating_file = Vytvára sa %1 choose = Zvoliť customize = Upraviť choose_folder = Zvoliť priečinok login = Prihlasovacie meno password = Heslo user = Užívateľ encoding = Kódovanie preferred_encodings = Predvolené kódovanie license = Licencia name = Meno size = Veľkosť date = Dátum extension = Prípona permissions = Oprávnenia owner = Vlastník group = Skupina location = Umiestnenie untitled = Bez mena source = Zdroj destination = Cieľ recurse_directories = Použiť aj pre vnorené zložky go_to = Ísť na example = Príklad preview = Ukážka comment = Komentár sample_text = Ukážkový text nb_files = %1 súbor(y) nb_folders = %1 priečinok(priečinky) loading = Načítavam obsah... this_operation_cannot_be_undone = Túto operáciu nie je možné vrátiť späť. remove = Odstrániť details = Detaily warning = Upozornenie error = Chyba generic_error = Pri požadovanej operácii sa vyskytla chyba. folder_does_not_exist = Tento priečinok neexistuje, alebo nie je k dispozícii. this_folder_does_not_exist = Tento priečinok neexistuje, alebo nie je k dispozícii: %1 this_file_does_not_exist = Tento súbor neexistuje, alebo nie je k dispozícii: %1 invalid_path = Neplatná cesta directory_already_exists = Priečinok %1 už existuje. file_exists_in_destination = Tento súbor už v cieli existuje source_parent_of_destination = Pokus o presun priečinku do jeho vlastného podpriečinku cannot_read_file = Súbor sa nedá čítať %1 cannot_write_file = Súbor sa nedá zapísať %1 cannot_create_folder = Priečinok sa nedá vytvoriť %1 cannot_read_folder = Obsah priečinku sa nedá čítať %1 cannot_delete_file = Súbor sa nedá zmazať %1 cannot_delete_folder = Priečinok sa nedá zmazať %1 error_while_transferring = Chyba pri prenose súboru %1 same_source_destination = Zdrojový a cieľový priečinok sa zhodujú file_already_exists = %1 už existuje, chcete ho prepísať ? write_error = Chyba zápisu read_error = Chyba pri čítaní integrity_check_error = Chyba pri kontrole integrity: zdroj a cieľ sa nezhodujú startup_error = Chyba bráni spusteniu trolCommander-a. permissions.read = Čítanie permissions.write = Zápis permissions.executable = Spúšťanie permissions.group = Skupina permissions.other = Ostatní permissions.octal_notation = Zápis v osmičkovej sústave action_categories.all = Všetky action_categories.navigation = Navigácia action_categories.selection = Výber action_categories.view = Zobraziť action_categories.file_operations = Operácie so súbormi action_categories.windows = Okná Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Pridať záložku AddBookmark.tooltip = Pridať aktuálny priečinok do zoznamu záložiek BatchRename.label = Dávkové premenovanie EditBookmarks.label = Editovať záložky ExploreBookmarks.label = Prechádzať záložky EditCredentials.label = Editovať účty ChangeLocation.label = Zmeniť aktuálne umiestnenie ChangeDate.label = Zmeniť dátum ChangeDate.tooltip = Zmeniť dátum vybraných súborov ChangePermissions.label = Zmeniť oprávnenia ChangePermissions.tooltip = Zmeniť oprávnenia pre vybrané súbory CheckForUpdates.label = Kontrola aktualizácii CompareFolders.label = Porovnať priečinky ConnectToServer.label = Pripojiť na server ConnectToServer.tooltip = Pripojiť na vzdialený server View.label = Zobraziť InternalView.label = Zobraziť (interne) View.tooltip = Zobraziť označené súbory InternalEdit.label = Upraviť (interne) Edit.tooltip = Upraviť označené súbory Copy.tooltip = Kopírovať označené súbory LocalCopy.label = Lokálna kópia LocalCopy.tooltip = Kopírovať vybraný súbor do aktuálneho priečinku Move.tooltip = Presunúť označené súbory Rename.tooltip = Premenovať vybrané súbory Mkdir.label = Vytvoriť priečinok Mkdir.tooltip = Vytvoriť priečinok v aktuálnom priečinku Mkfile.label = Vytvoriť súbor Mkfile.tooltip = Vytvoriť súbor v aktuálnom priečinku Delete.tooltip = Zmazať označené súbory (do koša, ak je k dispozícii) PermanentDelete.label = Úplne zmazať PermanentDelete.tooltip = Úplne zmazať označené súbory Refresh.label = Obnoviť Refresh.tooltip = Obnoviť aktuálny priečinok CloseWindow.label = Zatvoriť okno CloseWindow.tooltip = Zatvoriť toto okno CopyFileNames.label = Kopírovať názov(názvy) CopyFilePaths.label = Kopírovať cestu(y) CopyFilesToClipboard.label = Kopírovať súbor(y) PasteClipboardFiles.label = Prilepiť súbor(y) Email.label = Poslať súbory e-mailom Email.tooltip = Poslať označené súbory ako prílohu e-mailu GoBack.label = Späť GoBack.tooltip = Predchádzajúci priečinok GoForward.label = Dopredu GoForward.tooltip = Nasledujúci priečinok GoToHome.label = Domovský priečinok GoToParent.label = O úroveň vyššie GoToParent.tooltip = Nadradený priečinok GoToParentInOtherPanel.label = O úroveň vyššie v druhom panele GoToParentInBothPanels.label = O úroveň vyššie v obidvoch paneloch GoToRoot.label = Koreňový priečinok SortByName.label = Zoradiť podľa názvu SortByDate.label = Zoradiť podľa dátumu SortBySize.label = Zoradiť podľa veľkosti SortByExtension.label = Zoradiť podľa prípony SortByPermissions.label = Zoradiť podľa oprávnení SortByOwner.label = Zoradiť podľa vlastníka SortByGroup.label = Zoradiť podľa skupiny MarkGroup.label = Označiť súbory MarkGroup.tooltip = Označiť skupinu súborov UnmarkGroup.label = Odznačiť súbory UnmarkGroup.tooltip = Odznačiť skupinu súborov MarkAll.label = Označiť všetko UnmarkAll.label = Odznačiť všetko MarkSelectedFile.label = Označiť/odznačiť MarkSelectedFile.tooltip = Označiť/odznačiť vybraný súbor MarkNextBlock.label = Označiť blok dole MarkPreviousBlock.label = Označiť blok hore MarkNextRow.label = Označiť riadok dole MarkPreviousRow.label = Označiť riadok hore MarkNextPage.label = Označiť po nasledujúcu stránku MarkPreviousPage.label = Označiť po predchádzajúcu stránku MarkToFirstRow.label = Označiť súbory po začiatok MarkToLastRow.label = Označiť súbory do konca MarkExtension.label = Označit príponu InvertSelection.label = Obrátiť výber SwapFolders.label = Vymeniť priečinky SwapFolders.tooltip = Vymeniť ľavý a pravý priečinok SetSameFolder.label = Nastaviť ten istý priečinok SetSameFolder.tooltip = Nastaviť ten istý priečinok vľavo aj vpravo NewWindow.label = Nové okno NewWindow.tooltip = Otvoriť nové okno Open.label = Otvoriť Open.tooltip = Otvoriť priečinok / Otvoriť archív / Vykonať OpenNatively.label = Otvoriť predvolenou aplikáciou OpenNatively.tooltip = Otvoriť vybraný súbor s asociovanou aplikáciou OpenInOtherPanel.label = Otvoriť v druhom panely OpenInBothPanels.label = Otvoriť v obidvoch paneloch RevealInDesktop.label = Otvoriť v aplikácii %1 RunCommand.label = Spustiť príkaz RunCommand.tooltip = Spustiť príkaz v aktuálnom priečinku Pack.tooltip = Skomprimovať označené súbory do archívu Unpack.tooltip = Dekomprimovať súbory z označených archívov ShowFileProperties.label = Vlastnosti ShowFileProperties.tooltip = Zobraziť vlastnosti označených súborov ShowPreferences.label = Nastavenia ShowPreferences.tooltip = Konfigurovať trolCommander ShowServerConnections.label = Zobratiť otvorené pripojenia Quit.label = Koniec ReverseSortOrder.label = Obrátiť poradie ToggleAutoSize.label = Automatická šírka stĺpcov Stop.label = Zastaviť zmenu priečinka ToggleColumn.show = Zobraziť stĺpec %1 ToggleColumn.hide = Zobraziť stĺpec %1 ToggleCommandBar.show = Zobraziť panel s príkazmi ToggleCommandBar.hide = Skryť panel s príkazmi ToggleToolBar.show = Zobraziť panel s nástrojmi ToggleToolBar.hide = Skryť panel s nástrojmi CustomizeCommandBar.label = Vlastné nastavenie panela príkazov ToggleStatusBar.show = Zobraziť stavový riadok ToggleStatusBar.hide = Skryť stavový riadok ToggleShowFoldersFirst.label = Zobraziť priečinky ako prvé ToggleTree.label = Zobraziť strom PopupLeftDriveButton.label = Zmeniť ľavý priečinok PopupRightDriveButton.label = Zmeniť pravý priečinok RecallPreviousWindow.label = Prepnúť do predchádzajúceho okna RecallNextWindow.label = Prepnúť do nasledujúceho okna RecallWindow.label = Prepnúť do okna BringAllToFront.label = Okná dopredu SwitchActiveTable.label = Prepnúť medzi pravým a ľavým panelom SelectNextBlock.label = Skok o jeden blok dole SelectPreviousBlock.label = Skok o jeden blok hore SelectNextPage.label = Skok o stranu dole SelectPreviousPage.label = Skok o stranu hore SelectNextRow.label = Skok o riadok dole SelectPreviousRow.label = Skok o riadok hore SelectFirstRow.label = Vybrať prvý súbor v aktuálnom priečinku SelectLastRow.label = Vybrať posledný súbor v označenom priečinku SplitEqually.label = Rozdeliť rovnomerne SplitVertically.label = Rozdeliť vertikálne SplitHorizontally.label = Rozdeliť horizontálne ShowKeyboardShortcuts.label = Klávesové skratky GoToWebsite.label = Webová stránka GoToForums.label = Diskusné fórum ReportBug.label = Ohlásiť chybu Donate.label = Darovať príspevok ShowAbout.label = O aplikácii trolCommander OpenTrash.label = Otvoriť kôš EmptyTrash.label = Kôš je prázdny CalculateChecksum.label = Kontrolný súčet MaximizeWindow.label = Maximalizovať GoToDocumentation.label = Dokumentácia online ShowParentFoldersQL.label = Nadradený priečinok ShowRecentLocationsQL.label = Predchádzajúce umiestnenia ShowRecentExecutedFilesQL.label = Skôr spustené súbory SplitFile.tooltip = Rozdeliť súbor na viacero častí CombineFiles.tooltip = Spojiť časti súboru do originálneho súboru ShowDebugConsole.label = Konzola pre ladenie FocusPrevious.label = Označiť predchádzajúcu komponentu FocusNext.label = Označiť nasledujúcu komponentu file_menu = Súbor file_menu.open_with = Otvoriť s mark_menu = Označiť view_menu = Zobraziť view_menu.show_hide_columns = Zobraziť/skryť stĺpce go_menu = Prejsť bookmarks_menu = Záložky bookmarks_menu.no_bookmark = Bez záložiek quick_lists_menu = Rýchle prehľady window_menu = Okno help_menu = Pomocník status_bar.selected_files = %1 z %2 vybraných status_bar.connecting_to_folder = Pripája sa na priečinok, stlač ESC pre zrušenie status_bar.volume_free = Voľné miesto: %1 status_bar.volume_capacity = Kapacita: %1 shortcuts_panel.title = Skratky shortcuts_panel.restore_defaults = Obnoviť pôvodné shortcuts_panel.show = Zobraziť shortcuts_panel.default_message = Stlačte Enter alebo dvojklik myšou na skratku, ktorú chcete upraviť shortcuts_table.action_description = Popis akcie shortcuts_table.shortcut = Skratka shortcuts_table.alternate_shortcut = Alternatívna skratka shortcuts_table.type_in_a_shortcut = Zadajte skratku command_bar_dialog.help = Pre prispôsobenie príkazového panelu presuňte tlačidlá table.folder_access_error_title = Chyba pri prístupe k priečinku table.folder_access_error = Obsah priečinka sa nedá čítať table.download_or_browse = Chcete súbor prezerať, alebo stiahnuť? version_dialog.no_new_version_title = Žiadna nová verzia version_dialog.no_new_version = Blahoželáme, máte najnovšiu verziu. version_dialog.new_version_title = Nová verzia k dispozícii version_dialog.new_version = Nová verzia trolCommander-a je k dispozícii. version_dialog.new_version_url = Nová verzia trolCommander-a je k dispozícii na %1. version_dialog.not_available_title = Server je nedostupný version_dialog.not_available = Nie je možné získať informácie o verzii zo serveru. version_dialog.install_and_restart = Nainštalovať a reštartovať version_dialog.preparing_for_update = Pripravujem aktualizáciu... quit_dialog.title = Ukončiť trolCommander quit_dialog.desc = Chcete zatvoriť všetkých %1 okien a ukončiť trolCommander? quit_dialog.show_next_time = Zobraziť nabudúce destination_dialog.file_exists_action = Predvolená akcia pre existujúci súbor destination_dialog.verify_integrity = Overiť integritu destination_dialog.skip_errors = Ignorovať chyby file_collision_dialog.title = Kolízia súboru rename_dialog.new_name = Nový názov copy_dialog.destination = Kopírovať vybrané súbory do copy_dialog.error_title = Chyba pri kopírovaní copy_dialog.copying = Kopírujem súbory copy_dialog.copying_file = Kopírujem %1 pack_dialog.packing = Komprimovať súbory pack_dialog.packing_file = Komprimujem %1 pack_dialog.error_title = Chyba pri kompresii pack_dialog_description = Pridať vybrané súbory do pack_dialog.archive_format = Formát archívu unpack_dialog.destination = Dekomprimovať vybrané súbory do unpack_dialog.error_title = Chyba pre dekompresii unpack_dialog.unpacking = Dekomprimujem súbory unpack_dialog.unpacking_file = Dekomprimujem %1 optimizing_archive = Archív sa optimalizuje %1 error_while_optimizing_archive = Chyba pri optimalizácii archívu %1 move_dialog.move_description = Presunúť do move_dialog.error_title = Chyba pri presúvaní move_dialog.moving = Presúvam súbory move_dialog.moving_file = Presúvam %1 download_dialog.description = Stiahnuť súbor do download_dialog.error_title = Chyba pri sťahovaní download_dialog.downloading = Sťahuje sa download_dialog.downloading_file = Sťahuje sa %1 mkfile_dialog.allocate_space = Alokovaný priestor delete_dialog.permanently_delete.confirmation = Natrvalo odstrániť vybraný súbor(y) ? delete_dialog.move_to_trash.confirmation = Odstrániť vybraný súbor(y) ? delete_dialog.move_to_trash.confirmation_details = Súbory budú premiestnené do koša. delete_dialog.move_to_trash.option = Premiestniť do koša delete_dialog.move_to_trash.failed = Jeden alebo viacero súborov nemohlo byť presunutých do koša. delete_dialog.deleting = Odstraňujem delete_dialog.error_title = Chyba pri odstraňovaní delete.deleting_file = Odstraňujem %1 email_dialog.prefs_not_set_title = Elektronická pošta nie je nastavená email_dialog.prefs_not_set = Najprv je potrebné nastaviť parametre elektronickej pošty email_dialog.from = Od email_dialog.to = Komu email_dialog.subject = Predmet email_dialog.send = Odoslať email_dialog.error_title = Chyba pri odosielaní email_dialog.read_error = Súbory vo vnorených priečinkoch sa nedajú čítať. email_dialog.sending = Odosielam súbory email.sending_file = Odosielam %1 email.connecting_to_server = Pripájam sa k %1 email.server_unavailable = Nedá sa pripojiť na poštový server %1, skontrolujte nastavenie pošty, alebo akciu opakujte neskôr. email.connection_closed = Server zatvoril pripojenie, pošta nebola odoslaná. email.goodbye_failed = Chyba pri zatváraní pripojenia, pošta nemusela byť odoslaná. email.send_file_error = Súbor %1 sa nedá odoslať, pošta nebola odoslaná. split_file_dialog.error_title = Chyba pri delení súboru split_file_dialog.file_to_split = Súbor na rozdelenie split_file_dialog.target_directory = Cieľový priečinok split_file_dialog.part_size = Veľkosť jednej časti split_file_dialog.parts = Počet častí split_file_dialog.generate_CRC = Generovať CRC súboru split_file_dialog.max_parts = Maximálny počet častí je %1 split_file_dialog.auto = Automaticky split_file_dialog.insert_new_media = Vložte nové médium combine_files_dialog.error_title = Chyba pri spájaní súboru combine_files_job.no_crc_file = Spojenie úspešné. Žiadny CRC súbor. combine_files_job.crc_read_error = Chyba pri čítaní CRC súboru. combine_files_job.crc_check_failed = Chybné CRC: očakávané %2, nájdené %1 combine_files_job.crc_ok = Spojenie úspešné. CRC ok. file_selection_dialog.mark = Označiť file_selection_dialog.unmark = Odznačiť file_selection_dialog.mark_description = Označiť súbory s názvom file_selection_dialog.unmark_description = Odznačiť súbory s názvom file_selection_dialog.case_sensitive = Rozlišovať veľké/malé písmená file_selection_dialog.include_folders = Zahrnúť priečinky progress_dialog.starting = Prenos spustený... progress_dialog.transferred = Prenesených %1 pri %2 progress_dialog.elapsed_time = Uplynulý čas progress_dialog.advanced = Rozšírené progress_dialog.current_speed = Aktuálna rýchlosť progress_dialog.limit_speed = Rýchlostný limit progress_dialog.close_when_finished = Zatvoriť okno po dokončení progress_dialog.processing_files = Spracovávajú sa súbory progress_dialog.processing_file = Spracováva sa %1 progress_dialog.verifying_file = Overujem %1 progress_dialog.job_finished = Úloha dokončená progress_dialog.job_error = Chyba properties_dialog.file_properties = Vlastnosti properties_dialog.contents = Obsah properties_dialog.calculating = počítam... calculate_checksum_dialog.checksum_algorithm = Algoritmus výpočtu calculate_checksum_dialog.temporary_file = Dočasný súbor change_date_dialog.now = Aktuálny change_date_dialog.specific_date = Špecifický dátum run_dialog.run_command_description = Spustiť v aktuálnom priečinku run_dialog.run_in_home_description = Spustiť v domovskom priečinku run_dialog.command_output = Výstup príkazu run_dialog.run = Spustiť run_dialog.clear_history = Zmazať históriu server_connect_dialog.server_type = Typ pripojenia server_connect_dialog.server = Server server_connect_dialog.share = Zdieľať server_connect_dialog.domain = Doména server_connect_dialog.username = Užívateľské meno server_connect_dialog.initial_dir = Počiatočný priečinok server_connect_dialog.port = Port server_connect_dialog.server_url = Adresa serveru server_connect_dialog.http_url = Adresa web stránky server_connect_dialog.connect = Pripojiť server_connect_dialog.protocol = Protokol server_connect_dialog.nfs_version = Verzia NFS server_connect_dialog.private_key = Súkromý kľúč server_connect_dialog.passphrase = Heslo ftp_connect.passive_mode = Povoliť pasívny mód ftp_connect.anonymous_user = Anonymný užívateľ ftp_connect.nb_connection_retries = Počet pokusov o pripojenie ftp_connect.retry_delay = Pauza medzi pokusmi (v sekundách) http_connect.basic_authentication = Základné overenie HTTP (voliteľné) server_connections_dialog.disconnect = Odpojiť server_connections_dialog.connection_busy = Zaneprázdnený server_connections_dialog.connection_idle = Neaktívny bonjour.bonjour_services = Služba Bonjour bonjour.no_service_discovered = Služba nenájdená bonjour.bonjour_disabled = Služba Bonjour nepovolená auth_dialog.title = Overenie auth_dialog.desc = Zadajte prosím užívateľské meno a heslo auth_dialog.server = Server auth_dialog.store_credentials = Uložiť užívateľské meno a heslo (slabé šifrovanie) auth_dialog.connect_as = Pripojiť ako auth_dialog.authentication_failed = Overenie zlyhalo sortable_list.move_up = Presunúť vyššie sortable_list.move_down = Presunúť nižšie add_bookmark_dialog.add = Pridať edit_bookmarks_dialog.new = Nová file_viewer.view_error_title = Chyba pri zobrazovaní file_viewer.view_error = Súbor sa nedá zobraziť. file_viewer.file_menu = Súbor file_viewer.close = Zatvoriť file_viewer.large_file_warning = Tento súbor je pre túto operáciu priveľký. file_viewer.open_anyway = Otvoriť napriek tomu text_viewer.edit = Upraviť text_viewer.copy = Kopírovať text_viewer.select_all = Označiť všetko text_viewer.find = Nájsť text_viewer.find_next = Nájsť nasledujúce text_viewer.find_previous = Nájsť predchádzajúce text_viewer.binary_file_warning = Toto je pravdepodobne binárny súbor image_viewer.controls_menu = Ovládanie image_viewer.zoom_in = Zväčšiť image_viewer.zoom_out = Zmenšiť file_editor.edit_error_title = Chyba pri úprave file_editor.edit_error = Súbor sa nedá upraviť. file_editor.save = Uložiť file_editor.save_as = Uložiť ako... file_editor.save_warning = Uložiť zmeny v tomto súbore pred zatvorením ? file_editor.cannot_write = Do súboru sa nedá zapisovať. text_editor.cut = Vystrihnúť text_editor.paste = Vložiť shortcuts_dialog.quick_search.start_search = Vložením ľubovoľného znaku spustíte rýchle hľadanie shortcuts_dialog.quick_search.cancel_search = Zrušiť rýchle hľadanie shortcuts_dialog.quick_search.remove_last_char = Odstrániť z hľadaného reťazca posledný znak shortcuts_dialog.quick_search.jump_to_previous = Prejsť na predchádzajúci výsledok rýchleho hľadania shortcuts_dialog.quick_search.jump_to_next = Prejsť na nasledujúci výsledok rýchleho hľadania shortcuts_dialog.quick_search.mark_jump_next = Označiť/odznačiť aktuálny súbor a prejsť na nasledujúci výsledok hľadania theme_editor.title = Editor témy theme_editor.folder_tab = Sekcia s priečinkom theme_editor.shell_tab = Príkazový riadok theme_editor.shell_history_tab = História shellu theme_editor.statusbar_tab = Stavový riadok theme_editor.free_space = Voľné miesto theme_editor.free_space.ok = OK theme_editor.free_space.warning = Varovné theme_editor.free_space.critical = Kritické theme_editor.locationbar_tab = Panel s umiestnením theme_editor.editor_tab = Editor súboru theme_editor.font = Písmo theme_editor.active_panel = Aktívne theme_editor.inactive_panel = Neaktívne theme_editor.general = Všeobecné theme_editor.could_not_save_theme = Nie je možné zapísať tému %1 theme_editor.border = Rám theme_editor.background = Pozadie theme_editor.alternate_background = Alternatívne pozadie theme_editor.unfocused_background = Pozadie (neaktívne) theme_editor.quick_search.unmatched_file = Jedinečný súbor theme_editor.text = Text theme_editor.progress = Priebeh theme_editor.normal = Bežný theme_editor.normal_unfocused = Bežný (neaktívny) theme_editor.selected = Vybraný theme_editor.selected_unfocused = Vybraný (neaktívny) theme_editor.color = Farba theme_editor.colors = Farby theme_editor.plain_file = Bežný súbor theme_editor.marked_file = Označený súbor theme_editor.hidden_file = Skrutý súbor theme_editor.folder = Priečinok theme_editor.archive_file = Archív theme_editor.symbolic_link = Symbolický odkaz theme_editor.header = Hlavička theme_editor.item = Položka command_bar_customize_dialog.available_actions = Dostupné akcie command_bar_customize_dialog.modifier = Modifikátor prefs_dialog.title = Nastavenia prefs_dialog.general_tab = Všeobecné prefs_dialog.day = Deň prefs_dialog.month = Mesiac prefs_dialog.year = Rok prefs_dialog.language = Jazyk (vyžaduje reštart) prefs_dialog.date_time = Formát dátumu a času prefs_dialog.time = Čas prefs_dialog.date = Dátum prefs_dialog.date_separator = Oddeľovač prefs_dialog.time_12_hour = 12 hodinový formát prefs_dialog.time_24_hour = 24 hodinový formát prefs_dialog.show_seconds = Zobraziť sekundy prefs_dialog.show_century = Zobraziť storočie prefs_dialog.check_for_updates_on_startup = Hľadať aktualizácie pri spustení prefs_dialog.show_splash_screen = Zobraziť uvítací obrázok prefs_dialog.folders_tab = Priečinok prefs_dialog.startup_folders = Priečinky pri štarte prefs_dialog.left_folder = Ľavý priečinok prefs_dialog.right_folder = Pravý priečinok prefs_dialog.last_folder = Naposledy navštívený priečinok prefs_dialog.custom_folder = Vlastný priečinok prefs_dialog.show_hidden_files = Zobraziť skryté súbory prefs_dialog.show_ds_store_files = Zobraziť súbory .DS_Store prefs_dialog.show_system_folders = Zobraziť systémové priečinky prefs_dialog.compact_file_size = Zaokrúhliť zobrazenú veľkosť súborov prefs_dialog.follow_symlinks_when_cd = Nasladovať symbolické odkazy pri zmenách aktuálneho priečinku prefs_dialog.appearance_tab = Vzhľad prefs_dialog.look_and_feel = Vzhľad & chovanie prefs_dialog.icons_size = Veľkosť ikon prefs_dialog.toolbar_icons = Panel s nástrojmi prefs_dialog.command_bar_icons = Panel príkazov prefs_dialog.file_icons = Typy súborov prefs_dialog.use_system_file_icons = Použiť systémové ikony prefs_dialog.use_system_file_icons.always = Vždy prefs_dialog.use_system_file_icons.never = Nikdy prefs_dialog.use_system_file_icons.applications = Iba pre aplikácie prefs_dialog.edit_current_theme = Upraviť aktuálnu tému... prefs_dialog.themes = Témy prefs_dialog.import_theme = Import témy prefs_dialog.import_look_and_feel = Importovať vzhľad & chovanie prefs_dialog.no_look_and_feel = Žiadny vzhľad & chovanie nebol nájdený. prefs_dialog.error_in_import = Chyba pri importovaní témy %1. prefs_dialog.cannot_read_theme = Téma %1 sa nedá načítať prefs_dialog.export_theme = Export %1 prefs_dialog.import = Import prefs_dialog.export = Export prefs_dialog.theme_type = Typ: %1 prefs_dialog.delete_theme = Natrvalo odstrániť tému %1 ? prefs_dialog.delete_look_and_feel = Navždy odstrániť vzhľad & chovanie %1 ? prefs_dialog.rename_failed = Chyba pri premenovaní témy %1 prefs_dialog.xml_file = XML súbor prefs_dialog.jar_file = JAR súbor prefs_dialog.mail_tab = E-mail prefs_dialog.mail_settings = Nastavenie odchádzajúcej pošty prefs_dialog.mail_name = Vaše meno prefs_dialog.mail_address = Váš e-mail prefs_dialog.mail_server = Server SMTP prefs_dialog.misc_tab = Rôzne prefs_dialog.use_brushed_metal = Použiť vzhľad 'brushed metal' (vyžaduje reštart) prefs_dialog.confirm_on_quit = Pri ukončovaní zobraziť potvrdzujúci dialóg prefs_dialog.default_shell = Použiť preddefinovaný systémový shell prefs_dialog.custom_shell = Použiť vlastný shell prefs_dialog.shell_encoding = Kódovanie textu shellu prefs_dialog.auto_detect_shell_encoding = Automatické rozpoznávanie prefs_dialog.enable_bonjour_discovery = Povoliť služby Bonjour discoveny prefs_dialog.enable_system_notifications = Povoliť systémové oznámenia (v system tray) debug_console_dialog.level = Úroveň unit.byte = bajt unit.bytes = bajty unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mesiacov duration.years = %1rokov theme.custom_theme = Užívateľská téma theme.custom = Prispôsobená theme.built_in = Vstavaná theme.add_on = Prídavná theme.current = aktuálna theme_could_not_be_loaded = Chyba pri načítavaní témy. setup.title = Víta Vás trolCommander setup.intro = Prosím, zvoľte vlastné nastavenie aplikácie trolCommander. setup.look_and_feel = Vyberte Vzhľad & chovanie setup.theme = Vyberte tému font_chooser.font_size = Veľkosť font_chooser.font_bold = Tučné font_chooser.font_italic = Šikmé color_chooser.red = Červená color_chooser.green = Zelená color_chooser.blue = Modrá color_chooser.hue = Hue color_chooser.brightness = Jas color_chooser.swatches = Vzorka color_chooser.saturation = Sýtosť color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Naposledy použité color_chooser.alpha = Alpha priehľadnosť color_chooser.title = Vyberte farbu batch_rename_dialog.mask = Maska pre premenovanie batch_rename_dialog.search_replace = Nájsť & nahradiť batch_rename_dialog.search_for = Hľadaný výraz batch_rename_dialog.replace_with = Nahradiť s batch_rename_dialog.counter = Počítadlo batch_rename_dialog.start_at = Počítať od batch_rename_dialog.step_by = Prírastok batch_rename_dialog.format = Formát batch_rename_dialog.upper_lower_case = Veľkosť písmen batch_rename_dialog.no_change = Bez zmeny batch_rename_dialog.lower_case = malé písmená batch_rename_dialog.upper_case = VELKÉ PÍSMENÁ batch_rename_dialog.first_upper = Prvé písmeno veľké batch_rename_dialog.word = Prvé Písmená Veľké batch_rename_dialog.old_name = Pôvodný názov batch_rename_dialog.new_name = Nový názov batch_rename_dialog.block_name = Zachovať batch_rename_dialog.range = Rozsah batch_rename_dialog.proceed_renaming = %1 z %2 souborov bude premenovaných. Chcete pokračovat? batch_rename_dialog.duplicate_names = Rovnaký názov sa už vyskytuje! parent_folders_quick_list.empty_message = Umiestnenie nemá nadradený priečinok recent_locations_quick_list.empty_message = Žiadne skôr navštívené umiestnenia neboli nájdené recent_executed_files_quick_list.empty_message = Žiadne skôr spustené súbory neboli nájdené #move_dialog.rename_description = Premenovať súbor na #theme_editor.shell_font = Písmo príkazového riadku #theme_editor.history_font = Písmo histórie #theme_editor.shell_colors = Farba terminálu (shell) #theme_editor.history_colors = Farba histórie #auth_dialog.error_was = Chyba: %1 #table.hide_column = Skryť stĺpec #delete.symlink_warning_title = Nájdený symbolický odkaz #delete.symlink_warning = Tento súbor vyzerá ako symbolický odkaz:\n\n Súbor:%1\n Odkazuje na: %2\n\nOdstrániť iba symbolický odkaz, alebo\naj cieľ odkazu (UPOZORNENIE) ? #delete.delete_link_only = Odstrániť odkaz #delete.delete_linked_folder = Odstrániť priečinok #Unpack.label = Dekomprimovať súbory #Pack.label = Skomprimovať súbory ================================================ FILE: src/main/resources/dictionary_sl_SL.properties ================================================ ok = V redu yes = Da no = Ne cancel = Prekliči edit = Uredi close = Zapri reset = Resetiraj rename = Preimenuj apply = Potrdi change = Spremeni save = Shrani dont_save = Ne shrani replace = Zamenjaj dont_replace = Ne zamenjaj delete = Izbriši skip = Preskoči retry = Poskusi ponovno resume = Nadaljuj overwrite = Prepiši overwrite_if_older = Prepiši starejše duplicate = Podvoji apply_to_all = Uporabi za vse copy = Kopiraj move = Premakni pack = Stiskanje unpack = Razširjanje download = Shrani na disk browse = Prebrskaj ask = Vprašaj stop = Ustavi pause = Pavza quick_search = Hitro iskanje file_manager = Urejevalnik datotek create = Ustvari creating_file = Kreiram %1 choose = Izberi choose_folder = Izberi mapo login = Uporabniško ime password = Geslo user = Uporabnik encoding = Kodiranje license = Licenca name = Ime size = Velikost date = Datum extension = Končnica permissions = Pravice owner = Lastnik group = Skupina location = Lokacija untitled = Brez naslova source = Izvor destination = Cilj recurse_directories = Recurse directories go_to = Pojdi v example = Primer preview = Predogled comment = Komentar sample_text = Vzorec besedila nb_files = %1 datoteka(e) nb_folders = %1 mapa(e) loading = Nalagam... this_operation_cannot_be_undone = Ta operacija ne more biti razveljavljena. remove = Odstrani warning = Opozorilo error = Napaka generic_error = Med izvajanjem zahtevane operacije se je zgodila napaka. folder_does_not_exist = Ta mapa ne obstaja oz. ni dostopna. this_folder_does_not_exist = Mapa ne obstaja oz. ni dostopna: %1 this_file_does_not_exist = Datoteka ne obstaja oz. ni dostopna: %1 invalid_path = Napačna pot: %1 directory_already_exists = Mapa %1 že obstaja. file_exists_in_destination = Datoteka s tem imenom že obstaja source_parent_of_destination = Poskus prenosa mape v eno od njenih podmap cannot_read_file = Ne morem prebrati datoteke %1 cannot_write_file = Ne morem zapisati datoteke %1 cannot_create_folder = Ne morem ustvariti mape %1 cannot_read_folder = Ne morem prebrati vsebine mape %1 cannot_delete_file = Ne morem izbrisati datoteke %1 cannot_delete_folder = Ne morem izbrisati mape %1 error_while_transferring = Napaka pri prenosu datoteke %1 same_source_destination = Izvorna in ciljna mapa sta identični file_already_exists = %1 že obstaja, ali jo želiš zamenjati? write_error = Napaka pri zapisovanju read_error = Napaka pri branju integrity_check_error = Napaka pri preverjanju integritete: izvor in cilj se ne ujemata permissions.read = Branje permissions.write = Pisanje permissions.executable = Izvršitev permissions.group = Skupina permissions.other = Ostalo permissions.octal_notation = Oktanski sistem action_categories.navigation = Navigacija action_categories.selection = Izbira action_categories.view = Pogled action_categories.file_operations = Operacije z datotekami action_categories.windows = Okna Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Dodaj med priljubljene AddBookmark.tooltip = Dodaj trenutno mapo med priljubljene BatchRename.label = Preimenuj sveženj EditBookmarks.label = Uredi priljubljene ExploreBookmarks.label = Razišči Priljubljene EditCredentials.label = Uredi akreditive ChangeLocation.label = Spremeni trenutno lokacijo ChangeDate.label = Spremeni datum ChangeDate.tooltip = Spremeni datum izbranim datotekam ChangePermissions.label = Spremeni pravice ChangePermissions.tooltip = Spremeni pravice izbranih datotek CheckForUpdates.label = Nadgradnja programa CompareFolders.label = Primerjaj mapi ConnectToServer.label = Povezava s strežnikom ConnectToServer.tooltip = Poveži z oddaljenim strežnikom View.label = Pogled View.tooltip = Poglej izbrano datoteko Edit.tooltip = Uredi izbrano datoteko Copy.tooltip = Kopiraj označene datoteke LocalCopy.label = Lokalno kopiranje LocalCopy.tooltip = Kopiraj označeno datoteko v trenutno mapo Move.tooltip = Premakni označene datoteke Rename.tooltip = Preimenuj označeno datoteko Mkdir.label = Nova mapa Mkdir.tooltip = Ustvari mapo v trenutni mapi Mkfile.label = Ustvari datoteko Mkfile.tooltip = Ustvari datoteko v trenutni mapi Delete.tooltip = Izbriši označene datoteke z uporabo koša PermanentDelete.label = Trajno brisanje PermanentDelete.tooltip = Izbriši označene datoteke brez uporabe koša Refresh.label = Osveži Refresh.tooltip = Osveži trenutno mapo CloseWindow.label = Zapri okno CloseWindow.tooltip = Zapri okno CopyFileNames.label = Kopiraj ime/-na CopyFilePaths.label = Kopiraj pot/-i CopyFilesToClipboard.label = Kopiraj datoteko/-e PasteClipboardFiles.label = Prilepi datoteko/-e Email.label = Pošiljanje prek e-pošte Email.tooltip = Pošlji označene datoteke kot priponko e-pošte GoBack.label = Nazaj GoBack.tooltip = Prejšnja mapa GoForward.label = Naprej GoForward.tooltip = Naslednja mapa GoToHome.label = Pojdi v domačo mapo GoToParent.label = Izvor GoToParent.tooltip = Izvorna mapa GoToParentInOtherPanel.label = Pojdi v izvorno mapo v drugem oknu GoToParentInBothPanels.label = Pojdi v izvorno mapo v obeh oknih GoToRoot.label = Pojdi v koren SortByName.label = Razvrsti po imenu SortByDate.label = Razvrsti po datumu SortBySize.label = Razvrsti po velikosti SortByExtension.label = Razvrsti po podaljšku SortByPermissions.label = Razvrsti po pravicah SortByOwner.label = Razvrsti po lastniku SortByGroup.label = Razvrsti po skupini MarkGroup.label = Označi datoteke MarkGroup.tooltip = Označi skupino datotek UnmarkGroup.label = Odznači datoteke UnmarkGroup.tooltip = Odznači skupino datotek MarkAll.label = Označi vse UnmarkAll.label = Odznači vse MarkSelectedFile.label = Označi/Odznači MarkSelectedFile.tooltip = Označi/Odznači izbrano datoteko MarkNextPage.label = Označi datoteke do naslednje strani MarkPreviousPage.label = Označi datoteke od prejšnje strani MarkToFirstRow.label = Označi datoteke od začetka MarkToLastRow.label = Označi datoteke do konca MarkExtension.label = Označi končnico InvertSelection.label = Obrni izbor SwapFolders.label = Zamenjaj mapi SwapFolders.tooltip = Zamenjaj levo in desno okno SetSameFolder.label = Nastavi isto mapo SetSameFolder.tooltip = Nastavi enako mapo v levem in desnem oknu NewWindow.label = Novo okno NewWindow.tooltip = Odpri v novem oknu Open.label = Odpri Open.tooltip = Odpri mapo / Odpri arhiv / Zaženi OpenNatively.label = Odpri izvorno OpenNatively.tooltip = Zaženi izbrano datoteko OpenInOtherPanel.label = Odpri v drugem oknu OpenInBothPanels.label = Odpri v obeh oknih RevealInDesktop.label = Razkrij v %1 RunCommand.label = Ukazna vrstica RunCommand.tooltip = Zaženi ukaz s trenutne mape Pack.tooltip = Dodaj označene datoteke v arhiv Unpack.tooltip = Razširi označene arhivske datoteke ShowFileProperties.label = Lastnosti ShowFileProperties.tooltip = Prikaži podrobnosti označenih datotek ShowPreferences.label = Nastavitve ShowPreferences.tooltip = Konfiguriraj trolCommander ShowServerConnections.label = Prikaži aktivne povezave Quit.label = Izhod ReverseSortOrder.label = Obrni vrstni red ToggleAutoSize.label = Samodejna širina stolpcev Stop.label = Prekini spreminjanje mape ToggleCommandBar.show = Prikaži ukazno vrstico ToggleCommandBar.hide = Skrij ukazno vrstico ToggleToolBar.show = Prikaži orodno vrstico ToggleToolBar.hide = Skrij orodno vrstico ToggleStatusBar.show = Prikaži statusno vrstico ToggleStatusBar.hide = Skrij statusno vrstico ToggleShowFoldersFirst.label = Najprej prikaži mape ToggleTree.label = Prikaži drevesno strukturo PopupLeftDriveButton.label = Zamenjaj mapo na levi strani PopupRightDriveButton.label = Zamenjaj mapo na desni strani RecallPreviousWindow.label = Preklopi na prejšnje okno RecallNextWindow.label = Preklopi na naslednje okno RecallWindow.label = Prikliči okno #%1 BringAllToFront.label = Postavi vse v ospredje SwitchActiveTable.label = Preklopi med levo in desno stranjo SelectFirstRow.label = Izberi prvo datoteko v trenutni mapi SelectLastRow.label = Izberi zadnjo datoteko v trenutni mapi SplitEqually.label = Razdeli enako na obeh straneh SplitVertically.label = Razdeli navpično SplitHorizontally.label = Razdeli vodoravno ShowKeyboardShortcuts.label = Funkcije na tipkovnici GoToWebsite.label = Domača stran trolCommander GoToForums.label = Forum ReportBug.label = Poročanje o napakah Donate.label = Donacije ShowAbout.label = O programu trolCommander OpenTrash.label = Odpri koš EmptyTrash.label = Izprazni koš CalculateChecksum.label = Izračunaj vsoto MaximizeWindow.label = Povečaj MaximizeWindow.label.mac_os_x = Povečaj MinimizeWindow.label = Minimiziraj GoToDocumentation.label = Dokumentacija na spletu ShowParentFoldersQL.label = Izvorne mape ShowRecentLocationsQL.label = Nedavne lokacije ShowRecentExecutedFilesQL.label = Nedavno izvršene datoteke file_menu = Datoteka file_menu.open_with = Odpri z mark_menu = Označevanje view_menu = Pogled view_menu.show_hide_columns = Prikaži/Skrij stolpce go_menu = Pojdi bookmarks_menu = Priljubljene bookmarks_menu.no_bookmark = Ni priljubljenih quick_lists_menu = Hitri seznam window_menu = Okno help_menu = Pomoč status_bar.selected_files = %1 od %2 datotek status_bar.connecting_to_folder = Povezovanje z mapo, pritisni ESCAPE za preklic. status_bar.volume_free = Nezasedeno: %1 status_bar.volume_capacity = Kapaciteta: %1 table.folder_access_error_title = Napaka pri dostopu do mape table.folder_access_error = Ne morem prebrati vsebine mape table.download_or_browse = BI rad prebrskal ali prenesel na disk to datoteko? version_dialog.no_new_version_title = Ni novejše različice version_dialog.no_new_version = Sedaj imate zadnjo različico. version_dialog.new_version_title = Nova različica je na voljo version_dialog.new_version = Nova različica trolCommander-ja je na razpolago. version_dialog.new_version_url = Nova različica trolCommander-ja se nahaja na %1. version_dialog.not_available_title = Strežnik ni dosegljiv version_dialog.not_available = Ne morem dobiti informacij o različici s strežnika. version_dialog.install_and_restart = Namesti in ponovno zaženi version_dialog.preparing_for_update = Pripravljam za nadgradnjo... quit_dialog.title = Izhod iz trolCommander-ja quit_dialog.desc = Odprto/-ih je %1 okno/-en. Želite zapreti vsa okna? quit_dialog.show_next_time = Prikaži naslednjič destination_dialog.file_exists_action = Privzeto dejanje, ko datoteka obstaja destination_dialog.verify_integrity = Preverjanje integritete podatkov file_collision_dialog.title = Datotečni spor rename_dialog.new_name = Novo ime copy_dialog.destination = Kopiraj izbrano v copy_dialog.error_title = Napaka pri kopiranju copy_dialog.copying = Kopiram datoteke copy_dialog.copying_file = Kopiram %1 pack_dialog.packing = Stiskanje datotek pack_dialog.packing_file = Stiskanje %1 pack_dialog.error_title = Napaka pri stiskanju pack_dialog_description = Dodaj izbrane datoteke v pack_dialog.archive_format = Način stiskanja unpack_dialog.destination = Razširi izbrano v unpack_dialog.error_title = Napaka pri razširjanju unpack_dialog.unpacking = Razširjam datoteke unpack_dialog.unpacking_file = Razširjam %1 optimizing_archive = Optimiziranje arhiva %1 error_while_optimizing_archive = Napaka pri optimizaciji arhiva %1 move_dialog.move_description = Premakni v move_dialog.error_title = Napaka pri premikanju move_dialog.moving = Premikam datoteke move_dialog.moving_file = Premikanje %1 download_dialog.description = Prenesi datoteko v download_dialog.error_title = Napaka pri prenosu download_dialog.downloading = Prenašam na disk download_dialog.downloading_file = Prenašam %1 delete_dialog.permanently_delete.confirmation = Trajno izbrišem datoteko(-e)? delete_dialog.move_to_trash.confirmation = Izbrišem izbrane datoteke? delete_dialog.move_to_trash.confirmation_details = Datoteke bodo premaknjene v koš. delete_dialog.move_to_trash.option = Premakni v koš delete_dialog.deleting = Brisanje delete_dialog.error_title = Napaka pri brisanju delete.deleting_file = Brisanje %1 email_dialog.prefs_not_set_title = E-pošta ni nastavljena email_dialog.prefs_not_set = Vnesite parametre za e-pošto. email_dialog.from = Pošiljatelj email_dialog.to = Prejemnik email_dialog.subject = Zadeva email_dialog.send = Pošlji email_dialog.error_title = Napaka pri pošiljanju email_dialog.read_error = Ne morem prebrati dateteke v podmapi. email_dialog.sending = Pošiljam datoteke email.sending_file = Pošiljam %1 email.connecting_to_server = Povezovanje z %1 email.server_unavailable = Ne morem se povezati z e-poštnim strežnikom %1, preverite nastavitve ali poskusite kasneje. email.connection_closed = Povezava s strežnikom zaključena, sporočilo ni poslano. email.goodbye_failed = Napaka pri zaključevanju povezave, sporočilo morda ni poslano. email.send_file_error = Ne morem poslati datoteke %1, sporočilo ni poslano. file_selection_dialog.mark = Označi file_selection_dialog.unmark = Odznači file_selection_dialog.mark_description = Označi datoteke z imenom file_selection_dialog.unmark_description = Odznači datoteke z imenom file_selection_dialog.case_sensitive = Občutljiv na velike in male črke file_selection_dialog.include_folders = Vključi mape progress_dialog.starting = Prenos se izvaja... progress_dialog.transferred = Prenešeno %1 od %2 progress_dialog.elapsed_time = Pretečen čas progress_dialog.advanced = Napredno progress_dialog.current_speed = Trenutna hitrost progress_dialog.limit_speed = Omejitev hitrosti progress_dialog.close_when_finished = Po prenosu zapri okno progress_dialog.processing_files = Obdelujem datoteke progress_dialog.processing_file = Obdelujem %1 progress_dialog.verifying_file = Preverjam %1 progress_dialog.job_finished = Naloga zaključena progress_dialog.job_error = Napaka pri izvršitvi naloge properties_dialog.file_properties = %1 Lastnosti properties_dialog.contents = Vsebina properties_dialog.calculating = izračunavam... calculate_checksum_dialog.checksum_algorithm = Algoritem calculate_checksum_dialog.temporary_file = Začasna datoteka change_date_dialog.now = Trenutni change_date_dialog.specific_date = Določi datum run_dialog.run_command_description = Zaženi v trenutni mapi run_dialog.run_in_home_description = Zaženi v domači mapi run_dialog.command_output = Rezultat ukaza run_dialog.run = Zaženi run_dialog.clear_history = Počisti zgodovino server_connect_dialog.server_type = Tip povezave server_connect_dialog.server = Strežnik server_connect_dialog.share = Share server_connect_dialog.username = Uporabniško ime server_connect_dialog.initial_dir = Začetna mapa server_connect_dialog.port = Vrata server_connect_dialog.server_url = URL strežnika server_connect_dialog.http_url = Naslov spletne strani server_connect_dialog.connect = Poveži server_connect_dialog.protocol = Protokol server_connect_dialog.nfs_version = NFS različica server_connect_dialog.private_key = Zasebni ključ server_connect_dialog.passphrase = Geslo ftp_connect.passive_mode = Omogoči pasivni način ftp_connect.anonymous_user = Anonimni uporabnik ftp_connect.nb_connection_retries = Število poskusov povezave ftp_connect.retry_delay = Zamik med poskusi (v sekundah) http_connect.basic_authentication = HTTP osnovno overjanje (izbirno) server_connections_dialog.disconnect = Prekini povezavo server_connections_dialog.connection_busy = Zasedeno server_connections_dialog.connection_idle = Neaktivno bonjour.bonjour_services = "Bonjour" storitve bonjour.no_service_discovered = Ni aktivne storitve bonjour.bonjour_disabled = "Bonjour" onemogočen auth_dialog.title = Avtentikacija auth_dialog.desc = Vnesite uporabniško ime in geslo auth_dialog.server = Strežnik auth_dialog.store_credentials = Shrani uporabniško ime in geslo (nizka enkripcija) auth_dialog.connect_as = Poveži kot auth_dialog.authentication_failed = Avtentikacija neuspešna sortable_list.move_up = Premakni gor sortable_list.move_down = Premakni dol add_bookmark_dialog.add = Dodaj edit_bookmarks_dialog.new = Nova file_viewer.view_error_title = Napaka pri pregledu file_viewer.view_error = Ne morem pregledati datoteke. file_viewer.file_menu = Datoteka file_viewer.close = Zapri file_viewer.large_file_warning = Datoteka je morda prevelika za to opreacijo. file_viewer.open_anyway = Odpri vseeno text_viewer.edit = Uredi text_viewer.copy = Kopiraj text_viewer.select_all = Izberi vse text_viewer.find = Najdi text_viewer.find_next = Najdi naslednje text_viewer.find_previous = Najdi prejšnje text_viewer.binary_file_warning = Kaže se kot binarna datoteka image_viewer.controls_menu = Kontrolne tipke image_viewer.zoom_in = Približaj image_viewer.zoom_out = Oddalji file_editor.edit_error_title = Napaka pri urejanju file_editor.edit_error = Ne morem urejati datoteke. file_editor.save = Shrani file_editor.save_as = Shrani kot ... file_editor.save_warning = Shranim spremembe pred zapiranjem? file_editor.cannot_write = Ne morem zapisati datoteke. text_editor.cut = Izreži text_editor.paste = Prilepi shortcuts_dialog.quick_search.start_search = Vnesi kateri koli znak za začetek hitrega iskanja shortcuts_dialog.quick_search.cancel_search = Prekliči hitro iskanje shortcuts_dialog.quick_search.remove_last_char = Odstrani zadnji znak iz vrstice hitrega iskanja shortcuts_dialog.quick_search.jump_to_previous = Preskoči na prejšnji rezultat hitrega iskanja shortcuts_dialog.quick_search.jump_to_next = Preskoči na naslednji rezultat hitrega iskanja shortcuts_dialog.quick_search.mark_jump_next = Označi/Odznači trenutno datoteko in preskoči na naslednji rezultat iskanja theme_editor.title = Urejevalnik tem theme_editor.folder_tab = Vrstica z mapami theme_editor.shell_tab = Ogrodje theme_editor.shell_history_tab = Ogrodje zgodovine theme_editor.statusbar_tab = Statusna vrstica theme_editor.free_space = Nezaseden prostor theme_editor.free_space.ok = OK theme_editor.free_space.warning = Opozorilo theme_editor.free_space.critical = Kritično theme_editor.locationbar_tab = Lokacijska vrstica theme_editor.editor_tab = Urejevalnik datotek theme_editor.font = Pisava theme_editor.active_panel = Aktivno theme_editor.inactive_panel = Neaktivno theme_editor.general = Splošno theme_editor.could_not_save_theme = Ne morem zapisati teme %1 theme_editor.border = Okvir theme_editor.background = Ozadje theme_editor.alternate_background = Ozadje (izmenično) theme_editor.unfocused_background = Ozadje theme_editor.quick_search.unmatched_file = Neustrezna datoteka theme_editor.text = Pisava theme_editor.progress = Napredek theme_editor.normal = Normalno theme_editor.normal_unfocused = Normalno (without focus) theme_editor.selected = Izbrano theme_editor.selected_unfocused = Izbrano (without focus) theme_editor.color = Barva theme_editor.colors = Barve theme_editor.plain_file = Navadna datoteka theme_editor.marked_file = Označena datoteka theme_editor.hidden_file = Skrita datoteka theme_editor.folder = Mapa theme_editor.archive_file = Arhivska datoteka theme_editor.symbolic_link = Simbolna povezava prefs_dialog.title = Lastnosti prefs_dialog.general_tab = Splošno prefs_dialog.day = Dan prefs_dialog.month = Mesec prefs_dialog.year = Leto prefs_dialog.language = Jezik (zahteva ponovni zagon programa) prefs_dialog.date_time = Format datuma in časa prefs_dialog.time = Čas prefs_dialog.date = Datum prefs_dialog.date_separator = Ločilo prefs_dialog.time_12_hour = 12-urni format prefs_dialog.time_24_hour = 24-urni format prefs_dialog.show_seconds = Prikaži sekunde prefs_dialog.show_century = Prikaži stoletje prefs_dialog.check_for_updates_on_startup = Preglej za posodobitve ob zagonu prefs_dialog.show_splash_screen = Prikaži brizgana okna prefs_dialog.folders_tab = Mape prefs_dialog.startup_folders = Mape ob zagonu prefs_dialog.left_folder = Levo okno prefs_dialog.right_folder = Desno okno prefs_dialog.last_folder = Zadnja odprta mapa prefs_dialog.custom_folder = Mapa po meri prefs_dialog.show_hidden_files = Prikaži skrite datoteke prefs_dialog.show_ds_store_files = Prikaži .DS arhivske datoteke prefs_dialog.show_system_folders = Prikaži sistemske mape prefs_dialog.compact_file_size = Zaokroži prikazane velikosti datotek prefs_dialog.follow_symlinks_when_cd = Sledi simbolnim povezavam, ko spremeniš trenutno mapo prefs_dialog.appearance_tab = Videz prefs_dialog.look_and_feel = Shema prefs_dialog.icons_size = Velikost ikone prefs_dialog.toolbar_icons = Orodna vrstica prefs_dialog.command_bar_icons = Ukazna vrstica prefs_dialog.file_icons = Vrste datotek prefs_dialog.use_system_file_icons = Uporabi sistemske ikone prefs_dialog.use_system_file_icons.always = Vedno prefs_dialog.use_system_file_icons.never = Nikoli prefs_dialog.use_system_file_icons.applications = Samo za aplikacije prefs_dialog.edit_current_theme = Uredi trenutno temo... prefs_dialog.themes = Teme prefs_dialog.import_theme = Uvoz teme prefs_dialog.import_look_and_feel = Uvozi "Look & feel" prefs_dialog.no_look_and_feel = Ne najdem "Look & feel" prefs_dialog.error_in_import = Napaka pri uvozu teme %1. prefs_dialog.cannot_read_theme = Ne morem naložiti teme iz datoteke %1 prefs_dialog.export_theme = Izvoz %1 prefs_dialog.import = Uvoz prefs_dialog.export = Izvoz prefs_dialog.theme_type = Vrsta: %1 prefs_dialog.delete_theme = Trajno izbrišem temo %1? prefs_dialog.delete_look_and_feel = Trajno izbrišem "look & feel" %1 ? prefs_dialog.rename_failed = Ne morem preimenovati teme %1 prefs_dialog.xml_file = XML datoteka prefs_dialog.jar_file = JAR datoteka prefs_dialog.mail_tab = E-pošta prefs_dialog.mail_settings = Nastavitve odhodne pošte prefs_dialog.mail_name = Ime prefs_dialog.mail_address = Naslov vaše e-pošte prefs_dialog.mail_server = SMTP strežnik prefs_dialog.misc_tab = Razno prefs_dialog.use_brushed_metal = Uporabi shemo "brušena kovina" (zahteva ponovni zagon) prefs_dialog.confirm_on_quit = Pokaži pogovorno okno za potrditev izhoda prefs_dialog.default_shell = Uporabi privzeto izvajanje ukazov prefs_dialog.custom_shell = Uporabi izvajanje ukazov po meri prefs_dialog.shell_encoding = Kodiranje ogrodja prefs_dialog.auto_detect_shell_encoding = Samodejno zaznaj prefs_dialog.enable_bonjour_discovery = Omogoči odkrivanje "Bonjour" storitev prefs_dialog.enable_system_notifications = Omogoči sistemska opozorila unit.byte = bajt unit.bytes = bajtov unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1min duration.hours = %1h duration.days = %1d duration.months = %1m duration.years = %1l theme.custom_theme = Običajna tema theme.custom = Prirejeno theme.built_in = Vgrajeno theme.add_on = Dodatek theme.current = Trenutno theme_could_not_be_loaded = Napaka med nalaganjem teme. setup.title = Dobrodošli v trolCommanderju setup.intro = Izberite prosim lastnosti trolCommanderja. setup.look_and_feel = Izberite temo in videz setup.theme = Izberite temo font_chooser.font_size = Velikost font_chooser.font_bold = Krepko font_chooser.font_italic = Poševno color_chooser.red = Rdeča color_chooser.green = Zelena color_chooser.blue = Modra color_chooser.hue = Odtenek color_chooser.brightness = Svetlost color_chooser.swatches = Swatches color_chooser.saturation = Zasičenost color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Nedaven color_chooser.alpha = Alfa prepustnost color_chooser.title = Izberi barvo batch_rename_dialog.mask = Preimenuj vzorec batch_rename_dialog.search_replace = Išči in zamenjaj batch_rename_dialog.search_for = Iskanje za batch_rename_dialog.replace_with = Zamenjaj z batch_rename_dialog.counter = Števec batch_rename_dialog.start_at = Začni v batch_rename_dialog.step_by = Korak po batch_rename_dialog.format = Formatiraj batch_rename_dialog.upper_lower_case = Velike/Male črke batch_rename_dialog.no_change = Nespremenjeno batch_rename_dialog.lower_case = male črke batch_rename_dialog.upper_case = VELIKE ČRKE batch_rename_dialog.first_upper = Velika začetnica batch_rename_dialog.word = Velika Začetnica Vsake Besede batch_rename_dialog.old_name = Staro ime batch_rename_dialog.new_name = Novo ime batch_rename_dialog.block_name = Ohrani batch_rename_dialog.range = Razpon batch_rename_dialog.proceed_renaming = %1 datotek od %2 bo preimenovanih. Nadaljujem? batch_rename_dialog.duplicate_names = Podvojena imena! parent_folders_quick_list.empty_message = Trenutna lokacija nima izvora recent_locations_quick_list.empty_message = Ni nedavne lokacije recent_executed_files_quick_list.empty_message = Ni nedavno zagnanih datotek #server_connect_dialog.auth_error = Napačno uporabniško ime ali geslo #move_dialog.cannot_move_to_itself = Ne morem premakniti datotek v podmapo #pack.error_on_file = Napaka med stiskanjem %1 #pack_dialog.cannot_write = Ne morem ustvariti stisnjene datoteke v ciljni mapi. #unpack.unable_to_open_zip = Ne morem odpreti stisnjene datoteke %1. #image_viewer.previous_image = Prejšnja slika #image_viewer.next_image = Naslednja slika #mkdir_dialog.title = Nova mapa #mkdir_dialog.error_title = Napaka pri ustvarjanju mape #edit_bookmarks_dialog.remove = Odstrani #mkdir_dialog.description = Ustvari novo mapo #mkfile_dialog.description = Ustvari novo prazno datoteko #done = Narejeno #progress_dialog.hide = Skrij #move_dialog.rename_description = Preimenuj datoteko v #theme_editor.shell_font = Pisava ogrodja #theme_editor.history_font = Pisava zgodovine #theme_editor.shell_colors = Barve v ukazni vrstici #theme_editor.history_colors = Barve v menuju Zgodovina #ToggleHiddenFiles.hide = Skrij skrite datoteke #auth_dialog.error_was = Napaka: %1 #table.hide_column = Skrij stolpec #delete.symlink_warning_title = Najdena simbolična povezava #delete.symlink_warning = Datoteka jo podobna simbolični povezavi:\n\n File: %1\n Povezava z: %2\n\nIzbriši simbolično povezavo ali\nSledi simbolični povezavi in izbriši mapo? #delete.delete_link_only = Izbriši povezavo #delete.delete_linked_folder = Izbriši mapo #Unpack.label = Razširjanje datotek #Pack.label = Stiskanje datotek ================================================ FILE: src/main/resources/dictionary_sv_SV.properties ================================================ ok = OK yes = Ja no = Nej cancel = Avbryt edit = Redigera close = Stäng reset = Rensa rename = Döp om apply = Utför change = Ändra save = Spara dont_save = Spara inte replace = Ersätt dont_replace = Ersätt inte delete = Radera skip = Hoppa över skip_all = Hoppa över alla retry = Försök igen resume = Återuppta overwrite = Skriv över overwrite_if_older = Skriv över äldre duplicate = Duplicera apply_to_all = Utför allt copy = Kopiera move = Flytta pack = Komprimera unpack = Extrahera download = Ladda ner split = Dela combine = Sammanfläta browse = Bläddra ask = Fråga stop = Stoppa pause = Pausa quick_search = Snabbsök file_manager = Filhanterare create = Skapa creating_file = Skapar %1 choose = Välj customize = Anpassa choose_folder = Välj mapp login = Användarnamn password = Lösenord user = Användare encoding = Textkodning preferred_encodings = Förvald textkodning license = Licens name = Namn size = Storlek date = Datum extension = Filtyp permissions = Rättigheter owner = Ägare group = Grupp location = Plats untitled = Namnlös source = Källa destination = Mål recurse_directories = Behandla markerade mappar rekursivt go_to = Gå till example = Exempel preview = Förhandsgranska comment = Kommentar sample_text = Exempel på text nb_files = %1 fil(er) nb_folders = %1 mapp(ar) loading = Laddar... this_operation_cannot_be_undone = Du kan inte ångra denna handling. remove = Ta bort details = Detaljer warning = Varning error = Fel generic_error = Ett fel uppstod när handlingen utfördes. folder_does_not_exist = Mappen finns inte eller är inte tillgänglig. this_folder_does_not_exist = Mappen finns inte eller är inte tillgänglig: %1 this_file_does_not_exist = Filen finns inte eller är inte tillgänglig: %1 invalid_path = Ogiltig sökväg: %1 directory_already_exists = Mappen %1 finns redan. file_exists_in_destination = Filen finns redan på denna plats. source_parent_of_destination = Försöker flytta mappen till en av dess undermappar cannot_read_file = Kan inte läsa filen %1 cannot_write_file = Kan inte skriva till filen %1 cannot_create_folder = Kan inte skapa mappen %1 cannot_read_folder = Kan inte läsa innehållet i mappen %1 cannot_delete_file = Kan inte ta bort filen %1 cannot_delete_folder = Kan inte ta bort mappen %1 error_while_transferring = Fel vid överföring av filen %1 same_source_destination = Källan och målet är densamma file_already_exists = %1 finns redan, vill du ersätta den? write_error = Skrivfel read_error = Läsfel integrity_check_error = Integritetscheck fel: källan och målet överensstämmer inte startup_error = Ett fel förhindrade trolCommander från att starta. permissions.read = Läsbar permissions.write = Skrivbar permissions.executable = Exekverbar permissions.group = Grupp permissions.other = Annat permissions.octal_notation = Oktal notation action_categories.all = Alla action_categories.navigation = Navigation action_categories.selection = Markering action_categories.view = Visa action_categories.file_operations = Filåtgärder action_categories.windows = Fönster Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Nytt bokmärke AddBookmark.tooltip = Lägg till aktuell mapp i bokmärken BatchRename.label = Döp om filer EditBookmarks.label = Redigera bokmärken ExploreBookmarks.label = Visa bokmärken EditCredentials.label = Redigera referenser ChangeLocation.label = Ändra aktuell sökväg ChangeDate.label = Ändra datum ChangeDate.tooltip = Ändra datum på markerad(e) fil(er) ChangePermissions.label = Ändra rättigheter ChangePermissions.tooltip = Ändra rättigheter på markerad(e) fil(er) CheckForUpdates.label = Sök uppdateringar CompareFolders.label = Jämför mappar ConnectToServer.label = Anslut till server ConnectToServer.tooltip = Anslut till fjärrserver View.label = Visa InternalView.label = Visa (intern) View.tooltip = Visa markerad fil InternalEdit.label = Redigera (intern) Edit.tooltip = Ändra markerad fil Copy.tooltip = Kopiera markerade filer LocalCopy.label = Lokal kopia LocalCopy.tooltip = Kopiera markerad fil till aktuell mapp Move.tooltip = Flytta markerade filer Rename.tooltip = Döp om markerad fil Mkdir.label = Ny mapp Mkdir.tooltip = Skapa ny mapp i aktuell mapp Mkfile.label = Ny fil Mkfile.tooltip = Skapa ny fil i aktuell mapp Delete.tooltip = Radera markerade filer till papperskorgen när det är möjligt PermanentDelete.label = Radera permanent PermanentDelete.tooltip = Radera markerade filer utan att använda papperskorgen Refresh.label = Uppdatera Refresh.tooltip = Uppdatera aktuell mapp CloseWindow.label = Stäng fönster CloseWindow.tooltip = Stäng detta fönster CopyFileNames.label = Kopiera namn(en) CopyFilePaths.label = Kopiera sökväg(en) CopyFilesToClipboard.label = Kopiera fil(er) PasteClipboardFiles.label = Klistra in fil(er) Email.label = Mejla filer Email.tooltip = Mejla markerade filer GoBack.label = Tillbaka GoBack.tooltip = Föregående mapp GoForward.label = Framåt GoForward.tooltip = Nästa mapp GoToHome.label = Hemmamappen GoToParent.label = Överordnad GoToParent.tooltip = Till överordnad mapp GoToParentInOtherPanel.label = Gå till överordnad mapp i andra vyn GoToParentInBothPanels.label = Gå till överordnad mapp i båda vyerna GoToRoot.label = Gå till root SortByName.label = Sortera efter Namn SortByDate.label = Sortera efter Datum SortBySize.label = Sortera efter Storlek SortByExtension.label = Sortera efter Filtyp SortByPermissions.label = Sortera efter Rättigheter SortByOwner.label = Sortera efter Ägare SortByGroup.label = Sortera efter Grupp MarkGroup.label = Markera filer MarkGroup.tooltip = Markera en grupp filer UnmarkGroup.label = Avmarkera filer UnmarkGroup.tooltip = Avmarkera en grupp filer MarkAll.label = Markera allt UnmarkAll.label = Avmarkera allt MarkSelectedFile.label = Markera/Avmarkera MarkSelectedFile.tooltip = Markera/Avmarkera valda filer MarkNextBlock.label = Markera ett block neråt MarkPreviousBlock.label = Markera ett block uppåt MarkNextRow.label = Markera en rad neråt MarkPreviousRow.label = Markera en rad uppåt MarkNextPage.label = Markera sidan neråt MarkPreviousPage.label = Markera sidan uppåt MarkToFirstRow.label = Markera filer till början MarkToLastRow.label = Markera filer till slutet MarkExtension.label = Markera filtyp InvertSelection.label = Omvänd markering SwapFolders.label = Växla vyer SwapFolders.tooltip = Växla vänster och höger vy SetSameFolder.label = Visa samma vy SetSameFolder.tooltip = Visa samma vy till vänster och höger NewWindow.label = Nytt fönster NewWindow.tooltip = Öppna ett nytt fönster Open.label = Öppna Open.tooltip = Öppna mapp / Öppna arkiv / Kör OpenNatively.label = Öppna med förvalt OpenNatively.tooltip = Öppna markerad filen med systemets förvalda filanknytning OpenInOtherPanel.label = Öppna i andra vyn OpenInBothPanels.label = Öppna i båda vyerna RevealInDesktop.label = Visa i %1 RunCommand.label = Kör kommando RunCommand.tooltip = Kör ett kommando i aktuell mapp Pack.tooltip = Komprimera markerade filer till ett arkiv Unpack.tooltip = Extrahera markerade filarkiv ShowFileProperties.label = Egenskaper ShowFileProperties.tooltip = Visa egenskaper för markerade filer ShowPreferences.label = Inställningar ShowPreferences.tooltip = Konfigurera trolCommander ShowServerConnections.label = Visa öppna anslutningar Quit.label = Avsluta ReverseSortOrder.label = Omvänd sortering ToggleAutoSize.label = Automatisk kolumnbredd Stop.label = Stoppa mappbyte ToggleColumn.show = Visa %1 kolumn ToggleColumn.hide = Göm %1 kolumn ToggleCommandBar.show = Visa kommandoraden ToggleCommandBar.hide = Dölj kommandoraden ToggleToolBar.show = Visa verktygsfältet ToggleToolBar.hide = Dölj vertygsfältet CustomizeCommandBar.label = Anpassa kommandoraden ToggleStatusBar.show = Visa statuslisten ToggleStatusBar.hide = Dölj statuslisten ToggleShowFoldersFirst.label = Visa mappar först ToggleTree.label = Visa trädvy PopupLeftDriveButton.label = Ändra vänster vy PopupRightDriveButton.label = Ändra höger vy RecallPreviousWindow.label = Återkalla föregående fönster RecallNextWindow.label = Återkalla nästa fönster RecallWindow.label = Återkalla fönster #%1 BringAllToFront.label = Lägga alla överst SwitchActiveTable.label = Växla mellan vänster och höger vy SelectNextBlock.label = Hoppa ett block neråt SelectPreviousBlock.label = Hoppa ett block uppåt SelectNextPage.label = Hoppa en sida neråt SelectPreviousPage.label = Hoppa en sida uppåt SelectNextRow.label = Hoppa en rad neråt SelectPreviousRow.label = Hoppa en rad uppåt SelectFirstRow.label = Välj första filen i aktuell vy SelectLastRow.label = Välj sista filen i aktuell vy SplitEqually.label = Dela lika SplitVertically.label = Dela vertikalt SplitHorizontally.label = Dela horisontellt ShowKeyboardShortcuts.label = Kortkommandon GoToWebsite.label = Besök hemsidan GoToForums.label = Besök forum ReportBug.label = Rapportera en bugg Donate.label = Donera ShowAbout.label = Om trolCommander OpenTrash.label = Öppna papperskorg EmptyTrash.label = Töm papperskorg CalculateChecksum.label = Beräkna kontrollsumma MaximizeWindow.label = Maximera MaximizeWindow.label.mac_os_x = Zooma MinimizeWindow.label = Minimera GoToDocumentation.label = Online dokumentation ShowParentFoldersQL.label = Överordnade mappar ShowRecentLocationsQL.label = Nyligen besökta platser ShowRecentExecutedFilesQL.label = Nyligen öppnade filer SplitFile.tooltip = Dela upp en fil i flera delar CombineFiles.tooltip = Slå samman uppdelade filer för att återskapa originalfilen ShowDebugConsole.label = Felsökningskonsol FocusPrevious.label = Fokusera på föregående komponent FocusNext.label = Fokusera på nästa komponent file_menu = Arkiv file_menu.open_with = Öppna med mark_menu = Markera view_menu = Visa view_menu.show_hide_columns = Visa/Dölj kolumner go_menu = Gå bookmarks_menu = Bokmärken bookmarks_menu.no_bookmark = Det finns inga bokmärken quick_lists_menu = Snabblista window_menu = Fönster help_menu = Hjälp status_bar.selected_files = %1 av %2 markerade status_bar.connecting_to_folder = Ansluter till mapp, tryck ESC för att avbryta. status_bar.volume_free = Ledigt: %1 status_bar.volume_capacity = Kapacitet: %1 shortcuts_panel.title = Kortkommandon shortcuts_panel.restore_defaults = Återställ förval shortcuts_panel.show = Visa shortcuts_panel.default_message = Tryck Enter eller dubbelklicka på kortkommandot som du vill redigera shortcuts_table.action_description = Handlingsbeskrivning shortcuts_table.shortcut = Kortkommando shortcuts_table.alternate_shortcut = Alternativt kortkommando shortcuts_table.type_in_a_shortcut = Ange kortkommando command_bar_dialog.help = Dra knapparna för att anpassa kommandoraden table.folder_access_error_title = Fel vid mappåtkomst table.folder_access_error = Mappens innehåll kan inte läsas table.download_or_browse = Vill du bläddra eller laddar ner filen? version_dialog.no_new_version_title = Ingen ny version finns version_dialog.no_new_version = Grattis, du har redan den senaste versionen. version_dialog.new_version_title = Ny version tillgänglig version_dialog.new_version = En ny version av trolCommander finns tillgänglig. version_dialog.new_version_url = En ny version av trolCommander finns här: %1. version_dialog.not_available_title = Servern är inte tillgänglig version_dialog.not_available = Kan inte få information om version från servern. version_dialog.install_and_restart = Installera och starta om version_dialog.preparing_for_update = Förbereder uppdatering... quit_dialog.title = Avsluta trolCommander quit_dialog.desc = Du har %1 fönster öppna. Vill du avsluta ändå? quit_dialog.show_next_time = Visa nästa gång destination_dialog.file_exists_action = Standardåtgärd när filen redan finns destination_dialog.verify_integrity = Verifierar data integritet destination_dialog.skip_errors = Ignorera fel file_collision_dialog.title = Filen finns redan rename_dialog.new_name = Nytt namn copy_dialog.destination = Kopiera markerad(e) fil(er) till copy_dialog.error_title = Kopieringsfel copy_dialog.copying = Kopierar filer copy_dialog.copying_file = Kopierar %1 pack_dialog.packing = Komprimerar filer pack_dialog.packing_file = Komprimerar %1 pack_dialog.error_title = Komprimeringsfel pack_dialog_description = Lägg till markerade filer i pack_dialog.archive_format = Arkivformat unpack_dialog.destination = Extrahera markerade fil(er) till unpack_dialog.error_title = Extraheringsfel unpack_dialog.unpacking = Extraherar filer unpack_dialog.unpacking_file = Extraherar %1 optimizing_archive = Optimerar arkiv %1 error_while_optimizing_archive = Fel vid optimering av arkiv %1 move_dialog.move_description = Flytta till move_dialog.error_title = Flyttfel move_dialog.moving = Flyttar filer move_dialog.moving_file = Flyttar %1 download_dialog.description = Ladda ner fil till download_dialog.error_title = Nerladdningsfel download_dialog.downloading = Laddar ner download_dialog.downloading_file = Laddar ner %1 mkfile_dialog.allocate_space = Tilldela utrymme delete_dialog.permanently_delete.confirmation = Radera markerade fil(er) permanent? delete_dialog.move_to_trash.confirmation = Radera markerade fil(er)? delete_dialog.move_to_trash.confirmation_details = Filerna kommer flyttas till papperskorgen. delete_dialog.move_to_trash.option = Flytta till papperskorgen delete_dialog.move_to_trash.failed = En eller flera filer kunde inte flyttas till papperskorgen delete_dialog.deleting = Raderar delete_dialog.error_title = Raderingsfel delete.deleting_file = Raderar %1 email_dialog.prefs_not_set_title = Mejlen är inte inställd email_dialog.prefs_not_set = Du måste ställa in din mejl först email_dialog.from = Från email_dialog.to = Till email_dialog.subject = Ämne email_dialog.send = Skicka email_dialog.error_title = Mejlfel av filerna email_dialog.read_error = Kunde inte läsa filer i undermappen email_dialog.sending = Skickar filer email.sending_file = Skickar %1 email.connecting_to_server = Ansluter till %1 email.server_unavailable = Kunde inte kontakta mejlservern %1, kolla dina mejlinställningar eller försök igen senare. email.connection_closed = Anslutningen stängd av servern, mejlet skickades inte. email.goodbye_failed = Fel när anslutning stängdes, mejlet kanske inte skickades. email.send_file_error = Kunde inte skicka filen %1, mejlet skickades inte. split_file_dialog.error_title = Fel vid uppdelning av fil split_file_dialog.file_to_split = Dela upp fil split_file_dialog.target_directory = Målmapp split_file_dialog.part_size = Delstorlek split_file_dialog.parts = Antal delar split_file_dialog.generate_CRC = Skapa CRC fil split_file_dialog.max_parts = Maximalt antal tillåtna delar är %1 split_file_dialog.auto = Automatiskt split_file_dialog.insert_new_media = Sätt in ny media combine_files_dialog.error_title = Fel vid sammanslagning av filer combine_files_job.no_crc_file = Sammanslagning klart. Ingen CRC fil. combine_files_job.crc_read_error = Fel vid läsning av CRC fil. combine_files_job.crc_check_failed = CRC matchar inte: förväntade %2, hittade %1 combine_files_job.crc_ok = Sammanslagning klart. CRC checksum ok. file_selection_dialog.mark = Markera file_selection_dialog.unmark = Avmarkera file_selection_dialog.mark_description = Markera filer vars namn file_selection_dialog.unmark_description = Avmarkera filer vars namn file_selection_dialog.case_sensitive = Skiftlägeskänslig file_selection_dialog.include_folders = Inkludera mappar progress_dialog.starting = Startar överföringen... progress_dialog.transferred = Överfört %1 med %2 progress_dialog.elapsed_time = Förfluten tid progress_dialog.advanced = Avancerat progress_dialog.current_speed = Aktuell hastighet progress_dialog.limit_speed = Begränsad hastighet progress_dialog.close_when_finished = Stäng fönstret när det är klart progress_dialog.processing_files = Bearbetar filer progress_dialog.processing_file = Bearbetar %1 progress_dialog.verifying_file = Verifierar %1 progress_dialog.job_finished = Arbetet klart progress_dialog.job_error = Bearbetningsfel properties_dialog.file_properties = %1 Egenskaper properties_dialog.contents = Innehåller properties_dialog.calculating = Beräknar... calculate_checksum_dialog.checksum_algorithm = Kontrollsumma algoritm calculate_checksum_dialog.temporary_file = Temporär fil change_date_dialog.now = Nu change_date_dialog.specific_date = Ange datum run_dialog.run_command_description = Kör i aktuell mapp run_dialog.run_in_home_description = Kör i hemmamappen run_dialog.command_output = Kommandoutskrift run_dialog.run = Kör run_dialog.clear_history = Rensa historik server_connect_dialog.server_type = Anslutningstyp server_connect_dialog.server = Server server_connect_dialog.share = Dela server_connect_dialog.domain = Domän server_connect_dialog.username = Användarnamn server_connect_dialog.initial_dir = Startmapp server_connect_dialog.port = Port server_connect_dialog.server_url = Serveradress server_connect_dialog.http_url = Hemsideadress server_connect_dialog.connect = Anslut server_connect_dialog.protocol = Protokoll server_connect_dialog.nfs_version = NFS-version server_connect_dialog.private_key = Privat nyckel server_connect_dialog.passphrase = Lösenfras ftp_connect.passive_mode = Aktivera passivt läge ftp_connect.anonymous_user = Anonym användare ftp_connect.nb_connection_retries = Antal försök att återansluta ftp_connect.retry_delay = Fördröjning mellan försöken (i sekunder) http_connect.basic_authentication = HTTP grundläggande autentisering (frivilligt) server_connections_dialog.disconnect = Koppla ner server_connections_dialog.connection_busy = Upptaget server_connections_dialog.connection_idle = Overksam bonjour.bonjour_services = Bonjour-tjänster bonjour.no_service_discovered = Ingen tjänst hittades bonjour.bonjour_disabled = Bonjour frånkopplat auth_dialog.title = Autentisering auth_dialog.desc = Vänligen ange ett användarnam och lösenord auth_dialog.server = Server auth_dialog.store_credentials = Spara användarnamn och lösenord (lätt kryptering) auth_dialog.connect_as = Anslut som auth_dialog.authentication_failed = Autentisering misslyckades sortable_list.move_up = Flytta upp sortable_list.move_down = Flytta ner add_bookmark_dialog.add = Lägg till edit_bookmarks_dialog.new = Nytt file_viewer.view_error_title = Visningfel file_viewer.view_error = Kunde inte visa filen file_viewer.file_menu = Arkiv file_viewer.close = Stäng file_viewer.large_file_warning = Filen kan vara för stort för denna begäran. file_viewer.open_anyway = Öppna ändå text_viewer.edit = Redigera text_viewer.copy = Kopiera text_viewer.select_all = Markera allt text_viewer.find = Sök text_viewer.find_next = Sök nästa text_viewer.find_previous = Sök föregående text_viewer.binary_file_warning = Detta verkar vara en binär fil image_viewer.controls_menu = Funktioner image_viewer.zoom_in = Zomma in image_viewer.zoom_out = Zomma ut file_editor.edit_error_title = Redigeringsfel file_editor.edit_error = Kunde inte redigera filen. file_editor.save = Spara file_editor.save_as = Spara som... file_editor.save_warning = Spara ändringar till filen innan avslut? file_editor.cannot_write = Kunde inte läsa filen. text_editor.cut = Klipp ut text_editor.paste = Klistra in shortcuts_dialog.quick_search.start_search = Skriv något för att starta ett snabbsök shortcuts_dialog.quick_search.cancel_search = Avbryt snabbsök shortcuts_dialog.quick_search.remove_last_char = Ta bort sista tecknet från snabbsökstråden shortcuts_dialog.quick_search.jump_to_previous = Gå till föregående snabbsöksresultat shortcuts_dialog.quick_search.jump_to_next = Gå till nästa snabbsöksresultat shortcuts_dialog.quick_search.mark_jump_next = Markera/Avmarkera aktuell fil och gå till nästa sökresultat theme_editor.title = Temaredigeraren theme_editor.folder_tab = Mappvyer theme_editor.shell_tab = Terminal theme_editor.shell_history_tab = Terminalhistorik theme_editor.statusbar_tab = Statuslisten theme_editor.free_space = Ledigt utrymme theme_editor.free_space.ok = OK theme_editor.free_space.warning = Varning theme_editor.free_space.critical = Kritiskt theme_editor.locationbar_tab = Sökvägsfältet theme_editor.editor_tab = Filredigeraren theme_editor.font = Typsnitt theme_editor.active_panel = Aktiv theme_editor.inactive_panel = Inaktiv theme_editor.general = Allmänt theme_editor.could_not_save_theme = Kunde inte spara temat %1 theme_editor.border = Kanter theme_editor.background = Bakgrund theme_editor.alternate_background = Alternativ bakgrund theme_editor.unfocused_background = Bakgrund (ej i fokus) theme_editor.quick_search.unmatched_file = Filer som ej matchar theme_editor.text = Text theme_editor.progress = Förlopp theme_editor.normal = Normal theme_editor.normal_unfocused = Normal (i fokus) theme_editor.selected = Markerad theme_editor.selected_unfocused = Markerad (ej i fokus) theme_editor.color = Färg theme_editor.colors = Färger theme_editor.plain_file = Enkel fil theme_editor.marked_file = Markerad fil theme_editor.hidden_file = Dold fil theme_editor.folder = Mapp theme_editor.archive_file = Arkiv theme_editor.symbolic_link = Symbolisk länk theme_editor.header = Sidhuvud theme_editor.item = Lista command_bar_customize_dialog.available_actions = Tillgängliga handlingar command_bar_customize_dialog.modifier = Specialtangent prefs_dialog.title = Inställningar prefs_dialog.general_tab = Allmänt prefs_dialog.day = Dag prefs_dialog.month = Månad prefs_dialog.year = År prefs_dialog.language = Språk (kräver omstart) prefs_dialog.date_time = Datum- och tidsformat prefs_dialog.time = Tid prefs_dialog.date = Datum prefs_dialog.date_separator = Avskiljare prefs_dialog.time_12_hour = 12-timmarsklocka prefs_dialog.time_24_hour = 24-timmarsklocka prefs_dialog.show_seconds = Visa sekunder prefs_dialog.show_century = Visa århundrade prefs_dialog.check_for_updates_on_startup = Sök uppdateringar vid uppstart prefs_dialog.show_splash_screen = Visa startbild prefs_dialog.folders_tab = Mappvyer prefs_dialog.startup_folders = Startvyer prefs_dialog.left_folder = Vänster vy prefs_dialog.right_folder = Höger vy prefs_dialog.last_folder = Senast öppnade mappen prefs_dialog.custom_folder = Välj mapp prefs_dialog.show_hidden_files = Visa dolda filer prefs_dialog.show_ds_store_files = Visa .DS_Store filer prefs_dialog.show_system_folders = Visa systemkataloger prefs_dialog.compact_file_size = Avrunda filstorlek prefs_dialog.follow_symlinks_when_cd = Följ symboliska länkar när aktuell vy ändras prefs_dialog.appearance_tab = Utseende prefs_dialog.look_and_feel = Utseende och känsla prefs_dialog.icons_size = Ikonstorlek prefs_dialog.toolbar_icons = Verktygsfältet prefs_dialog.command_bar_icons = Kommandoraden prefs_dialog.file_icons = Filtyper prefs_dialog.use_system_file_icons = Använd systemets filikoner prefs_dialog.use_system_file_icons.always = Alltid prefs_dialog.use_system_file_icons.never = Aldrig prefs_dialog.use_system_file_icons.applications = Bara för program prefs_dialog.edit_current_theme = Redigera aktuellt tema... prefs_dialog.themes = Teman prefs_dialog.import_theme = Importera tema prefs_dialog.import_look_and_feel = Importera utseende och känsla prefs_dialog.no_look_and_feel = Inget utseende och känsla hittades. prefs_dialog.error_in_import = Fel vid import av temat %1. prefs_dialog.cannot_read_theme = Kunde inte ladda tema från filen %1 prefs_dialog.export_theme = Exportera %1 prefs_dialog.import = Importera prefs_dialog.export = Exportera prefs_dialog.theme_type = Typ: %1 prefs_dialog.delete_theme = Radera temat %1 ? prefs_dialog.delete_look_and_feel = Radera utseende och känsla %1 ? prefs_dialog.rename_failed = Kunde inte döpa om temat %1 prefs_dialog.xml_file = XML-fil prefs_dialog.jar_file = JAR-fil prefs_dialog.mail_tab = Mejl prefs_dialog.mail_settings = Utgående mejlinställningar prefs_dialog.mail_name = Mitt namn prefs_dialog.mail_address = Min mejladress prefs_dialog.mail_server = SMTP-server prefs_dialog.misc_tab = Blandat prefs_dialog.use_brushed_metal = Använd "borstad metall" utseendet (kräver omstart) prefs_dialog.confirm_on_quit = Visa bekräftelseruta vid avslut prefs_dialog.default_shell = Använd systemets standard kommandotolk prefs_dialog.custom_shell = Använd skräddarsytt shell prefs_dialog.shell_encoding = Terminal textkodning prefs_dialog.auto_detect_shell_encoding = Välj automatiskt prefs_dialog.enable_bonjour_discovery = Aktivera upptäckt av Bonjour-tjänster prefs_dialog.enable_system_notifications = Aktivera systemmeddelanden debug_console_dialog.level = Nivå unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = kB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/sek duration.seconds = %1sek duration.minutes = %1min duration.hours = %1tim duration.days = %1dag duration.months = %1mån duration.years = %1år theme.custom_theme = Skräddarsy tema theme.custom = Skräddarsytt theme.built_in = Inbyggt theme.add_on = Tillägg theme.current = aktuell theme_could_not_be_loaded = Ett fel uppstod när temat öppnades. setup.title = Välkommen till trolCommander setup.intro = Vänligen välj hur du vill att trolCommander ska agera. setup.look_and_feel = Välj utseende och känsla setup.theme = Välj tema font_chooser.font_size = Storlek font_chooser.font_bold = Fet font_chooser.font_italic = Kursiv color_chooser.red = Röd color_chooser.green = Grön color_chooser.blue = Blå color_chooser.hue = Färgton color_chooser.brightness = Intensitet color_chooser.swatches = Färgkarta color_chooser.saturation = Mättnad color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Senaste color_chooser.alpha = Alpha-transparent color_chooser.title = Välj en färg batch_rename_dialog.mask = Mönster batch_rename_dialog.search_replace = Sök och Ersätt batch_rename_dialog.search_for = Sök efter batch_rename_dialog.replace_with = Ersätt med batch_rename_dialog.counter = Räknare batch_rename_dialog.start_at = Börja vid batch_rename_dialog.step_by = Öka med batch_rename_dialog.format = Format batch_rename_dialog.upper_lower_case = Skiftläge batch_rename_dialog.no_change = Oförändrad batch_rename_dialog.lower_case = små bokstäver batch_rename_dialog.upper_case = STORA BOKSTÄVER batch_rename_dialog.first_upper = Första bokstaven stor batch_rename_dialog.word = Fösta Bokstaven I Varje Ord batch_rename_dialog.old_name = Gammalt namn batch_rename_dialog.new_name = Nytt namn batch_rename_dialog.block_name = Bevara batch_rename_dialog.range = Område batch_rename_dialog.proceed_renaming = %1 av %2 filer kommer döpas om. Vill du fortsätta? batch_rename_dialog.duplicate_names = Duplicera namn! parent_folders_quick_list.empty_message = Det finns ingen överodnad mapp till denna plats recent_locations_quick_list.empty_message = Inga nyligen besökta platser recent_executed_files_quick_list.empty_message = Inga nyligen öppnade filer #auth_dialog.error_was = Felet var %1 #table.hide_column = Dölj kolumn #delete.symlink_warning_title = Symbolisk länk hittad #delete.symlink_warning = Filen ser ut som en symbolisk länk:\n\n Fil: %1\n Länkar till: %2\n\nRadera bara länken eller\nFölj länken och radera mappen (VARNING) ? #delete.delete_link_only = Radera länk #delete.delete_linked_folder = Radera mapp #Unpack.label = Extrahera filer #Pack.label = Komprimera filer ================================================ FILE: src/main/resources/dictionary_uk_UA.properties ================================================ ok = Гаразд yes = Так no = Ні cancel = Скасувати edit = Редагувати close = Закрити reset = Скинути rename = Перейменувати apply = Застосувати change = Змінити save = Зберегти dont_save = Не зберігати replace = Замінити dont_replace = Не заміняти delete = Видалити skip = Пропустити skip_all = Пропустити все retry = Повторити resume = Продовжити overwrite = Перезаписати overwrite_if_older = Перезаписати старіший duplicate = Зкопіювати apply_to_all = Застосувати до всіх copy = Копіювати move = Перенести pack = Стиснути unpack = Розпакувати download = Завантажити split = Розділити combine = Об'єднати browse = Переглянути ask = Запитати stop = Зупинити pause = Призупинити quick_search = Швидкий пошук file_manager = Менеджер файлів create = Створити creating_file = Створюється %1 choose = Вибрати customize = Налаштувати choose_folder = Вибрати папку login = Логін password = Пароль user = Користувач encoding = Кодування preferred_encodings = Доступні кодування license = Ліцензія name = Ім'я size = Розмір date = Дата extension = Розширення permissions = Привілеї доступу owner = Власник group = Група location = Розташування untitled = Без імені source = Відправник destination = Записати у recurse_directories = Рекурсивно обробляти вкладені папки go_to = Перейти example = Приклад preview = Попередній перегляд comment = Коментар sample_text = Приклад тексту nb_files = %1 файл(ів) nb_folders = %1 папка(и) loading = Завантаження... this_operation_cannot_be_undone = Неможливо скасувати дану операцію remove = Видалити details = Детальніше warning = Попередження error = Помилка generic_error = При виконанні операції сталася помилка folder_does_not_exist = Папка не існує або є недоступною this_folder_does_not_exist = Папка %1 не існує або є недоступною this_file_does_not_exist = Файл %1 не існує або є недоступним invalid_path = Неправильний шлях directory_already_exists = Папка %1 вже існує file_exists_in_destination = Файл вже існує source_parent_of_destination = Спроба перенести папку в одну з її вкладених папок cannot_read_file = Не вдається прочитати файл %1 cannot_write_file = Не вдається записати файл %1 cannot_create_folder = Не вдається створити папку %1 cannot_read_folder = Не вдається прочитати вміст папки %1 cannot_delete_file = Не вдається видалити файл %1 cannot_delete_folder = Не вдається видалити папку %1 error_while_transferring = Помилка при передачі файлу %1 same_source_destination = Вихідна та кінцева папки однакові file_already_exists = %1 вже існує, хочете перезаписати ? write_error = Помилка під час запису read_error = Помилка під час читання integrity_check_error = Не пройдено перевірки цілісності: початковий та кінцевий файли відрізняються startup_error = Під час запуску trolCommander'а виникла помилка. permissions.read = Читати permissions.write = Писати permissions.executable = Виконувати permissions.group = Група permissions.other = Інші permissions.octal_notation = Числове значення action_categories.all = Всі action_categories.navigation = Навігація action_categories.selection = Вибір action_categories.view = Перегляд action_categories.file_operations = Файлові операції action_categories.windows = Вікна Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = Додати закладку AddBookmark.tooltip = Додати поточну папку до закладок BatchRename.label = Масове перейменування EditBookmarks.label = Редагувати закладки ExploreBookmarks.label = Переглянути закладки EditCredentials.label = Переглянути реквізити ChangeLocation.label = Зміти розташування ChangeDate.label = Змінити дату ChangeDate.tooltip = Змінити дату вибраних файлів ChangePermissions.label = Змінити привілеї доступу ChangePermissions.tooltip = Змінити привілеї доступу вибраного файла(файлів) CheckForUpdates.label = Перевірити наявність оновлення CompareFolders.label = Порівняти папки ConnectToServer.label = З'єднатися з сервером ConnectToServer.tooltip = З'єднатися з віддаленим сервером View.label = Переглянути InternalView.label = Перегляд (вбудованим) View.tooltip = Переглянути вибраний файл InternalEdit.label = Редагувати (вбудованим) Edit.tooltip = Редагувати вибраний файл Copy.tooltip = Копіювати відмічені файл LocalCopy.label = Копіювати локально LocalCopy.tooltip = Копіювати вибраний файл в поточну папку Move.tooltip = Перенести відмічені файли Rename.tooltip = Перейменувати відмічені файли Mkdir.label = Нова папка Mkdir.tooltip = Створити в поточній папці Mkfile.label = Створити файл Mkfile.tooltip = Створити файл в поточній папці Delete.tooltip = Видаляти відмічені файли у системний кошик, коли доступно PermanentDelete.label = Видаляти зразу PermanentDelete.tooltip = Видаляти відмічені файли не використовуючи системний кошик Refresh.label = Оновити Refresh.tooltip = Оновити поточну папку CloseWindow.label = Закрити вікно CloseWindow.tooltip = Закрити дане вікно CopyFileNames.label = Копіювати імена CopyFilePaths.label = Копіювати шлях(и) CopyFilesToClipboard.label = Копіювати файл(и) PasteClipboardFiles.label = Вставити файл(и) Email.label = Надіслати файл(и) по email Email.tooltip = Надіслати відмічені файли додатками до email GoBack.label = Назад GoBack.tooltip = Перейти до попередньої папки GoForward.label = Вперед GoForward.tooltip = Перейти до наступної папки GoToHome.label = Перейти в домашню папку GoToParent.label = Перейти наверх GoToParent.tooltip = Перейти в батьківську папку GoToParentInOtherPanel.label = Перейти наверх в іншій панелі GoToParentInBothPanels.label = Перейти наверх в обох панелях GoToRoot.label = Перейти в кореневу папку SortByName.label = Сортувати за ім'ям SortByDate.label = Сортувати за датою SortBySize.label = Сортувати за розміром SortByExtension.label = Сортувати за розширенням SortByPermissions.label = Сортувати за привілеями доступу SortByOwner.label = Сортувати за власником SortByGroup.label = Сортувати за групою MarkGroup.label = Відмітити файли MarkGroup.tooltip = Відмітити групу файлів UnmarkGroup.label = Зняти відмітку з файлів UnmarkGroup.tooltip = Зняти відмітку з групи файлів MarkAll.label = Відмітити все UnmarkAll.label = Зняти відмітку з всього MarkSelectedFile.label = Відмітити/зняти MarkSelectedFile.tooltip = Відмітити/зняти відмітку для вибраного файла MarkNextBlock.label = Виділити блок рядків вниз MarkPreviousBlock.label = Виділити блок рядків вверх MarkNextRow.label = Виділити рядок вниз MarkPreviousRow.label = Виділити рядок вверх MarkNextPage.label = Відмітити файли на сторінку вниз MarkPreviousPage.label = Відмітити файли на сторінку вверх MarkToFirstRow.label = Відмітити файли до початку MarkToLastRow.label = Відмітити файли до кінця MarkExtension.label = Відмітити розширення InvertSelection.label = Інвертувати виділення SwapFolders.label = Поміняти папки місцями SwapFolders.tooltip = Поміняти праву і ліву папки місцями SetSameFolder.label = Встановити однакові папки SetSameFolder.tooltip = Однакові папки в обох панелях NewWindow.label = Нове вікно NewWindow.tooltip = Відкрити нове вікно Open.label = Відкрити Open.tooltip = Відкрити папку / Відкрити архів / Виконати OpenNatively.label = Відкрити з допомогою ОС OpenNatively.tooltip = Виконати вибраний файл програмою, асоційованою з ним в налаштуваннях ситеми OpenInNewTab.label = Відкрити у новій вкладці OpenInOtherPanel.label = Відкрити в іншій панелі OpenInBothPanels.label = Відкрити в обох панелях RevealInDesktop.label = Відкрити в %1 RunCommand.label = Виконати команду RunCommand.tooltip = Виконати в поточній папці Pack.tooltip = Стиснути вибрані файли у архів Unpack.tooltip = Розпакувати відмічені файли-архіви ShowFileProperties.label = Властивості ShowFileProperties.tooltip = Переглянути властивості відмічених файлів ShowPreferences.label = Налаштування ShowPreferences.tooltip = Налаштувати trolCommander ShowServerConnections.label = Переглянути відкриті з'єднання Quit.label = Вихід ReverseSortOrder.label = В зворотньому порядку ToggleAutoSize.label = Колонки автоматично змінюють розмір Stop.label = Зупинити зміну папки ToggleColumn.show = Показати стовпець %1 ToggleColumn.hide = Сховати стовпець %1 ToggleCommandBar.show = Показати панель команд ToggleCommandBar.hide = Сховати панель команд ToggleToolBar.show = Показати панель інструментів ToggleToolBar.hide = Сховати панель інструментів CustomizeCommandBar.label = Налаштування панелі команд ToggleStatusBar.show = Показати статус-панель ToggleStatusBar.hide = Сховати статус-панель ToggleShowFoldersFirst.label = Показувати папки першими в списку ToggleTree.label = Показати дерево PopupLeftDriveButton.label = Змінити ліву папку PopupRightDriveButton.label = Змінити праву папку RecallPreviousWindow.label = Перейти в попереднє вікно RecallNextWindow.label = Перейти в наступне вікно RecallWindow.label = Відновити вікно %1 BringAllToFront.label = Показати всі вікна SwitchActiveTable.label = Перемкнути праву і ліву панелі SelectNextBlock.label = Опуститись на блок рядків вниз SelectPreviousBlock.label = Піднятись на блок рядків вверх SelectNextPage.label = Перейти в кінець сторінки SelectPreviousPage.label = Перейти на початок сторінки SelectNextRow.label = Опуститись на один рядок вниз SelectPreviousRow.label = Піднятись на один рядок вверх SelectFirstRow.label = Перейти до першого файлу в каталозі SelectLastRow.label = Перейти до останнього файлу в каталозі SplitEqually.label = Поділити навпіл SplitVertically.label = Вертикальний поділ SplitHorizontally.label = Горизонтальний поділ ShowKeyboardShortcuts.label = Гарячі клавіші GoToWebsite.label = Перейти на сторінку trolCommander GoToForums.label = Перейти на форум ReportBug.label = Повідомити про помилку Donate.label = Пожертвувати на розвиток програми ShowAbout.label = Про програму trolCommander OpenTrash.label = Відкрити Смітник EmptyTrash.label = Очистити Смітник CalculateChecksum.label = Порахувати контрольну суму MaximizeWindow.label = Розгорнути MaximizeWindow.label.mac_os_x = Збільшити MinimizeWindow.label = Згорнути MinimizeWindow.label.mac_os_x = В Док GoToDocumentation.label = Документація в мережі ShowParentFoldersQL.label = Наверх ShowRecentLocationsQL.label = Останні відвідані місця ShowRecentExecutedFilesQL.label = Останні виконані файли ShowRootFoldersQL.label = Кореневі папки SplitFile.tooltip = Розділити файл на декілька частин CombineFiles.tooltip = Об'єднати розділений на частини файл в початковий файл ShowDebugConsole.label = Консоль Debug FocusPrevious.label = Перейти до попереднього компоненту FocusNext.label = Перейти до наступного компоненту file_menu = Файл file_menu.open_with = Відкрити з mark_menu = Відмітити view_menu = Перегляд view_menu.show_hide_columns = Показати/сховати колонки go_menu = Перейти bookmarks_menu = Закладки bookmarks_menu.no_bookmark = Закладок немає drive_popup.network_shares = Спільний мережевий ресурс quick_lists_menu = Швидкий список window_menu = Вікно help_menu = Допомога status_bar.selected_files = %1 з %2 вибрано status_bar.connecting_to_folder = З'єднання з папкою, натисни ESCAPE щоб скасувати status_bar.volume_free = Вільно %1 status_bar.volume_capacity = Ємність %1 shortcuts_panel.title = Комбінації клавіш shortcuts_panel.restore_defaults = Відновити стандартні shortcuts_panel.show = Показати shortcuts_panel.default_message = Натиснення Enter або подвійний клік надасть змогу редагувати комбінації клавіш shortcuts_table.action_description = Опис дії shortcuts_table.shortcut = Комбінації клавіш shortcuts_table.alternate_shortcut = Альтернативні комбінації shortcuts_table.type_in_a_shortcut = Натисніть комбінацію клавіш command_bar_dialog.help = Перетягуйте кнопки для налаштування панелі команд table.folder_access_error_title = Помилка під час доступу до папки table.folder_access_error = Не вдалося прочитати вміст папки table.download_or_browse = Хочете переглянути чи завантажити даний файл ? CloseDuplicateTabs.tooltip = Закрити вкладки, що повторюються CloseOtherTabs.tooltip = Закрити інші вкладки CloseTab.tooltip = Закрити вкладку MoveTabToOtherPanel.tooltip = Перемістити вкладку на іншу панель NextTab.label = Наступна вкладка PreviousTab.label = Попередня вкладка version_dialog.no_new_version_title = Немає нової версії version_dialog.no_new_version = Ви вже користуєтесь найостаннішою версією version_dialog.new_version_title = Є нова версія version_dialog.new_version = Є нова версія trolCommander. version_dialog.new_version_url = Нова версія trolCommander доступна за адресою %1. version_dialog.not_available_title = Сервер не відповідає version_dialog.not_available = Не вдалося отримати інформацію про версії з сервера version_dialog.install_and_restart = Встановити та перезапустити version_dialog.preparing_for_update = Готуюсь до оновлення... quit_dialog.title = Вийти з trolCommander quit_dialog.desc = Вийти з trolCommander (відкрито %1 вікон) ? quit_dialog.show_next_time = Показувати наступного разу destination_dialog.file_exists_action = Дія за замовчуванням, якщо файл вже існує destination_dialog.verify_integrity = Перевірити цілісність даних destination_dialog.skip_errors = Пропустити помилки file_collision_dialog.title = Співпадіння файлів rename_dialog.new_name = Нове ім'я copy_dialog.destination = Копіювати вибрані файли до copy_dialog.error_title = Помилка при копіюванні copy_dialog.copying = Копіювання файлів copy_dialog.copying_file = Копіюю %1 pack_dialog.packing = Стиснення файлів pack_dialog.packing_file = Стискаю %1 pack_dialog.error_title = Помилка стиснення pack_dialog_description = Додати вибрані файли до pack_dialog.archive_format = Формат архіву unpack_dialog.destination = Розпакувати вибрані файли до unpack_dialog.error_title = Помилка при розпакуванні unpack_dialog.unpacking = Розпакування файлів unpack_dialog.unpacking_file = Розпакування %1 optimizing_archive = Оптимізація архіву %1 error_while_optimizing_archive = Помилка при оптимізації архіву %1 move_dialog.move_description = Перемістити до move_dialog.error_title = Помилки при переміщенні move_dialog.moving = Переміщення файлів move_dialog.moving_file = Переміщення %1 download_dialog.description = Завантажити файл до download_dialog.error_title = Помилка при завантаженні download_dialog.downloading = Завантаження download_dialog.downloading_file = Завантаження %1 mkfile_dialog.allocate_space = Виділити місце delete_dialog.permanently_delete.confirmation = Видалити вибрані файли безвідновно ? delete_dialog.move_to_trash.confirmation = Видалити вибрані файли ? delete_dialog.move_to_trash.confirmation_details = Файли буде перенесено в Смітник delete_dialog.move_to_trash.option = Перенести в Смітник delete_dialog.move_to_trash.failed = Один або декілька файлів не можуть бути перенесені у смітник. delete_dialog.deleting = Видалення delete_dialog.error_title = Помилка при видаленні delete.deleting_file = Видалення %1 email_dialog.prefs_not_set_title = Пошта не налаштована email_dialog.prefs_not_set = Спершу налаштуйте пошту. email_dialog.from = Від email_dialog.to = Кому email_dialog.subject = Тема email_dialog.send = Надіслати email_dialog.error_title = Помилка при відсиланні файлів email_dialog.read_error = Не вдалося прочитати файли з вкладених папок email_dialog.sending = Відсилання файлів email.sending_file = Відсилання %1 email.connecting_to_server = З'єднання з %1 email.server_unavailable = Не вдалось з'єднатися з поштовим сервером %1, перевір налаштування пошти або спробуй пізніше email.connection_closed = Сервер перервав з'єднання, пошта не відіслана email.goodbye_failed = Помилка при закритті з'єднання, можливо пошта не відіслана email.send_file_error = Не вдалося надіслати файл %1, пошта не відіслана split_file_dialog.error_title = Помилка під час розподілу файлу split_file_dialog.file_to_split = Файл для розподілу split_file_dialog.target_directory = Каталог призначення split_file_dialog.part_size = Розмір кожної частини split_file_dialog.parts = Кількість частин split_file_dialog.generate_CRC = Сформувати CRC файл split_file_dialog.max_parts = Максимально допустима кількість частин %1 split_file_dialog.auto = Автоматично split_file_dialog.insert_new_media = Вставте новий носій combine_files_dialog.error_title = Помилка об'єднання файлу combine_files_job.no_crc_file = Об'єднано успішно. Немає CRC файлу. combine_files_job.crc_read_error = Помилка читання CRC файлу. combine_files_job.crc_check_failed = CRC розбіжність: очікується %2, отримано %1 combine_files_job.crc_ok = Об'єднано успішно. CRC контрольна сума ok. file_selection_dialog.mark = Видмітити file_selection_dialog.unmark = Зняти відмітку file_selection_dialog.mark_description = Відмітити файли з ім'ям file_selection_dialog.unmark_description = Зняти відмітку для файлів з ім'ям, яке file_selection_dialog.case_sensitive = Враховувати регістр file_selection_dialog.include_folders = Включати папки progress_dialog.starting = Передача почалась... progress_dialog.transferred = Передано %1 зі швидкістю %2 progress_dialog.elapsed_time = Час progress_dialog.advanced = Додатково progress_dialog.current_speed = Поточна швидкість progress_dialog.limit_speed = Обмеження швидкості progress_dialog.close_when_finished = Закрити вікно після закінчення progress_dialog.processing_files = Обробка файлів progress_dialog.processing_file = Обробка %1 progress_dialog.verifying_file = Перевірка %1 progress_dialog.job_finished = Закінчено progress_dialog.job_error = Помилка properties_dialog.file_properties = Властивості %1 properties_dialog.contents = Вміст properties_dialog.calculating = Обчислення... calculate_checksum_dialog.checksum_algorithm = Алгоритм підрахунку контрольної суми calculate_checksum_dialog.temporary_file = Тимчасовий файл change_date_dialog.now = Поточний час change_date_dialog.specific_date = Вказана дата run_dialog.run_command_description = Виконати в поточній папці run_dialog.run_in_home_description = Виконати в домашній папці run_dialog.command_output = Результат команди run_dialog.run = Запустити run_dialog.clear_history = Очистити запам'ятовані команди server_connect_dialog.server_type = Тип з'єднання server_connect_dialog.server = Сервер server_connect_dialog.share = Спільний ресурс server_connect_dialog.domain = Домен server_connect_dialog.username = Ім'я користувача server_connect_dialog.initial_dir = Початкова папка server_connect_dialog.port = Порт server_connect_dialog.server_url = Адреса сервера server_connect_dialog.http_url = Адреса сторінки в інтернеті server_connect_dialog.connect = Під'єднатися server_connect_dialog.protocol = Протокол server_connect_dialog.nfs_version = Версія NFS server_connect_dialog.private_key = Приватний ключ server_connect_dialog.passphrase = Ключова фраза ftp_connect.passive_mode = Використовувати пасивний режим ftp_connect.anonymous_user = Анонімний користувач ftp_connect.nb_connection_retries = Кількість спроб з'єднання ftp_connect.retry_delay = Пауза між спробами (секунди) http_connect.basic_authentication = Базова аутентифікація HTTP (не обов'язково) server_connections_dialog.disconnect = Від'єднатися server_connections_dialog.connection_busy = Зайнято server_connections_dialog.connection_idle = Вільно bonjour.bonjour_services = Служба Bonjour bonjour.no_service_discovered = Не знайдено служб bonjour.bonjour_disabled = Служба Bonjour відключена auth_dialog.title = Аутентифікація auth_dialog.desc = Введіть ім'я користувача і пароль auth_dialog.server = Сервер auth_dialog.store_credentials = Зберігати ім'я користувача і пароль (слабке шифрування) auth_dialog.connect_as = Під'єднатись як auth_dialog.authentication_failed = Аутентифікація не пройдена sortable_list.move_up = Перемістити вгору sortable_list.move_down = Перемістити вниз add_bookmark_dialog.add = Додати edit_bookmarks_dialog.new = Нова file_viewer.view_error_title = Помилка при перегляді file_viewer.view_error = Перегляд файлу неможливий file_viewer.file_menu = Файл file_viewer.close = Закрити file_viewer.large_file_warning = Вибраний файл може бути завеликим для перегляду file_viewer.open_anyway = Відкрити будьщо text_viewer.edit = Редагувати text_viewer.copy = Копіювати text_viewer.select_all = Вибрати все text_viewer.find = Шукати text_viewer.find_next = Шукати далі text_viewer.find_previous = Знайти попереднє text_viewer.view = Вигляд text_viewer.line_wrap = Перенесення слів text_viewer.line_numbers = Номери рядків text_viewer.binary_file_warning = Файл, скоріш за все, двійковий image_viewer.controls_menu = Елементи керування image_viewer.zoom_in = Збільшити image_viewer.zoom_out = Зменшити file_editor.edit_error_title = Помилка при редагуванні file_editor.edit_error = Редагування файлу неможливе file_editor.save = Зберегти file_editor.save_as = Зберегти з ім'ям... file_editor.save_warning = Зберегти внесені зміни і закрити ? file_editor.cannot_write = Не вдається записати файл. text_editor.cut = Вирізати text_editor.paste = Вставити shortcuts_dialog.quick_search.start_search = Для швидкого пошуку, введіть будь-який символ shortcuts_dialog.quick_search.cancel_search = Скасувати швидкий пошук shortcuts_dialog.quick_search.remove_last_char = Видалити останній символ з стрічки швидкого пошуку shortcuts_dialog.quick_search.jump_to_previous = Перейти до попередніх результатів швидкого пошуку shortcuts_dialog.quick_search.jump_to_next = Перейти до наступних результатів швидкого пошуку shortcuts_dialog.quick_search.mark_jump_next = Відмітити/зняти відмітку з поточного файлу і перейти до наступних результатів швидкого пошуку theme_editor.title = Редактор теми theme_editor.folder_tab = Панель папки theme_editor.shell_tab = Оболонка theme_editor.shell_history_tab = Історія команд theme_editor.statusbar_tab = Статус-панель theme_editor.free_space = Вільне місце theme_editor.free_space.ok = Гаразд theme_editor.free_space.warning = Увага theme_editor.free_space.critical = Дуже мало theme_editor.locationbar_tab = Панель місцезнаходження theme_editor.editor_tab = Редактор файлів theme_editor.font = Шрифт theme_editor.active_panel = Активна theme_editor.inactive_panel = Неактивна theme_editor.general = Загальні theme_editor.could_not_save_theme = Не вдалося зберегти тему %1 theme_editor.border = Рамка theme_editor.background = Фон theme_editor.alternate_background = Інший фон theme_editor.unfocused_background = Фон (без фокусу) theme_editor.quick_search.unmatched_file = Відсіяні файли theme_editor.text = Текст theme_editor.progress = Індикатор виконання theme_editor.normal = Звичайний theme_editor.normal_unfocused = Звичайний (без фокусу) theme_editor.selected = Вибраний theme_editor.selected_unfocused = Вибраний (без фокусу) theme_editor.color = Колір theme_editor.colors = Кольори theme_editor.plain_file = Звичайний файл theme_editor.marked_file = Відмічений файл theme_editor.hidden_file = Прихований файл theme_editor.folder = Папка theme_editor.archive_file = Архів theme_editor.symbolic_link = Символьний лінк theme_editor.header = Заголовок theme_editor.item = Елемент command_bar_customize_dialog.available_actions = Доступні дії command_bar_customize_dialog.modifier = Перемикач prefs_dialog.title = Налаштування prefs_dialog.general_tab = Загальні prefs_dialog.day = День prefs_dialog.month = Місяць prefs_dialog.year = Рік prefs_dialog.language = Мова (потрібно перезапустити) prefs_dialog.date_time = Формат дати і часу prefs_dialog.time = Час prefs_dialog.date = Дата prefs_dialog.date_separator = Розділювач prefs_dialog.time_12_hour = 12-годинний формат prefs_dialog.time_24_hour = 24-годинний формат prefs_dialog.show_seconds = Показувати секунди prefs_dialog.show_century = Показувати століття prefs_dialog.check_for_updates_on_startup = Перевіряти наявність оновлень при запуску prefs_dialog.show_splash_screen = Показувати початкову заставку prefs_dialog.show_splash_screen = Показувати заставку prefs_dialog.folders_tab = Папки prefs_dialog.startup_folders = Папки при запуску prefs_dialog.left_folder = Ліва папка prefs_dialog.right_folder = Права папка prefs_dialog.last_folder = Остання відвідана папка prefs_dialog.custom_folder = Задана папка prefs_dialog.show_hidden_files = Показувати приховані файли prefs_dialog.show_ds_store_files = Показувати .DS_Store файли prefs_dialog.show_system_folders = Показувати системні папки prefs_dialog.compact_file_size = Заокруглювати розміри файлів prefs_dialog.follow_symlinks_when_cd = Переходити за символьним посиланням при зміні папки prefs_dialog.appearance_tab = Вигляд prefs_dialog.look_and_feel = Налаштування вигляду prefs_dialog.icons_size = Розмір значків prefs_dialog.toolbar_icons = Панель інструментів prefs_dialog.command_bar_icons = Панель команд prefs_dialog.file_icons = Типи файлів prefs_dialog.use_system_file_icons = Використовувати системні значки для файлів prefs_dialog.use_system_file_icons.always = Завжди prefs_dialog.use_system_file_icons.never = Ніколи prefs_dialog.use_system_file_icons.applications = Тільки для програм prefs_dialog.edit_current_theme = Редагувати поточну тему... prefs_dialog.themes = Теми prefs_dialog.import_theme = Імпортувати тему prefs_dialog.import_look_and_feel = Імпортувати налаштування вигляду prefs_dialog.no_look_and_feel = Не знайдено налаштувань вигляду prefs_dialog.error_in_import = Помилка при імпорті теми %1 prefs_dialog.cannot_read_theme = Не вдається завантажити тему з файла %1 prefs_dialog.export_theme = Експорт prefs_dialog.import = Імпорт prefs_dialog.export = Експорт prefs_dialog.theme_type = Тип: %1 prefs_dialog.delete_theme = Видалити тему %1 безвідновно ? prefs_dialog.delete_look_and_feel = Видалити налаштування вигляду %1 безвідновно ? prefs_dialog.rename_failed = Не вдалося перейменувати тему %1 prefs_dialog.xml_file = XML файл prefs_dialog.jar_file = JAR файл prefs_dialog.mail_tab = Електронна пошта prefs_dialog.mail_settings = Налаштування відправлення пошти prefs_dialog.mail_name = Ваше ім'я prefs_dialog.mail_address = Ваша email адреса prefs_dialog.mail_server = SMTP сервер prefs_dialog.misc_tab = Додатково prefs_dialog.use_brushed_metal = Використовувати тему 'шліфований метал' (потрібно перезапустити) prefs_dialog.confirm_on_quit = Питати підтвердження при виході prefs_dialog.default_shell = Використовувати стандартну системну оболонку prefs_dialog.custom_shell = Використовувати вказану оболонку prefs_dialog.shell_encoding = Кодування командної оболонки prefs_dialog.auto_detect_shell_encoding = Авто визначення prefs_dialog.enable_bonjour_discovery = Включити службу виявлення Bonjour prefs_dialog.enable_system_notifications = Включити системні повідомлення debug_console_dialog.level = Рівень unit.byte = байт unit.bytes = байти unit.bytes_short = б unit.kb = Кб unit.mb = Мб unit.gb = Гб unit.tb = Тб unit.speed = %1/сек duration.seconds = %1с duration.minutes = %1х duration.hours = %1г duration.days = %1д duration.months = %1м duration.years = %1р theme.custom_theme = Власна тема theme.custom = Тема користувача theme.built_in = Вбудована theme.add_on = Додаток theme.current = поточна theme_could_not_be_loaded = Відбулася помилка при завантаженні теми. setup.title = Вітаємо в trolCommander setup.intro = Виберіть головні налаштування trolCommander setup.look_and_feel = Виберіть вигляд програми setup.theme = Виберіть тему оформлення font_chooser.font_size = Розмір font_chooser.font_bold = Жирний font_chooser.font_italic = Курсив color_chooser.red = Червоний color_chooser.green = Зелений color_chooser.blue = Синій color_chooser.hue = Відтінок color_chooser.brightness = Яскравість color_chooser.swatches = Зразки color_chooser.saturation = Насиченість color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = Попередній color_chooser.alpha = Прозорість color_chooser.title = Виберіть колір batch_rename_dialog.mask = Маска перейменування batch_rename_dialog.search_replace = Знайти та замінити batch_rename_dialog.search_for = Шукати batch_rename_dialog.replace_with = Замінити на batch_rename_dialog.counter = Лічильник batch_rename_dialog.start_at = Почати з batch_rename_dialog.step_by = Крок batch_rename_dialog.format = Формат batch_rename_dialog.upper_lower_case = Верхній/нижній регістр batch_rename_dialog.no_change = Без змін batch_rename_dialog.lower_case = нижній регістр batch_rename_dialog.upper_case = ВЕРХІНЙ РЕГІСТР batch_rename_dialog.first_upper = Велика парша буква batch_rename_dialog.word = Перша У Кожному Слові batch_rename_dialog.old_name = Старе ім'я batch_rename_dialog.new_name = Нове ім'я batch_rename_dialog.block_name = Не змінювати batch_rename_dialog.range = Інтервал batch_rename_dialog.proceed_renaming = %1 фа1ли з %2 будуть перейменовані. Продовжити ? batch_rename_dialog.duplicate_names = Імена співпадають batch_rename_dialog.names_conflict = Конфлікт імен! Деякі значення співпадають в старих і нових іменах. parent_folders_quick_list.empty_message = Ви на верхньому рівні recent_locations_quick_list.empty_message = Нема попереднього місцезнаходження recent_executed_files_quick_list.empty_message = Нема попереднього виконаного файла #table.hide_column = Сховати колонку #delete.symlink_warning_title = Знайдено символьне посилання #delete.symlink_warning = Даний файл є символьним посиланням:\n\n Файл: %1 посилається на: %2\n\nВидалити тільки символьне посилання чи\n\nПерейти за посиланням і видалити папку (УВАГА) ? #delete.delete_link_only = Видалити посилання #delete.delete_linked_folder = Видалати папку #Unpack.label = Розпакувати файли #Pack.label = Стиснути файли ================================================ FILE: src/main/resources/dictionary_zh_CN.properties ================================================ ok = 确定 yes = 是 no = 否 cancel = 取消 edit = 编辑 close = 关闭 reset = 复位 rename = 重新命名 apply = 应用 change = 变更 save = 保存 dont_save = 不保存 replace = 替换 dont_replace = 不替换 delete = 删除 skip = 忽略 skip_all = 全部忽略 retry = 重试 resume = 继续 overwrite = 覆盖 overwrite_if_older = 若较旧则覆盖 duplicate = 复制 apply_to_all = 全部应用 copy = 复制 move = 移动 pack = 压缩 unpack = 解压缩 download = 下载 split = 分割 combine = 合并 browse = 浏览 ask = 询问 stop = 停止 pause = 暂停 quick_search = 快速搜索 file_manager = 文件管理器 create = 创建 creating_file = 正在创建 %1 choose = 选择 customize = 自定义 choose_folder = 选择文件夹 login = 登录 password = 密码 user = 用户 encoding = 编码 preferred_encodings = 偏好的编码 license = 许可证 name = 名称 size = 大小 date = 日期 extension = 扩展名 permissions = 权限 owner = 所有者 group = 群组 location = 位置 untitled = 未命名 source = 来源 destination = 目的 recurse_directories = 递归所有目录 go_to = 转到 example = 示例 preview = 预览 comment = 注释 sample_text = 示例文本 nb_files = %1 文件 nb_folders = %1 文件夾 loading = 载入中... this_operation_cannot_be_undone = 此操作不能还原. remove = 移除 details = 详细 warning = 警告 error = 错误 generic_error = 处理指定操作时, 发生错误. folder_does_not_exist = 文件夹不存在或无法使用. this_folder_does_not_exist = 文件夹不存在或无法使用: %1 this_file_does_not_exist = 文件不存在或无法使用: %1 invalid_path = 无效路径: %1 directory_already_exists = 文件夹 %1 已存在. file_exists_in_destination = 文件已经在目的地 source_parent_of_destination = 试图传送一个文件夹到其子文件夹 cannot_read_file = 无法读取文件 %1 cannot_write_file = 无法写入文件 %1 cannot_create_folder = 无法创建目录 %1 cannot_read_folder = 无法读取文件夹内容 %1 cannot_delete_file = 无法删除文件 %1 cannot_delete_folder = 无法删除文件夹 %1 error_while_transferring = 传送文件时出现错误 %1 same_source_destination = 源/目的文件夹相同 file_already_exists = %1 已存在, 你是否要取代它 ? write_error = 写入错误 read_error = 读取错误 integrity_check_error = 完整性检查失败: 来源和目的不一致 startup_error = 一个错误阻止了 trolCommander 的启动 permissions.read = 读取 permissions.write = 写入 permissions.executable = 执行 permissions.group = 组 permissions.other = 其它 permissions.octal_notation = 八进制表示 action_categories.all = 全部 action_categories.navigation = 导航 action_categories.selection = 选择 action_categories.view = 查看 action_categories.file_operations = 文件操作 action_categories.windows = 窗口 Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = 添加书签 AddBookmark.tooltip = 收藏当前文件夹 BatchRename.label = 批量改名 EditBookmarks.label = 编辑书签 ExploreBookmarks.label = 浏览书签 EditCredentials.label = 编辑认证资料 ChangeLocation.label = 改变当前位置 ChangeDate.label = 改变日期 ChangeDate.tooltip = 改变所选文件的日期 ChangePermissions.label = 改变权限 ChangePermissions.tooltip = 改变所选文件的权限 CheckForUpdates.label = 检查更新 CompareFolders.label = 比较文件夹 ConnectToServer.label = 连接到服务器 ConnectToServer.tooltip = 连接到远程服务器 View.label = 查看 InternalView.label = 查看 (内置) View.tooltip = 查看所选文件 InternalEdit.label = 编辑 (内置) Edit.tooltip = 编辑所选文件 Copy.tooltip = 复制已标记文件 LocalCopy.label = 本地复制 LocalCopy.tooltip = 复制所选文件到当前文件夹 Move.tooltip = 移动已标记文件 Rename.tooltip = 重新命名选定文件 Mkdir.label = 新建文件夹 Mkdir.tooltip = 在当前文件夹新建目录 Mkfile.label = 创建文件 Mkfile.tooltip = 在当前文件夹创建文件 Delete.tooltip = 可能的话,将标记的文件放入回收站 PermanentDelete.label = 永久删除 PermanentDelete.tooltip = 永久删除标记的文件 Refresh.label = 刷新 Refresh.tooltip = 刷新当前文件夹 CloseWindow.label = 关闭窗口 CloseWindow.tooltip = 关闭此窗口 CopyFileNames.label = 拷贝名称 CopyFilePaths.label = 拷贝路径 CopyFilesToClipboard.label = 拷贝文件 PasteClipboardFiles.label = 粘贴文件 Email.label = 邮寄文件 Email.tooltip = 作为邮件附件发送标记文件 GoBack.label = 退回 GoBack.tooltip = 到前一个文件夹 GoForward.label = 前进 GoForward.tooltip = 到后一个文件夹 GoToHome.label = 到主目录 GoToParent.label = 上一级 GoToParent.tooltip = 到父文件夹 GoToParentInOtherPanel.label = 另一个面板返回上一级 GoToParentInBothPanels.label = 两边面板同时返回上一级 GoToRoot.label = 到根目录 SortByName.label = 按名称排序 SortByDate.label = 按日期排序 SortBySize.label = 按大小排序 SortByExtension.label = 按类型排序 SortByPermissions.label = 按权限排序 SortByOwner.label = 按所有者排序 SortByGroup.label = 按组排序 MarkGroup.label = 标记文件 MarkGroup.tooltip = 标记一组文件 UnmarkGroup.label = 取消标记 UnmarkGroup.tooltip = 取消一组文件的标记 MarkAll.label = 全部标记 UnmarkAll.label = 取消全部标记 MarkSelectedFile.label = 标记/取消标记 MarkSelectedFile.tooltip = 标记/取消标记 选定文件 MarkNextBlock.label = 往下标记一个区块 MarkPreviousBlock.label = 往上标记一个区块 MarkNextRow.label = 往下标记一行 MarkPreviousRow.label = 往上标记一行 MarkNextPage.label = 标记文件到下一页 MarkPreviousPage.label = 标记文件到上一页 MarkToFirstRow.label = 标记文件到开头 MarkToLastRow.label = 标记文件到结尾 MarkExtension.label = 标记扩展名 InvertSelection.label = 反向标记 SwapFolders.label = 文件夹互换 SwapFolders.tooltip = 交换左右文件夹 SetSameFolder.label = 设为同一文件夹 SetSameFolder.tooltip = 设置左右文件夹为同一目录 NewWindow.label = 新窗口 NewWindow.tooltip = 打开新窗口 Open.label = 打开 Open.tooltip = 进入文件夹 / 进入压缩包 / 执行 OpenNatively.label = 使用本机方式打开 OpenNatively.tooltip = 用系统的文件关联执行选定文件 OpenInOtherPanel.label = 开启于另一个面板 OpenInBothPanels.label = 开启于两边面板 RevealInDesktop.label = 显示在 %1 RunCommand.label = 执行命令 RunCommand.tooltip = 在当前文件夹执行命令 Pack.tooltip = 压缩选定文件到压缩包 Unpack.tooltip = 解压已标记的压缩包 ShowFileProperties.label = 属性 ShowFileProperties.tooltip = 显示已标记文件的属性 ShowPreferences.label = 偏好 ShowPreferences.tooltip = 配置 trolCommander ShowServerConnections.label = 显示打开的连接 Quit.label = 退出 ReverseSortOrder.label = 逆序 ToggleAutoSize.label = 自动设置栏宽 Stop.label = 停止改变文件夹 ToggleColumn.show = 显示 %1 列 ToggleColumn.hide = 隐藏 %1 列 ToggleCommandBar.show = 显示命令条 ToggleCommandBar.hide = 隐藏命令条 ToggleToolBar.show = 显示工具条 ToggleToolBar.hide = 隐藏工具条 CustomizeCommandBar.label = 自定义命令条 ToggleStatusBar.show = 显示状态行 ToggleStatusBar.hide = 隐藏状态行 ToggleShowFoldersFirst.label = 先显示文件夹 ToggleTree.label = 树状显示 PopupLeftDriveButton.label = 改变左边文件夹 PopupRightDriveButton.label = 改变右边文件夹 RecallPreviousWindow.label = 切换到前一个窗口 RecallNextWindow.label = 切换到后一个窗口 RecallWindow.label = 还原窗口 #%1 BringAllToFront.label = 所有都到前面 SwitchActiveTable.label = 在左右面板间切换 SelectNextBlock.label = 往下跳一区块 SelectPreviousBlock.label = 往上跳一区块 SelectNextPage.label = 往下跳一页 SelectPreviousPage.label = 往上跳一页 SelectNextRow.label = 往下跳一行 SelectPreviousRow.label = 往上跳一行 SelectFirstRow.label = 选取当前文件夹的第一个文件 SelectLastRow.label = 选取当前文件夹的最后一个文件 SplitEqually.label = 平均分割 SplitVertically.label = 垂直分割 SplitHorizontally.label = 水平分割 ShowKeyboardShortcuts.label = 快捷键 GoToWebsite.label = 访问网站 GoToForums.label = 访问论坛 ReportBug.label = 报告错误 Donate.label = 捐赠 ShowAbout.label = 关于 trolCommander OpenTrash.label = 打开垃圾桶 EmptyTrash.label = 清空垃圾桶 CalculateChecksum.label = 计算校验码 MaximizeWindow.label = 最大化 MaximizeWindow.label.mac_os_x = 缩放 MinimizeWindow.label = 最小化 GoToDocumentation.label = 在线文档 ShowParentFoldersQL.label = 上级目录 ShowRecentLocationsQL.label = 最近开启位置 ShowRecentExecutedFilesQL.label = 最近执行的文件 SplitFile.tooltip = 将文件分割成多个 CombineFiles.tooltip = 将分割的文件合并成原始文件 ShowDebugConsole.label = Debug 控制台 FocusPrevious.label = 将焦点移到前一个组件 FocusNext.label = 将焦点移到下一个组件 file_menu = 文件 file_menu.open_with = 用其它方式打开 mark_menu = 标记 view_menu = 查看 view_menu.show_hide_columns = 显示/隐藏栏位 go_menu = 到 bookmarks_menu = 书签 bookmarks_menu.no_bookmark = 没有书签 quick_lists_menu = 快速列表 window_menu = 窗口 help_menu = 帮助 status_bar.selected_files = %1 / %2 已选定 status_bar.connecting_to_folder = 正在连接文件夹,按ESCAPE键取消. status_bar.volume_free = 空闲:%1 status_bar.volume_capacity = 容量 %1 shortcuts_panel.title = 快捷方式 shortcuts_panel.restore_defaults = 恢复默认值 shortcuts_panel.show = 显示 shortcuts_panel.default_message = 按下 Enter 或双击快捷方式来进行编辑 shortcuts_table.action_description = 动作描述 shortcuts_table.shortcut = 快捷方式 shortcuts_table.alternate_shortcut = 替换快捷方式 shortcuts_table.type_in_a_shortcut = 在快捷方式输入 command_bar_dialog.help = 拖拽按钮来自定义命令条 table.folder_access_error_title = 文件夹存取错误 table.folder_access_error = 无法读取文件夹内容 table.download_or_browse = 你想浏览还是下载此文件? version_dialog.no_new_version_title = 没有新版本 version_dialog.no_new_version = 恭喜,你已经安装最新版本了。 version_dialog.new_version_title = 有新版本 version_dialog.new_version = 找到 trolCommander 的新版本 version_dialog.new_version_url = 找到 trolCommander 的新版本在 %1. version_dialog.not_available_title = 服务器不可到达 version_dialog.not_available = 无法从服务器上获得版本信息 version_dialog.install_and_restart = 安装并重启 version_dialog.preparing_for_update = 正在准备更新... quit_dialog.title = 退出 trolCommander quit_dialog.desc = 您有 %1 个开启中的窗口. 确认退出 ? quit_dialog.show_next_time = 下次还显示 destination_dialog.file_exists_action = 文件存在时的缺省动作 destination_dialog.verify_integrity = 检查数据完整性 destination_dialog.skip_errors = 忽略错误 file_collision_dialog.title = 文件冲突 rename_dialog.new_name = 新名称 copy_dialog.destination = 复制选定文件到 copy_dialog.error_title = 复制错误 copy_dialog.copying = 复制文件中 copy_dialog.copying_file = 正在复制 %1 pack_dialog.packing = 正在压缩文件 pack_dialog.packing_file = 正在压缩 %1 pack_dialog.error_title = 压缩错误 pack_dialog_description = 添加选定文件到 pack_dialog.archive_format = 压缩包格式 unpack_dialog.destination = 选定文件解压到 unpack_dialog.error_title = 解压错误 unpack_dialog.unpacking = 正在解压文件 unpack_dialog.unpacking_file = 正在解压 %1 optimizing_archive = 优化压缩包 %1 error_while_optimizing_archive = 优化压缩包 %1 时出错 move_dialog.move_description = 移动到 move_dialog.error_title = 移动错误 move_dialog.moving = 正在移动文件 move_dialog.moving_file = 正在移动 %1 download_dialog.description = 下载文件到 download_dialog.error_title = 下载错误 download_dialog.downloading = 正在下载 download_dialog.downloading_file = 正在下载 %1 mkfile_dialog.allocate_space = 分配空间 delete_dialog.permanently_delete.confirmation = 永久刪除选定文件 ? delete_dialog.move_to_trash.confirmation = 删除所选文件? delete_dialog.move_to_trash.confirmation_details = 文件将被移动到垃圾桶. delete_dialog.move_to_trash.option = 移动到垃圾桶 delete_dialog.move_to_trash.failed = 一个或多个文件无法移动到回收站 delete_dialog.deleting = 正在刪除 delete_dialog.error_title = 刪除錯誤 delete.deleting_file = 正在刪除 %1 email_dialog.prefs_not_set_title = 邮件尚未配置 email_dialog.prefs_not_set = 你需要先设置邮件参数 email_dialog.from = 发信人 email_dialog.to = 收信人 email_dialog.subject = 主题 email_dialog.send = 发送 email_dialog.error_title = Email文件错误 email_dialog.read_error = 无法读取子目录中的文件. email_dialog.sending = 发送文件 email.sending_file = 正在发送 %1 email.connecting_to_server = 连接到 %1 email.server_unavailable = 无法接洽邮件服务器 %1, 检查你的邮件首选项或稍后重试 email.connection_closed = 连接被服务器关闭, 邮件未发出 email.goodbye_failed = 关闭连接时出错, 邮件可能尚未发出 email.send_file_error = 无法发送文件 %1, 邮件未发出 split_file_dialog.error_title = 分割文件错误 split_file_dialog.file_to_split = 要分割的文件 split_file_dialog.target_directory = 目标目录 split_file_dialog.part_size = 分割大小 split_file_dialog.parts = 分割数目 split_file_dialog.generate_CRC = 生成 CRC 文件 split_file_dialog.max_parts = 允许的最大分割数量是 %1 split_file_dialog.auto = 自动 split_file_dialog.insert_new_media = 插入新的介质 combine_files_dialog.error_title = 合并文件错误 combine_files_job.no_crc_file = 合并成功, 没有 CRC 文件. combine_files_job.crc_read_error = 读取 CRC 文件时发生错误 combine_files_job.crc_check_failed = CRC 不符合: 需要 %2, 找到 %1 combine_files_job.crc_ok = 合并成功. CRC 校验成功. file_selection_dialog.mark = 标记 file_selection_dialog.unmark = 取消标记 file_selection_dialog.mark_description = 要标记文件名为 file_selection_dialog.unmark_description = 要取消标记的文件名为 file_selection_dialog.case_sensitive = 区分大小写 file_selection_dialog.include_folders = 包含文件夹 progress_dialog.starting = 传送开始... progress_dialog.transferred = 已传送 %1 速度 %2 progress_dialog.elapsed_time = 已用时间 progress_dialog.advanced = 高级 progress_dialog.current_speed = 当前速度 progress_dialog.limit_speed = 限制速度 progress_dialog.close_when_finished = 完成后关闭窗口 progress_dialog.processing_files = 正在处理文件 progress_dialog.processing_file = 正在处理 %1 progress_dialog.verifying_file = 正在检查 %1 progress_dialog.job_finished = 工作完成 progress_dialog.job_error = 工作出错 properties_dialog.file_properties = %1 属性 properties_dialog.contents = 內容 properties_dialog.calculating = 正在计算... calculate_checksum_dialog.checksum_algorithm = 校验码算法 calculate_checksum_dialog.temporary_file = 临时文件 change_date_dialog.now = 现在 change_date_dialog.specific_date = 指定日期 run_dialog.run_command_description = 在此文件夹执行 run_dialog.run_in_home_description = 在主目录下执行 run_dialog.command_output = 命令输出 run_dialog.run = 运行 run_dialog.clear_history = 清除历史记录 server_connect_dialog.server_type = 连接类型 server_connect_dialog.server = 服务器 server_connect_dialog.share = 共享 server_connect_dialog.domain = 域 server_connect_dialog.username = 用户名 server_connect_dialog.initial_dir = 起始目录 server_connect_dialog.port = 端口 server_connect_dialog.server_url = 服务器URL server_connect_dialog.http_url = 网站URL server_connect_dialog.connect = 连接 server_connect_dialog.protocol = 协议 server_connect_dialog.nfs_version = NFS版本 server_connect_dialog.private_key = 私钥 server_connect_dialog.passphrase = 密码 ftp_connect.passive_mode = 启用被动模式(Passive) ftp_connect.anonymous_user = 匿名用户 ftp_connect.nb_connection_retries = 连接重试次数 ftp_connect.retry_delay = 重试延迟时间(秒) http_connect.basic_authentication = HTTP Basic Authentication (可选) server_connections_dialog.disconnect = 断开 server_connections_dialog.connection_busy = 正忙 server_connections_dialog.connection_idle = 空闲 bonjour.bonjour_services = Bonjour 服务 bonjour.no_service_discovered = 未发现任何服务 bonjour.bonjour_disabled = Bonjour 被禁 auth_dialog.title = 验证 auth_dialog.desc = 請輸入用户名及密码 auth_dialog.server = 服务器 auth_dialog.store_credentials = 储存用户名及密码 (仅作简单加密!) auth_dialog.connect_as = 连接为 auth_dialog.authentication_failed = 验证失败 sortable_list.move_up = 上移 sortable_list.move_down = 下移 add_bookmark_dialog.add = 添加 edit_bookmarks_dialog.new = 添加 file_viewer.view_error_title = 查看错误 file_viewer.view_error = 无法查看文件 file_viewer.file_menu = 文件 file_viewer.close = 关闭 file_viewer.large_file_warning = 文件可能太大无法进行此项操作 file_viewer.open_anyway = 强制打开 text_viewer.edit = 编辑 text_viewer.copy = 复制 text_viewer.select_all = 全选 text_viewer.find = 查找 text_viewer.find_next = 查找下一个 text_viewer.find_previous = 查找上一个 text_viewer.binary_file_warning = 这是二进制文件 image_viewer.controls_menu = 控制 image_viewer.zoom_in = 放大 image_viewer.zoom_out = 缩小 file_editor.edit_error_title = 编辑错误 file_editor.edit_error = 无法编辑文件 file_editor.save = 保存 file_editor.save_as = 另存为... file_editor.save_warning = 关闭前保存对文件的修改 ? file_editor.cannot_write = 无法写入文件. text_editor.cut = 剪切 text_editor.paste = 粘贴 shortcuts_dialog.quick_search.start_search = 键入任何字符启用快速搜索 shortcuts_dialog.quick_search.cancel_search = 取消快速搜索 shortcuts_dialog.quick_search.remove_last_char = 从快速搜索字符串中删除最后一個字符 shortcuts_dialog.quick_search.jump_to_previous = 跳至上次快速搜索的结果 shortcuts_dialog.quick_search.jump_to_next = 跳至下个快速搜索的结果 shortcuts_dialog.quick_search.mark_jump_next = 标记/取消标记当前文件并跳至下各搜索结果 theme_editor.title = 主题编辑器 theme_editor.folder_tab = 文件夹面板 theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Shell 历史记录 theme_editor.statusbar_tab = 状态栏 theme_editor.free_space = 剩余空间 theme_editor.free_space.ok = OK theme_editor.free_space.warning = 告警 theme_editor.free_space.critical = 紧急 theme_editor.locationbar_tab = 位置栏 theme_editor.editor_tab = 文件编辑器 theme_editor.font = 字体 theme_editor.active_panel = 取得焦点 theme_editor.inactive_panel = 失去焦点 theme_editor.general = 一般 theme_editor.could_not_save_theme = 无法写入主题 %1 theme_editor.border = 边框 theme_editor.background = 背景 theme_editor.alternate_background = 间隔替代背景 theme_editor.unfocused_background = 背景(无焦点) theme_editor.quick_search.unmatched_file = 未匹配的文件 theme_editor.text = 文本 theme_editor.progress = 进度 theme_editor.normal = 正常 theme_editor.normal_unfocused = 正常(无焦点) theme_editor.selected = 选中的 theme_editor.selected_unfocused = 选中的(无焦点) theme_editor.color = 颜色 theme_editor.colors = 颜色 theme_editor.plain_file = 一般文件 theme_editor.marked_file = 已标记文件 theme_editor.hidden_file = 隐藏文件 theme_editor.folder = 文件夹 theme_editor.archive_file = 压缩文件 theme_editor.symbolic_link = 符号连接 theme_editor.header = 标头 theme_editor.item = 项目 command_bar_customize_dialog.available_actions = 可供选择的动作 command_bar_customize_dialog.modifier = 修饰键 prefs_dialog.title = 首选项 prefs_dialog.general_tab = 一般 prefs_dialog.day = 日 prefs_dialog.month = 月 prefs_dialog.year = 年 prefs_dialog.language = 语言 (需重新启动) prefs_dialog.date_time = 日期及时间格式 prefs_dialog.time = 时间 prefs_dialog.date = 日期 prefs_dialog.date_separator = 分隔符 prefs_dialog.time_12_hour = 12小时制 prefs_dialog.time_24_hour = 24小时制 prefs_dialog.show_seconds = 显示秒 prefs_dialog.show_century = 显示世纪 prefs_dialog.check_for_updates_on_startup = 启动时检查新版本 prefs_dialog.show_splash_screen = 启动时显示欢迎画面 prefs_dialog.folders_tab = 文件夹 prefs_dialog.startup_folders = 起始文件夹 prefs_dialog.left_folder = 左文件夹 prefs_dialog.right_folder = 右文件夹 prefs_dialog.last_folder = 最后一次访问的文件夹 prefs_dialog.custom_folder = 定制文件夹 prefs_dialog.show_hidden_files = 显示隐藏文件 prefs_dialog.show_ds_store_files = 显示 .DS_Store 文件 prefs_dialog.show_system_folders = 显示系统文件夾 prefs_dialog.compact_file_size = 显示文件概略大小 prefs_dialog.follow_symlinks_when_cd = 改变当前目录时跟随符号连接 prefs_dialog.appearance_tab = 展现 prefs_dialog.look_and_feel = 外观 prefs_dialog.icons_size = 图标大小 prefs_dialog.toolbar_icons = 工具条 prefs_dialog.command_bar_icons = 命令条 prefs_dialog.file_icons = 文件类型 prefs_dialog.use_system_file_icons = 使用系统文件图标 prefs_dialog.use_system_file_icons.always = 总是 prefs_dialog.use_system_file_icons.never = 从不 prefs_dialog.use_system_file_icons.applications = 只对应用程序 prefs_dialog.edit_current_theme = 编辑当前主题 prefs_dialog.themes = 主题 prefs_dialog.import_theme = 导入主题 prefs_dialog.import_look_and_feel = 导入外观(look & feel) prefs_dialog.no_look_and_feel = 找不到外观(look & feel). prefs_dialog.error_in_import = 导入主题 %1 时出错. prefs_dialog.cannot_read_theme = 不能从文件 %1 中载入主题 prefs_dialog.export_theme = 导出 %1 prefs_dialog.import = 导入 prefs_dialog.export = 导出 prefs_dialog.theme_type = 类型: %1 prefs_dialog.delete_theme = 永久删除主题 %1 ? prefs_dialog.delete_look_and_feel = 永久删除外观(look & feel) %1? prefs_dialog.rename_failed = 未能重命名主题 %1 prefs_dialog.xml_file = XML 文件 prefs_dialog.jar_file = JAR 文件 prefs_dialog.mail_tab = 邮件 prefs_dialog.mail_settings = 外发邮件设置 prefs_dialog.mail_name = 你的姓名 prefs_dialog.mail_address = 你的邮件地址 prefs_dialog.mail_server = SMTP服务器 prefs_dialog.misc_tab = 其它 prefs_dialog.use_brushed_metal = 使用 'brushed metal' 外观 (需重新启动) prefs_dialog.confirm_on_quit = 退出时显示确认对话框 prefs_dialog.default_shell = 使用缺省的系统 shell prefs_dialog.custom_shell = 使用定制 shell prefs_dialog.shell_encoding = Shell 编码 prefs_dialog.auto_detect_shell_encoding = 自动检测 prefs_dialog.enable_bonjour_discovery = 打开 Bonjour 服务发现功能 prefs_dialog.enable_system_notifications = 打开系统通告 debug_console_dialog.level = 等级 unit.byte = 字节 unit.bytes = 字节 unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mo duration.years = %1y theme.custom_theme = 定制的主题 theme.custom = 定制的 theme.built_in = 内置的 theme.add_on = 附加 theme.current = 当前 theme_could_not_be_loaded = 在装入此主题时发生错误 setup.title = 欢迎进入 trolCommander setup.intro = 请选择你所希望的 trolCommander 行为模式 setup.look_and_feel = 选择你要的外观 setup.theme = 选择你的主题 font_chooser.font_size = 字号 font_chooser.font_bold = 粗体 font_chooser.font_italic = 斜体 color_chooser.red = 红色 color_chooser.green = 绿色 color_chooser.blue = 蓝色 color_chooser.hue = 色调 color_chooser.brightness = 亮度 color_chooser.swatches = 采样 color_chooser.saturation = 饱和度 color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = 最近的 color_chooser.alpha = Alpha 透明度 color_chooser.title = 挑选一个颜色 batch_rename_dialog.mask = 重命名模式 batch_rename_dialog.search_replace = 查找并替换 batch_rename_dialog.search_for = 查找 batch_rename_dialog.replace_with = 替换为 batch_rename_dialog.counter = 计数器 batch_rename_dialog.start_at = 开始于 batch_rename_dialog.step_by = 每次递增 batch_rename_dialog.format = 格式 batch_rename_dialog.upper_lower_case = 大小写 batch_rename_dialog.no_change = 无变更 batch_rename_dialog.lower_case = 小写 batch_rename_dialog.upper_case = 大写 batch_rename_dialog.first_upper = 首字母大写 batch_rename_dialog.word = 每个单词的首字母 batch_rename_dialog.old_name = 原名称 batch_rename_dialog.new_name = 新名称 batch_rename_dialog.block_name = 保留 batch_rename_dialog.range = 范围 batch_rename_dialog.proceed_renaming = %1 个文件(全部 %2 个)将会被重命名. 确认执行? batch_rename_dialog.duplicate_names = 重复的名称 ! parent_folders_quick_list.empty_message = 当前位置没有上一级 recent_locations_quick_list.empty_message = 没有最近打开的位置 recent_executed_files_quick_list.empty_message = 没有最近执行的文件 #pack.error_on_file = 压缩时发生错误 %1 #pack_dialog.cannot_write = 无法在目的文件夹创建压缩文件 #unpack.unable_to_open_zip = 无法打开压缩文件 %1 #mkdir_dialog.title = 新建文件夹 #mkdir_dialog.error_title = 新建文件夹错误 #edit_bookmarks_dialog.remove = 删除 #mkdir_dialog.description = 创建文件夹 #done = 完成 #progress_dialog.hide = 隐藏 #move_dialog.rename_description = 文件重新命名为 #theme_editor.shell_font = Shell字体 #theme_editor.history_font = 历史字体 #theme_editor.shell_colors = Shell 颜色 #theme_editor.history_colors = 历史颜色 #auth_dialog.error_was = 错误: %1 #table.hide_column = 隐藏栏位 #delete.symlink_warning_title = 发现符号连接 #delete.symlink_warning = 此文件像是為符号连接:\n\n 文件: %1\n 连接到: %2\n\n只刪除符号连接或\n跟随连接刪除文件夹 (小心使用) ? #delete.delete_link_only = 刪除连接 #delete.delete_linked_folder = 刪除文件夹 #Unpack.label = 解压缩文件 #Pack.label = 压缩文件 ================================================ FILE: src/main/resources/dictionary_zh_TW.properties ================================================ ok = 確定 yes = 是 no = 否 cancel = 取消 edit = 編輯 close = 關閉 reset = 重置 rename = 重新命名 apply = 套用 change = 變更 save = 儲存 dont_save = 不儲存 replace = 取代 dont_replace = 不取代 delete = 刪除 skip = 略過 skip_all = 全部略過 retry = 重試 resume = 繼續 overwrite = 覆寫 overwrite_if_older = 若較舊則覆寫 duplicate = 複製 apply_to_all = 全部套用 copy = 複製 move = 搬移 pack = 壓縮 unpack = 解壓縮 download = 下載 split = 分割 combine = 合併 browse = 瀏覽 ask = 詢問 stop = 停止 pause = 暫停 quick_search = 快速搜尋 file_manager = 檔案管理員 create = 新增 creating_file = 正在建立 %1 choose = 選擇 customize = 自訂 choose_folder = 選擇資料夾 login = 登入 password = 密碼 user = 使用者 encoding = 編碼 preferred_encodings = 偏好的編碼 license = 版權 name = 名稱 size = 大小 date = 日期 extension = 延伸檔名 permissions = 權限 owner = 擁有者 group = 群組 location = 位置 untitled = 未命名 source = 來源 destination = 目的 recurse_directories = 遞迴所有目錄 go_to = 前往 example = 範例 preview = 預覽 comment = 備註 sample_text = 範例文字 nb_files = %1 檔案 nb_folders = %1 資料夾 loading = 載入中... this_operation_cannot_be_undone = 此操作無法回復 remove = 移除 details = 細節 warning = 警告 error = 錯誤 generic_error = 當進行指定操作時, 發生錯誤. folder_does_not_exist = 資料夾不存在或無法使用. this_folder_does_not_exist = 資料夾不存在或無法使用: %1 this_file_does_not_exist = 此檔案不存在或是無法使用: %1 invalid_path = 不合法的路徑: %1 directory_already_exists = 文件夹 %1 已存在. directory_already_exists = 文件夹 %1 已存在. file_exists_in_destination = 檔案已經存在於目的地 source_parent_of_destination = 嘗試移轉一個資料夾到其子目錄中 cannot_read_file = 無法讀取檔案 %1 cannot_write_file = 無法寫入檔案 %1 cannot_create_folder = 無法產生資料夾 %1 cannot_read_folder = 無法讀取資料夾內容 %1 cannot_delete_file = 無法刪除檔案 %1 cannot_delete_folder = 無法刪除資料夾 %1 error_while_transferring = 在傳輸檔案時發生錯誤 %1 same_source_destination = 來源及目的資料夾一樣 file_already_exists = %1 已存在, 你是否要取代它 ? write_error = 寫入錯誤 read_error = 讀取錯誤 integrity_check_error = 完整性檢查失敗: 來源及目的不一致 startup_error = 有錯誤造成 trolCommander 無法啟動 permissions.read = 讀取 permissions.write = 寫入 permissions.executable = 執行 permissions.group = 群組 permissions.other = 其它 permissions.octal_notation = 八進位顯示 action_categories.all = 全部 action_categories.navigation = 導覽 action_categories.selection = 選取 action_categories.view = 檢視 action_categories.file_operations = 檔案操作 action_categories.windows = 視窗 Terminal.label = Terminal Terminal.tooltip = Terminal FindFile.label = Find file FindFile.tooltip = Find file AddBookmark.label = 新增書籤 AddBookmark.tooltip = 加入目前資料夾為書籤 BatchRename.label = 批次改名 EditBookmarks.label = 編輯書籤 ExploreBookmarks.label = 檢視書籤 EditCredentials.label = 編輯認證資料 ChangeLocation.label = 變更目前位置 ChangeDate.label = 變更日期 ChangeDate.tooltip = 變更所選檔案的日期 ChangePermissions.label = 變更權限 ChangePermissions.tooltip = 變更所選檔案的權限 CheckForUpdates.label = 檢查更新版本 CompareFolders.label = 比較資料夾 ConnectToServer.label = 連接至伺服器 ConnectToServer.tooltip = 連接至遠端伺服器 View.label = 檢視 InternalView.label = 檢視 (內建) View.tooltip = 檢視已選取檔案 InternalEdit.label = 編輯 (內建) Edit.tooltip = 編輯已選取檔案 Copy.tooltip = 複製已標記檔案 LocalCopy.label = 本地複製 LocalCopy.tooltip = 複製已選取檔案至此資料夾 Move.tooltip = 搬移已標記檔案 Rename.tooltip = 重新命名已選取檔案 Mkdir.label = 新增資料夾 Mkdir.tooltip = 在此處新增資料夾 Mkfile.label = 新增檔案 Mkfile.tooltip = 在此處新增檔案 Delete.tooltip = 刪除已標記檔案 PermanentDelete.label = 永久刪除 PermanentDelete.tooltip = 永久刪除標記檔案 Refresh.label = 重新整理 Refresh.tooltip = 重新整理此資料夾 CloseWindow.label = 關閉視窗 CloseWindow.tooltip = 關閉此視窗 CopyFileNames.label = 拷貝名稱 CopyFilePaths.label = 拷貝路徑 CopyFilesToClipboard.label = 拷貝檔案 PasteClipboardFiles.label = 貼上檔案 Email.label = 郵寄檔案 Email.tooltip = 傳送已標記檔案成電郵附件 GoBack.label = 退回 GoBack.tooltip = 到前一個資料夾 GoForward.label = 前進 GoForward.tooltip = 到後一個資料夾 GoToHome.label = 跳至家目錄 GoToParent.label = 上一層 GoToParent.tooltip = 到上一層資料夾 GoToParentInOtherPanel.label = 另一個面板回到上一層 GoToParentInBothPanels.label = 兩邊面板同時回到上一層 GoToRoot.label = 跳至根目錄 SortByName.label = 依名稱排序 SortByDate.label = 依日期排序 SortBySize.label = 依大小排序 SortByExtension.label = 依類型排序 SortByPermissions.label = 依權限排序 SortByOwner.label = 依擁有者排序 SortByGroup.label = 依群組排序 MarkGroup.label = 標記檔案 MarkGroup.tooltip = 標記檔案群組 UnmarkGroup.label = 取消標記 UnmarkGroup.tooltip = 取消檔案群組標記 MarkAll.label = 全部標記 UnmarkAll.label = 取消全部標記 MarkSelectedFile.label = 標記/取消標記 MarkSelectedFile.tooltip = 標記/取消標記 已選取檔案 MarkNextBlock.label = 往下標記一個區塊 MarkPreviousBlock.label = 往上標記一個區塊 MarkNextRow.label = 往下標記一行 MarkPreviousRow.label = 往上標記一行 MarkNextPage.label = 標記檔案至下一頁 MarkPreviousPage.label = 標記檔案至上一頁 MarkToFirstRow.label = 標記檔案至開頭 MarkToLastRow.label = 標記檔案至結尾 MarkExtension.label = 標記延伸檔名 InvertSelection.label = 反向標記 SwapFolders.label = 資料夾互換 SwapFolders.tooltip = 切換左右方的資料夾 SetSameFolder.label = 跳至同一資料夾 SetSameFolder.tooltip = 設定左右資料夾成一樣 NewWindow.label = 新視窗 NewWindow.tooltip = 開啟新視窗 Open.label = 開啟 Open.tooltip = 進入資料夾 / 進入壓縮檔 / 執行 OpenNatively.label = 使用原先方式來開啟 OpenNatively.tooltip = 利用系統的檔案關聯來執行已選取檔案 OpenInOtherPanel.label = 開啟於其它面板 OpenInBothPanels.label = 開啟於兩邊面板 RevealInDesktop.label = 顯示在 %1 RunCommand.label = 執行命令 RunCommand.tooltip = 在此資料夾執行命令 Pack.tooltip = 將標記檔案加入壓縮檔 Unpack.tooltip = 解開標記的壓縮檔 ShowFileProperties.label = 屬性 ShowFileProperties.tooltip = 顯示已標記檔案的屬性 ShowPreferences.label = 設定 ShowPreferences.tooltip = 設定 trolCommander ShowServerConnections.label = 顯示開啟的連線 Quit.label = 離開 ReverseSortOrder.label = 反向排序 ToggleAutoSize.label = 自動調寬欄位 Stop.label = 停止資料夾變更 ToggleColumn.show = 顯示 %1 欄位 ToggleColumn.hide = 隱藏 %1 欄位 ToggleCommandBar.show = 顯示命令列 ToggleCommandBar.hide = 隱藏命令列 ToggleToolBar.show = 顯示工具列 ToggleToolBar.hide = 隱藏工具列 CustomizeCommandBar.label = 自訂命令工作列 ToggleStatusBar.show = 顯示狀態列 ToggleStatusBar.hide = 隱藏狀態列 ToggleShowFoldersFirst.label = 優先顯示資料夾 ToggleTree.label = 樹狀檢視 PopupLeftDriveButton.label = 變更左方資料夾 PopupRightDriveButton.label = 變更右方資料夾 RecallPreviousWindow.label = 切換至前一個視窗 RecallNextWindow.label = 切換至後一個視窗 RecallWindow.label = 還原視窗 #%1 BringAllToFront.label = 顯示全部視窗至最前面 SwitchActiveTable.label = 在左右面板間交互切換 SelectNextBlock.label = 往下跳一區塊 SelectPreviousBlock.label = 往上跳一區塊 SelectNextPage.label = 往下跳一頁 SelectPreviousPage.label = 往上跳一頁 SelectNextRow.label = 往下跳一行 SelectPreviousRow.label = 往上跳一行 SelectFirstRow.label = 選取此資料夾的第一個檔案 SelectLastRow.label = 選取此資料夾的最後一個檔案 SplitEqually.label = 等距分割 SplitVertically.label = 垂直分割 SplitHorizontally.label = 水平分割 ShowKeyboardShortcuts.label = 快捷鍵 GoToWebsite.label = 連至網站 GoToForums.label = 連至討論區 ReportBug.label = 回報問題 Donate.label = 捐贈 ShowAbout.label = 有關 trolCommander OpenTrash.label = 開啟資源回收桶 EmptyTrash.label = 清空資源回收桶 CalculateChecksum.label = 計算驗證碼 MaximizeWindow.label = 最大化 MinimizeWindow.label = 最小化 GoToDocumentation.label = 線上文件 ShowParentFoldersQL.label = 上層目錄 ShowRecentLocationsQL.label = 最近開啟位置 ShowRecentExecutedFilesQL.label = 最近執行檔案 SplitFile.tooltip = 將檔案分割成多個 CombineFiles.tooltip = 將分割檔合併成原檔案 ShowDebugConsole.label = 偵錯主控台 FocusPrevious.label = 將輸入焦點移至前一個元件 FocusNext.label = 將輸入焦點移至後一個元件 file_menu = 檔案 file_menu.open_with = 用其它方式開啟 mark_menu = 標記 view_menu = 檢視 view_menu.show_hide_columns = 顯示/隱藏欄位 go_menu = 跳至 bookmarks_menu = 書籤 bookmarks_menu.no_bookmark = 沒有書籤 quick_lists_menu = 快速列表 window_menu = 視窗 help_menu = 求助 status_bar.selected_files = %1 / %2 已選取 status_bar.connecting_to_folder = 連接至資料夾, 按ESCAPE取消. status_bar.volume_free = 可用空間: %1 status_bar.volume_capacity = 全部空間: %1 shortcuts_panel.title = 捷徑 shortcuts_panel.restore_defaults = 回復預設值 shortcuts_panel.show = 顯示 shortcuts_panel.default_message = 按下 Enter 或雙擊捷徑來進行編輯 shortcuts_table.action_description = 操作描述 shortcuts_table.shortcut = 捷徑 shortcuts_table.alternate_shortcut = 替換捷徑 shortcuts_table.type_in_a_shortcut = 在捷徑輸入 command_bar_dialog.help = 拖曳按鈕來自訂命令工作列 table.folder_access_error_title = 資料夾存取錯誤 table.folder_access_error = 無法讀取資料夾內容 table.download_or_browse = 你要瀏覽或是下載此檔案? version_dialog.no_new_version_title = 沒有新版本 version_dialog.no_new_version = 恭喜, 你已經安裝最新版本了. version_dialog.new_version_title = 找到新版本 version_dialog.new_version = 找到 trolCommander 的新版本. version_dialog.new_version_url = 找到 trolCommander 的新版本在 %1. version_dialog.not_available_title = 伺服器無法連結 version_dialog.not_available = 無法從伺服器上取得版本資訊. version_dialog.install_and_restart = 安裝及重啟 version_dialog.preparing_for_update = 準備更新中... quit_dialog.title = 離開 trolCommander quit_dialog.desc = 你有 %1 個開啟的視窗. 確定要離開 ? quit_dialog.show_next_time = 下次再顯示 destination_dialog.file_exists_action = 當檔案存在時的預設動作 destination_dialog.verify_integrity = 檢查資料完整性 destination_dialog.skip_errors = 略過錯誤 file_collision_dialog.title = 檔案已存在 rename_dialog.new_name = 新名稱 copy_dialog.destination = 複製已選取檔案至 copy_dialog.error_title = 複製錯誤 copy_dialog.copying = 複製檔案 copy_dialog.copying_file = 正在複製 %1 pack_dialog.packing = 正在壓縮檔案 pack_dialog.packing_file = 正在壓縮 %1 pack_dialog.error_title = 壓縮錯誤 pack_dialog_description = 新增已選取檔案至 pack_dialog.archive_format = 壓縮檔格式 unpack_dialog.destination = 解壓縮已選取檔案至 unpack_dialog.error_title = 解壓縮錯誤 unpack_dialog.unpacking = 正在解壓縮檔案 unpack_dialog.unpacking_file = 正在解壓縮 %1 optimizing_archive = 最佳化備份檔 %1 error_while_optimizing_archive = 最佳化壓縮檔 %1 時出錯 move_dialog.move_description = 搬移至 move_dialog.error_title = 搬移錯誤 move_dialog.moving = 正在搬移檔案 move_dialog.moving_file = 正在搬移 %1 download_dialog.description = 下載檔案至 download_dialog.error_title = 下載錯誤 download_dialog.downloading = 正在下載 download_dialog.downloading_file = 正在下載 %1 mkfile_dialog.allocate_space = 分配空間 delete_dialog.permanently_delete.confirmation = 永久刪除已選取檔案 ? delete_dialog.move_to_trash.confirmation = 刪除所選檔案 ? delete_dialog.move_to_trash.confirmation_details = 檔案將被移至資源回收桶 delete_dialog.move_to_trash.option = 移至資源回收桶 delete_dialog.move_to_trash.failed = 一個或多個檔案無法移至垃圾桶 delete_dialog.deleting = 正在刪除 delete_dialog.error_title = 刪除錯誤 delete.deleting_file = 正在刪除 %1 email_dialog.prefs_not_set_title = 郵件尚未設定 email_dialog.prefs_not_set = 你必須要先設定郵件參數 email_dialog.from = 寄件者 email_dialog.to = 收件者 email_dialog.subject = 主旨 email_dialog.send = 傳送 email_dialog.error_title = 郵寄檔案錯誤 email_dialog.read_error = 無法讀取在子目錄中的檔案. email_dialog.sending = 傳送檔案 email.sending_file = 正在傳送 %1 email.connecting_to_server = 連接至 %1 email.server_unavailable = 無法連接至郵件伺服器 %1, 檢查你的郵件設定或稍後重試. email.connection_closed = 連線被伺服器關閉, 郵件未寄出. email.goodbye_failed = 當關閉連線時發生錯誤, 郵件可能尚未寄出. email.send_file_error = 無法傳送檔案 %1, 郵件未寄出. split_file_dialog.error_title = 分割檔案錯誤 split_file_dialog.file_to_split = 欲分割的檔案 split_file_dialog.target_directory = 目的資料夾 split_file_dialog.part_size = 分割大小 split_file_dialog.parts = 分割數目 split_file_dialog.generate_CRC = 產生 CRC 檔案 split_file_dialog.max_parts = 最多允許的分割數目為 %1 split_file_dialog.auto = 自動 split_file_dialog.insert_new_media = 插入新的媒體 combine_files_dialog.error_title = 合併檔案錯誤 combine_files_job.no_crc_file = 合併成功, 沒有 CRC 檔案. combine_files_job.crc_read_error = 當讀取 CRC 檔案時發生錯誤. combine_files_job.crc_check_failed = CRC 不符合: 預期 %2, 發現 %1 combine_files_job.crc_ok = 合併成功. CRC 驗證成功. file_selection_dialog.mark = 標記 file_selection_dialog.unmark = 取消標記 file_selection_dialog.mark_description = 標記檔案, 若檔名為 file_selection_dialog.unmark_description = 取消標記, 若檔名為 file_selection_dialog.case_sensitive = 分大小寫 file_selection_dialog.include_folders = 包括資料夾 progress_dialog.starting = 傳送開始... progress_dialog.transferred = 已傳送 %1 速度 %2 progress_dialog.elapsed_time = 已用時間 progress_dialog.advanced = 進階 progress_dialog.current_speed = 目前速度 progress_dialog.limit_speed = 限制速度 progress_dialog.close_when_finished = 完成時關閉視窗 progress_dialog.processing_files = 正在處理檔案 progress_dialog.processing_file = 正在處理 %1 progress_dialog.verifying_file = 正在檢查 %1 progress_dialog.job_finished = 工作完成 progress_dialog.job_error = 工作錯誤 properties_dialog.file_properties = %1 屬性 properties_dialog.contents = 內容 properties_dialog.calculating = 計算中... calculate_checksum_dialog.checksum_algorithm = 驗證碼演算法 calculate_checksum_dialog.temporary_file = 暫存檔 change_date_dialog.now = 現在 change_date_dialog.specific_date = 指定日期 run_dialog.run_command_description = 在此資料夾執行 run_dialog.run_in_home_description = 在家目錄下執行 run_dialog.command_output = 命令結果輸出 run_dialog.run = 執行 run_dialog.clear_history = 清除歷史記錄 server_connect_dialog.server_type = 連接類型 server_connect_dialog.server = 伺服器 server_connect_dialog.share = 共用 server_connect_dialog.domain = 網域 server_connect_dialog.username = 帳號 server_connect_dialog.initial_dir = 初始位置 server_connect_dialog.port = 埠號 server_connect_dialog.server_url = 伺服器URL server_connect_dialog.http_url = Web網址 server_connect_dialog.connect = 連接 server_connect_dialog.protocol = 協議 server_connect_dialog.nfs_version = NFS 版本 server_connect_dialog.private_key = 私鑰 server_connect_dialog.passphrase = 密碼 ftp_connect.passive_mode = 啟動被動模式(Passive) ftp_connect.anonymous_user = 匿名使用者 ftp_connect.nb_connection_retries = 連線重試次數 ftp_connect.retry_delay = 重試延遲時間(秒) http_connect.basic_authentication = HTTP Basic Authentication (非必要) server_connections_dialog.disconnect = 停止連接 server_connections_dialog.connection_busy = 忙碌 server_connections_dialog.connection_idle = 閒置 bonjour.bonjour_services = Bonjour 服務 bonjour.no_service_discovered = 未發現任何服務 bonjour.bonjour_disabled = Bonjour 功能關閉 auth_dialog.title = 驗證 auth_dialog.desc = 請輸入帳號及密碼 auth_dialog.server = 伺服器 auth_dialog.store_credentials = 儲存登入帳號及密碼(僅作簡易加密) auth_dialog.connect_as = 連線為 auth_dialog.authentication_failed = 驗證失敗 sortable_list.move_up = 上移 sortable_list.move_down = 下移 add_bookmark_dialog.add = 新增 edit_bookmarks_dialog.new = 新增 file_viewer.view_error_title = 檢視錯誤 file_viewer.view_error = 無法檢視檔案 file_viewer.file_menu = 檔案 file_viewer.close = 關閉 file_viewer.large_file_warning = 檔案可能太大而無法進行此操作 file_viewer.open_anyway = 強迫開啟 text_viewer.edit = 編輯 text_viewer.copy = 複製 text_viewer.select_all = 全部選取 text_viewer.find = 尋找 text_viewer.find_next = 尋找下一個 text_viewer.find_previous = 尋找上一個 text_viewer.binary_file_warning = 此為二進位檔案 image_viewer.controls_menu = 控制 image_viewer.zoom_in = 放大 image_viewer.zoom_out = 縮小 file_editor.edit_error_title = 編輯錯誤 file_editor.edit_error = 無法編輯檔案 file_editor.save = 儲存 file_editor.save_as = 另存為... file_editor.save_warning = 在關閉之前是否儲存檔案變更 ? file_editor.cannot_write = 無法寫入檔案. text_editor.cut = 剪下 text_editor.paste = 貼上 shortcuts_dialog.quick_search.start_search = 輸入任何字元來啟用快速搜尋 shortcuts_dialog.quick_search.cancel_search = 取消快速搜尋 shortcuts_dialog.quick_search.remove_last_char = 從快速搜尋的字串中移除最後一個字元 shortcuts_dialog.quick_search.jump_to_previous = 跳至上一個快速搜尋的結果 shortcuts_dialog.quick_search.jump_to_next = 跳至下一個快速搜尋的結果 shortcuts_dialog.quick_search.mark_jump_next = 標記/取消標記目前檔案並跳至下一個搜尋結果 theme_editor.title = 佈景主題編輯器 theme_editor.folder_tab = Folder pane theme_editor.shell_tab = Shell theme_editor.shell_history_tab = Shell 歷史記錄 theme_editor.statusbar_tab = 狀態列 theme_editor.free_space = 剩餘空間 theme_editor.free_space.ok = OK theme_editor.free_space.warning = 警告 theme_editor.free_space.critical = 嚴重 theme_editor.locationbar_tab = 位址列 theme_editor.editor_tab = 檔案編輯器 theme_editor.font = 字型 theme_editor.active_panel = 取得焦點 theme_editor.inactive_panel = 失去焦點 theme_editor.general = 一般 theme_editor.could_not_save_theme = 無法寫入佈景主題 %1 theme_editor.border = 框線 theme_editor.background = 背景 theme_editor.alternate_background = 間隔替代背景 theme_editor.unfocused_background = 背景 (無焦點) theme_editor.quick_search.unmatched_file = 沒有匹配的檔案 theme_editor.text = 文字 theme_editor.progress = 進度 theme_editor.normal = 一般 theme_editor.normal_unfocused = 一般 (無焦點) theme_editor.selected = 被選取 theme_editor.selected_unfocused = 被選取 (無焦點) theme_editor.color = 顏色 theme_editor.colors = 顏色 theme_editor.plain_file = 一般檔案 theme_editor.marked_file = 已標記檔案 theme_editor.hidden_file = 隱藏檔 theme_editor.folder = 資料夾 theme_editor.archive_file = 壓縮檔 theme_editor.symbolic_link = 符號連結 theme_editor.header = 標頭 theme_editor.item = 項目 command_bar_customize_dialog.available_actions = 可供選擇的操作 command_bar_customize_dialog.modifier = 切換鍵 prefs_dialog.title = 設定 prefs_dialog.general_tab = 一般 prefs_dialog.day = 日 prefs_dialog.month = 月 prefs_dialog.year = 年 prefs_dialog.language = 語言 (需重新啟動) prefs_dialog.date_time = 日期及時間格式 prefs_dialog.time = 時間 prefs_dialog.date = 日期 prefs_dialog.date_separator = 分隔符號 prefs_dialog.time_12_hour = 12小時制 prefs_dialog.time_24_hour = 24小時制 prefs_dialog.show_seconds = 顯示秒數 prefs_dialog.show_century = 顯示世紀 prefs_dialog.check_for_updates_on_startup = 啟動時檢查是否有更新版本 prefs_dialog.show_splash_screen = 顯示啟動歡迎畫面 prefs_dialog.folders_tab = 資料夾 prefs_dialog.startup_folders = 開始資料夾 prefs_dialog.left_folder = 左方資料夾 prefs_dialog.right_folder = 右方資料夾 prefs_dialog.last_folder = 上次所在的資料夾 prefs_dialog.custom_folder = 自訂資料夾 prefs_dialog.show_hidden_files = 顯示隱藏檔案 prefs_dialog.show_ds_store_files = 顯示 .DS_Store 檔案 prefs_dialog.show_system_folders = 顯示系統資料夾 prefs_dialog.compact_file_size = 顯示檔案約略大小 prefs_dialog.follow_symlinks_when_cd = 切換目前資料夾時跟隨符號鏈結 prefs_dialog.appearance_tab = 呈現 prefs_dialog.look_and_feel = Look & Feel prefs_dialog.icons_size = 圖示大小 prefs_dialog.toolbar_icons = 工具列 prefs_dialog.command_bar_icons = 指令列 prefs_dialog.file_icons = 檔案類型 prefs_dialog.use_system_file_icons = 使用系統檔案的小圖示 prefs_dialog.use_system_file_icons.always = 總是 prefs_dialog.use_system_file_icons.never = 從不 prefs_dialog.use_system_file_icons.applications = 只限應用程式 prefs_dialog.edit_current_theme = 編輯目前佈景主題... prefs_dialog.themes = 佈景主題 prefs_dialog.import_theme = 匯入佈景主題 prefs_dialog.import_look_and_feel = 匯入 look & feel prefs_dialog.no_look_and_feel = 找不到 look & feel. prefs_dialog.error_in_import = 匯入佈景主題時發生錯誤 %1. prefs_dialog.cannot_read_theme = 無法載入佈景主題從檔案 %1 prefs_dialog.export_theme = 匯出 %1 prefs_dialog.import = 匯入 prefs_dialog.export = 匯出 prefs_dialog.theme_type = 類型: %1 prefs_dialog.delete_theme = 永遠刪除佈景主題 %1 ? prefs_dialog.delete_look_and_feel = 永久刪除 look & feel %1 ? prefs_dialog.rename_failed = 無法更名佈景主題 %1 prefs_dialog.xml_file = XML 檔案 prefs_dialog.jar_file = JAR 檔案 prefs_dialog.mail_tab = 郵件 prefs_dialog.mail_settings = 外送郵件設定 prefs_dialog.mail_name = 你的名稱 prefs_dialog.mail_address = 你的電郵地址 prefs_dialog.mail_server = SMTP 伺服器 prefs_dialog.misc_tab = 其它 prefs_dialog.use_brushed_metal = 使用 'brushed metal' 風格 (需重新啟動) prefs_dialog.confirm_on_quit = 離開時顯示確認對話盒 prefs_dialog.default_shell = 使用預設的系統 shell prefs_dialog.custom_shell = 使用自訂 shell prefs_dialog.shell_encoding = Shell 編碼 prefs_dialog.auto_detect_shell_encoding = 自動偵測 prefs_dialog.enable_bonjour_discovery = 開啟 Bonjour 服務尋找功能 prefs_dialog.enable_system_notifications = 啟用系統提醒功能 debug_console_dialog.level = 程度 unit.byte = byte unit.bytes = bytes unit.bytes_short = b unit.kb = KB unit.mb = MB unit.gb = GB unit.tb = TB unit.speed = %1/s duration.seconds = %1s duration.minutes = %1m duration.hours = %1h duration.days = %1d duration.months = %1mo duration.years = %1y theme.custom_theme = 自訂佈景主題 theme.custom = 自訂 theme.built_in = 內建 theme.add_on = 外掛 theme.current = 目前 theme_could_not_be_loaded = 在載入此佈景主題時發生錯誤 setup.title = 歡迎來到 trolCommander setup.intro = 請選擇你所希望的 trolCommander 行為模式 setup.look_and_feel = 選擇你要的 Look & Feel setup.theme = 選擇你的佈景主題 font_chooser.font_size = 大小 font_chooser.font_bold = 粗體 font_chooser.font_italic = 斜體 color_chooser.red = 紅色 color_chooser.green = 綠色 color_chooser.blue = 藍色 color_chooser.hue = 色調 color_chooser.brightness = 明亮度 color_chooser.swatches = 樣本 color_chooser.saturation = 飽和度 color_chooser.rgb = RGB color_chooser.hsb = HSB color_chooser.recent = 最近 color_chooser.alpha = Alpha 透明度 color_chooser.title = 挑選一個顏色 batch_rename_dialog.mask = 改名樣式 batch_rename_dialog.search_replace = 尋找並取代 batch_rename_dialog.search_for = 尋找 batch_rename_dialog.replace_with = 取代為 batch_rename_dialog.counter = 計數器 batch_rename_dialog.start_at = 開始於 batch_rename_dialog.step_by = 每次漸增 batch_rename_dialog.format = 格式 batch_rename_dialog.upper_lower_case = 大小寫 batch_rename_dialog.no_change = 無變更 batch_rename_dialog.lower_case = 小寫 batch_rename_dialog.upper_case = 大寫 batch_rename_dialog.first_upper = 首字大寫 batch_rename_dialog.word = 每個詞的首字 batch_rename_dialog.old_name = 原名稱 batch_rename_dialog.new_name = 新名稱 batch_rename_dialog.block_name = 保留 batch_rename_dialog.range = 範圍 batch_rename_dialog.proceed_renaming = %1 個檔案(全部 %2 個)將會被重新命名. 請確認要執行 ? batch_rename_dialog.duplicate_names = 重覆的名稱 ! parent_folders_quick_list.empty_message = 目前位置沒有上一層 recent_locations_quick_list.empty_message = 沒有最近開啟位置 recent_executed_files_quick_list.empty_message = 沒有最近執行檔案 #server_connect_dialog.auth_error = 不合法的帳號或密碼 #move_dialog.cannot_move_to_itself = 無法搬移檔案至子目錄 #pack.error_on_file = 當壓縮時發生錯誤 %1 #pack_dialog.cannot_write = 無法在目的資料夾產生壓縮檔 #unpack.unable_to_open_zip = 無法打開壓縮檔 %1 #image_viewer.previous_image = 前一個圖片 #image_viewer.next_image = 後一個圖片 #mkdir_dialog.title = 新增資料夾 #mkdir_dialog.error_title = 新增資料夾錯誤 #edit_bookmarks_dialog.remove = 移除 #mkdir_dialog.description = 新增資料夾 #done = 完成 #progress_dialog.hide = 隱藏 #move_dialog.rename_description = 重新命名檔案為 #theme_editor.shell_font = Shell 字型 #theme_editor.history_font = 歷史記錄字型 #theme_editor.shell_colors = Shell 顏色 #theme_editor.history_colors = 歷史記錄顏色 #ToggleHiddenFiles.hide = 不顯示隱藏檔案 #auth_dialog.error_was = 錯誤為: %1 #table.hide_column = 隱藏欄位 #delete.symlink_warning_title = 發現符號連結 #delete.symlink_warning = 此檔案可能為符號連結:\n\n 檔案: %1\n 連結至: %2\n\n只刪除符號連結或\n根據連結來刪除資料夾 (小心使用) ? #delete.delete_link_only = 刪除連結 #delete.delete_linked_folder = 刪除資料夾 #Unpack.label = 解壓縮檔案 #Pack.label = 壓縮檔案 ================================================ FILE: src/main/resources/license.txt ================================================ ABSTRACT trolCommander is released under the terms of the GNU General Public License found below. The trolCommander Ant tools and trolCommander Configuration API are released separately under the terms of the Lesser General Public License which permits use of the libraries in proprietary programs under certain conditions. Additionally, trolCommander uses the following third party works: - the Ant library under the terms of the Apache License - Apache Commons libraries under the terms of the Apache License - Apache Hadoop under the terms of the Apache License - the Furbelow library under the terms of the GNU Lesser General Public License - the ICU4J library under the terms of the ICU License - the J2SSH library under the terms of the GNU Lesser General Public License - the J7Zip library under the terms of the GNU Lesser General Public License - the jCIFS library under the terms of the GNU Lesser General Public License - the JetS3t library under the terms of the Apache License - the JmDNS library under the terms of the GNU Lesser General Public License - the JNA library under the terms of the GNU Lesser General Public License - the JUnRar library under the terms of the GNU Lesser General Public License - the Yanfs library under the terms of the Berkeley Software Distribution License - Mark James' icons under the terms of the Creative Commons Attribution License All the above mentioned licenses are included in this file. Copyright (C) 2002-2010 Maxence Bernard ------------------------------------------------------------------------- GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. ------------------------------------------------------------------------- GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ------------------------------------------------------------------------- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ------------------------------------------------------------------------- Berkeley Software Distribution (BSD) License Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the java.net nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------------------- Creative Commons Attribution License 2.5 http://creativecommons.org/licenses/by/2.5/ You are free: - to Share - to copy, distribute and transmit the work - to Remix - to adapt the work Under the following conditions: - Attribution. You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). For any reuse or distribution, you must make clear to others the license terms of this work. The best way to do this is with a link to http://creativecommons.org/licenses/by/2.5/ . Any of the above conditions can be waived if you get permission from the copyright holder. Nothing in this license impairs or restricts the author's moral rights. ------------------------------------------------------------------------- ICU License - ICU 1.8.1 and later COPYRIGHT AND PERMISSION NOTICE Copyright (c) 1995-2006 International Business Machines Corporation and others All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, provided that the above copyright notice(s) and this permission notice appear in all copies of the Software and that both the above copyright notice(s) and this permission notice appear in supporting documentation. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization of the copyright holder. ================================================ FILE: src/main/resources/logback.xml ================================================ %4relative %highlight(%5p) [%-17thread] - %-200msg - %d %logger{30} \(%file:%line\)%n%ex{full} true target/trolCommander.log %4relative %5p [%-17thread] - %-200msg - %d %logger{30} \(%file:%line\)%n%ex{full} ================================================ FILE: src/main/resources/themes/ClassicCommander.xml ================================================ ================================================ FILE: src/main/resources/themes/Native.xml ================================================ ================================================ FILE: src/main/resources/themes/RetroCommander.xml ================================================

    ================================================ FILE: src/main/resources/themes/Striped.xml ================================================
    ================================================ FILE: src/main/resources/themes/Trol.xml ================================================
    ================================================ FILE: src/main/resources/themes/editor/Dark.xml ================================================